目录
JVM内存区域划分
1、堆(线程共享)
2、方法区(线程共享)
3、栈(线程私有)
4、程序计数器(线程私有)
JVM类加载机制
加载
验证
准备
解析
初始化
双亲委派模型
JVM垃圾回收机制(GC)
1、寻找
引用计数法(Python、PHP采用)
可达性分析(Java采用)
2、释放
标记清除
复制算法
标记整理
分代回收
此篇学习笔记总结包括了3个方面的知识点:
1、JVM内存区域划分
2、JVM类加载机制
3、JVM垃圾回收机制
后面就将重点来介绍这3个方面的知识收获
JVM内存区域划分
JVM内存区域划分可以分为四部分:堆、栈、方法区(元数据区)、程序计数器
1、堆(线程共享)
堆的作用:程序中创建的所有对象都在保存在堆中,即new 出来的所有对象(全局变量、成员方法)
堆是JVM 中最大的内存区域
堆在JVM进程中,一个进程只有一个堆,进程内所有的线程共享一个堆
2、方法区(线程共享)
方法区在JVM进程中,一个进程只有一个方法区,进程内所有的线程共享一个方法区
3、栈(线程私有)
栈的作用:维护方法之间的调用关系(局部变量)
栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中
一个JVM进程中,可以有多个线程,每一个线程都包含一个栈
4、程序计数器(线程私有)
一个JVM进程中,可以有多个线程,每一个线程都包含一个程序计数器
举例:
public class test {
public int a; // 全局变量(也可叫做test这个类的成员变量)
public static int b; // 静态变量
public void method() {
int c = 0; // 局部变量
System.out.println("这是test这个类的一个普通成员方法");
}
public static void run() {
System.out.println("这是一个静态方法!"); // 静态方法不需要借助实例化对象
}
public static void main(String[] args) {
test test1 = new test(); // test1 与 test2 是引用类型,也是局部变量
test test2 = new test(); // new test是一个实例化对象
}
}
堆中存放的有:全局变量a、成员方法method、new test()
栈中存放的有:局部变量c、test1、test2
方法区中存放的有:静态变量b、静态方法run
JVM类加载机制
类加载可以理解为把.class文件从文件 (硬盘) 被加载到内存 (元数据区) 的过程
加载
把.class文件找到,找到文件并把文件内容读到内存中去
在寻找.class文件的过程中,需要使用到双亲委派模型来寻找,双亲委派模型在下面文章讲解
验证
检查.class文件格式是否符合规范要求(JVM规范中明确描述了)
准备
给类对象分配内存空间(未初始化的空间,内存空间的数据是全0的)
类加载的目的就是得到类对象
解析
针对字符串常量进行初始化,也就是Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
具体解析过程如下:
初始化
针对类对象进行初始化(初始化静态成员、执行静态代码块,类要是有父类,还需要加载父类)
类加载机制是一个“懒加载”的策略(懒汉模式),非必要不加载,不是JVM一启动就把所有的.class都加载了。
必要时机进行类加载:
1、创建了这个类的实例
2、使用了这个类的静态方法/静态属性
3、使用子类会触发父类的加载
双亲委派模型
双亲委派模型的作用就是在进行加载过程中,找.class文件这个过程。
在JVM加载的过程中需要使用到类加载器,JVM里面内置了三个类加载器:
BootStrap ClassLoader 负责加载Java标准库中的类
Extension ClassLoader 负责加载一些非标准的但是Sun / Oracle扩展的库的类Application ClassLoader 负责加载项目中自己写的类以及第三方库中的类
它们三者关系如下:
双亲委派模型的原理:当我们需要去加载一个具体类时,不会去扫描自己的目录,而是优先去扫描父类加载器,直到扫描到最后一个父类加载器。最后一个父类加载器若扫描到具体类就进行后续加载步骤,若没有扫描到就交给自己孩子来处理。
举例:若我们想要去加载"java.lang.String"这个类时,扫描过程如下:
按照双亲委派模型的规则来工作的最大目的是为了防止程序员自己写一个特殊类,把标准类库里面的类给覆盖了
双亲委派模型也是可以打破~~
你自己实现的类加载器,可以继续遵守~~也可以不遵守~~(Tomcat里针对webapp的类加载器就没遵守)
JVM垃圾回收机制(GC)
在JVM 内存区域中,
1、栈随着线程一起销毁,当方法调用完毕,方法的局部变量自然随着出栈操作就销毁了
2、程序计数器就是一个单纯存地址的整数,也是随着线程一起销毁
3、方法区(元数据区)存放的类对象,很少会卸载
4、堆(new 出来的对象)存放的是全局变量,因此GC回收的目标主要是堆,GC是以 对象 为单位进行释放
JVM垃圾回收机制主要分为2个阶段:
1、寻找:谁是垃圾
2、释放:把垃圾对象的内存给释放掉
1、寻找
一个对象判断是否是垃圾,只需要判断其是否有引用指向对象即可,若无引用指向对象,则该对象可以被视认为垃圾。
在垃圾回收机制中,对于如何判断一个对象是否有引用指向主要有2种方法:
1.引用计数法 (Python 、 PHP编程语言采用的这种方法)
2.可达性分析 (Java 采用的此方法)
在《深入理解Java虚拟机》这本书中,这两种办法都有提到。
- 那么如果我们在面试中被问到:在垃圾回收机制中,判断对象是不是垃圾有哪几种方法?你可以说引用计数法和可达性分析两种方法
- 如果问的是:在Java的垃圾回收机制中,如何判断对象是不是垃圾,你只需回答“可达性分析”就行。
引用计数法(Python、PHP采用)
引用计数法就是在对象里面安排一个额外的空间,保存一个整数,来记录指向该对象的引用个数。每个对象都有一个单独的计数器,不是每一个类独有。
若引用个数为0,说明该对象没有引用指向就可以便认为是垃圾
随着引用的增加,计数器就随着增加;引用的销毁,计数器就随着减少
当计数器为0时,则认为该对象没有引用了,可以当做垃圾进行回收
引用计数法的缺点:
1、需要额外安排空间来保存引用计数器,浪费内存空间
2、存在循环引用的情况,会导致引用计数的判定逻辑出错
可达性分析(Java采用)
可达性分析可以理解为是否可以达到访问这个对象。若能够访问到这个对象,则认为此对象不是垃圾,若不能到达该对象则认为此对象是垃圾,进行回收。
拿二叉树来举例,通过根节点root可以访问到整棵树的任意节点。
因此在进行可达性分析的时候,需要确定遍历到达的起点:
1)栈上的局部变量(每个栈的每个局部变量都是起点)
2) 常量池中引用的对象
3)方法区中静态成员引用的对象
可达性分析,总的来说就是从所有的起点出发,看看该对象里又通过引用能访问到哪些对象,顺藤摸瓜把所有可访问到达的对象都遍历一遍(遍历的同时把对象标记成“可达”),剩下的自然就是“不可达的”。
可达性分析克服了引用计数的两个缺点,但也有自己的问题:
1、消耗更多的时间,因此某个对象成了垃圾,也不一定能第一时间发现,因为在扫描过程中是需要消耗时间的
2、在进行可达性分析的时候,要顺藤摸瓜,一旦在这个过程中,当前代码中的对象的引用关系发生了变化,就十分麻烦(于是,为了更准确的完成这个顺藤摸瓜的过程,就需要让其他业务线程暂停工作,此时便引来STW问题)
2、释放
标记清除
复制算法
复制算法的缺点:复制算法虽然解决了内存碎片问题,但也造成了如下问题:
1、内存利用率比较低
2、如果当前大部分对象都是需要保留的,只有少部分垃圾,此时复制的层本比较高
标记整理
分代回收