从零开始的jvm之垃圾收集器与内存分配策略

该篇源自于对《深入理解java虚拟机》的学习和总结。大牛拍砖请轻点。

在这里我们带着三个疑问去看这篇总结:

1、哪些内存需要回收?

2、什么时候回收?

3、怎么回收?

 

1、哪些内存需要回收?

解答:

回收的主要区域为堆,其次方法区。

解释:回顾之前的从零开始的jvm之内存管理机制,我们提到的运行时数据区域,除堆和方法区,其他的程序计数器,虚拟机栈、本地方法栈随线程的生而生,死而死,其内存分配和回收具备确定性,线程结束时,内存自然就跟着回收了。

1.1 堆

我们大多数开发者经常接触到的内存回收,指的基本上都是对堆的GC。

特点:回收性价比高。对新生代的回收率甚至可以达到“70-95%”的内存空间。

1.2 方法区

特点:回收性价比低。

回收内容:废弃常量、无用类。

废弃常量:

没有被其他对象引用,将可能被GC并清除出方法区的运行时常量池。

无用类:

1、堆中不存在该类的任何实例;

2、加载该类的classloader被回收;

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

要满足这些条件,jvm才可以对无用类进行回收。

2、什么时候回收?

解答:

这个回答可以说是五花八门,什么内存不够用,没有被引用的时候……等等,但究其根本来说即对象“死”的情况下回收。

那什么时候对象“死”了呢?

判断对象是否死了或者存活的方法:

2.1 引用计数法

原理:给对象添加一个引用计数器,有地方引用则+1,引用失效则-1,为0时则为该对象未被引用。

特点:实现简单,效率高。

缺点:很难解决对象之间相互循环引用的问题。

2.2 可达性分析法

原理:通过一些列“GC Roots”为起点,从这些节点向下搜索(这些搜索路径称作引用链),当一个对象到GC Roots没有任何引用链相连,则证明这个对象不可用。

应用:主流语言中使用,java也是使用该算法。

2.2.1 GC Roots对象:

1、虚拟机栈(栈帧中局部变量表)中引用的对象;

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

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

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

从零开始的jvm之垃圾收集器与内存分配策略

解释:上图中obj8,obj9,obj10均不可用,都需要被GC回收

2.3 生死存亡的判断与finalize

要真正宣布一个对象的死亡,至少要经过2次标记过程。

过程:先对对象进行可达性分析,若没有到GC Roots的引用链,将进行第一次标记和筛选。

筛选条件是该对象是否有必要执行finalize()方法,如果没有覆盖finalize()方法或者已经被jvm执行过,则视为没有必要执行。

a、有必要执行finalize()方法

将对象放置到F-queen中,由虚拟机创建的低优先级线程finalizer去执行。执行后,GC将对F-queen中的对象再一次进行标记,以判断是否需要回收。

finalizer线程特点:只会去触发对象的finalize()方法,但并不会等待方法执行结束。

设计目的:防止finalize()出现过于耗时或者死循环导致F-queen中其他对象等待时间过久。

3、怎么回收?

解答:

3.1 垃圾收集算法

当我们通过可达性分析知道哪些对象需要回收时。将采用垃圾收集算法进行回收。

3.1.1 标记-清除(Mark-Sweep)算法

实现过程:标记需要回收的对象,标记完成后统一回收被标记的对象。

缺点:效率不高、内存碎片化(内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作)。

从零开始的jvm之垃圾收集器与内存分配策略

3.1.2 复制(Copying)算法

设计目的:解决效率、碎片化的问题。

实现过程:它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。

缺点:内存缩小为了原来的一半、需要分配担保。

应用于分代回收算法中的新生代中的内存回收。

从零开始的jvm之垃圾收集器与内存分配策略

3.1.3 标记-整理(Mark-Compact)算法

实现过程:标记需要回收的对象,让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。

对比copy的优点:减少内存浪费,不需要分配担保(也没空间给他分配担保)。

应用于分代回收算法中的老年代中的内存回收。

从零开始的jvm之垃圾收集器与内存分配策略

3.1.4 分代收集算法

JVM将堆分成了二个大区新生代(Young)和老年代(Old),新生代又被进一步划分为Eden和Survivor区,而Survivor由FromSpace和ToSpace组成,也有些人喜欢用Survivor1和Survivor2来代替。JVM默认分配是8:1:1,每次调用Eden和其中的Survivor1(FromSpace),当发生回收的时候,将Eden和Survivor1(FromSpace)存活的对象复制到Survivor2(ToSpace),然后直接清理掉Eden和Survivor1的空间。

从零开始的jvm之垃圾收集器与内存分配策略

新生代:新创建的对象都是用新生代分配内存,Eden空间不足时,触发Minor GC,这时会把存活的对象转移进Survivor区。
老年代:老年代用于存放经过多次Minor GC之后依然存活的对象。

新生代的GC(Minor GC):新生代通常存活时间较短基于复制算法进行回收,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

老年代的GC(Major GC/Full GC):老年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此采用标记-整理算法来进行回收,用于减少内存碎片带来的效率损耗。