JVM学习笔记 -- 垃圾收集器

时间:2023-01-02 08:50:43

  垃圾收集(Garbage Collection,GC)需要考虑3件事:

1、哪些内存需要回收

2、什么时候回收

3、如何回收

  Java内存运行时区域中,程序计数器、虚拟机栈、本地方法栈3个区域生命周期与线程相同,这几个区域的内存分配和回收都具备确定性,不需要考虑回收的问题,在方法结束或线程结束后内存自然跟着回收。Java堆和方法区在运行期才能知道创建哪些对象,这部分内存的分配和回收是动态的,,垃圾收集器关注的是这部分内存。

1、判断对象是否存活

  在堆中存放着大部分的对象实例,在回收内存前需判定这些对象哪些还存活着。

1.1 引用计数算法

  引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1,当引用失效时则计数器值减1,任何时刻计数器为0的对象就是不可能再被使用的。

  主流JVM没有选用引用计数算法来管理内存,最主要的原因是它很难解决对象之间互相循环引用的问题:

 1 public class Test1 {
 2 
 3     public Object instance = null;
 4 
 5     public static void main(String[] args) {
 6         Test1 objA = new Test1();
 7         Test1 objB = new Test1();
 8         objA.instance = objB;
 9         objB.instance = objA;
10         objA = null;
11         objB = null;
12     }
13 }

  如果使用引用计数器,对于objA指向的对象,在代码第6行计数器加1,第9行计数器加1,第10行计数器减1,这样虽然这个对象之后不会再被使用到,但引用计数器无法通知GC收集器回收内存。

1.2 可达性分析算法

  基本思路:通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,即GC Roots到这个对象不可达时,说明此对象是不可用的。

  在Java中,可作为GC Roots的对象包括:

1、虚拟机栈(栈帧中的本地变量表)中引用的对象

2、方法区中类静态属性引用的对象

3、方法区中常量引用的对象

4、本地方法栈中JNI(Native方法)引用的对象

  在引用计数算法的那个示例中,objA指向的对象一开始是被objA这个引用使用,因为objA是在本地变量表中,所以objA指向的对象是GC Roots的对象。之后objA不再指向那个对象,那个对象只被objB.instance引用,因为这个对象没有了到GC Roots的引用链,所以可以被回收。

1.3 引用类型

  判断对象是否存活与"引用"有关,在JDK1.2之后,Java对引用的概念进行了扩充,将引用按强度依次逐渐减弱分为4种:

1、强引用:在程序中类似"Object obj = new Object()"这类的引用,只要强引用海存在,垃圾收集器永远不会回收掉被引用的对象。

2、软引用:描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。

3、弱引用:也是描述非必需对象的,但是强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。

4、虚引用:最弱的一种引用关系,无法通过虚引用来取得一个对象实例。

1.4 何时回收

  真正宣告一个对象死亡,至少要经历2次标记过程:对象在可达性分析时发现没有与GC Roots相连接的引用链,那它将第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有finalize()方法或者这个对象的finalize()方法已经被虚拟机调用过,这2种情况为"没有必要执行"。

  如果这个对象没必须要执行finalize(),则会在下一次回收时被回收内存。如果被判定为由必要执行,这个对象会放在一个F-Queue队列中,在稍后会由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里执行是指虚拟机会触发这个对象的finalize()方法,但不会等待它运行结束,因为如果一个对象在finalize()方法中执行缓慢或发生死循环,那么F-Queue队列的其他对象可能永久处于等待,导致整个内存回收系统崩溃。稍后GC将对F-Queue中的对象进行第2次小规模的标记,如果对象重新与引用链上的对象建立关联,那么可以避免被回收,finalize()方法是对象逃脱死亡的最后一次机会。

1.5 回收方法区

  Java虚拟机规范中不要求虚拟机在方法区(或者HotSpot虚拟机中的永久代)实现垃圾收集,而且在永久代的垃圾收集效率是很低的。永久代的垃圾收集主要回收2部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象类似,当常量没有被引用时就可以被回收。而判断类是否无用需要同时满足下面3个条件:

1、该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例。

2、加载该类的ClassLoader已经被回收。

3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问到该类的方法。

2、垃圾收集算法

2.1 标记-清除算法

  这个是最基础的收集算法,其他算法都是基于这种思路并对其不足进行改进而得到的。算法思想:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这个算法主要有2个不足:

1、效率问题,标记和清除的效率都不高

2、空间问题,标记清除后会产生大量不连续的内存碎片

2.2 标记-整理算法

  标记-整理算法是根据老年代的特点提出的:首先标记所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

JVM学习笔记 -- 垃圾收集器

2.3 分代收集算法

  根据对象存活周期的不同将内存划分为几块,一般把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法,新生代的对象多次垃圾回收都没有回收对应的内存会移到老年代中,默认新生代和老年代比例是1:2。新生代中每次垃圾收集都只有少量对象存活,选用复制算法。老年代中因为对象存活率高,使用标记-清理或标记-整理算法来回收。

2.4 复制算法

  将可用内存按容量大小划分为大小相等的2块,每次只使用其中的一块,当这一块的内存用完了,将还存活的对象复制到另外一块上,然后再把已使用的内存空间一次清理掉。这样每次都是对整个半区进行内存是收集,内存分配不用考虑内存碎片等复杂情况。

  现在的商用虚拟机使用复制算法来回收新生代,不按照1:1的比例来划分内存空间,而是分为8:1:1的Eden:From Survivor:To Survivor3个空间,每次使用Eden和其中一块Survivor,回收时将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理Eden和用过的Survivor对象。不能保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时,需要依赖老年代进行分配担保。