《深入理解Java虚拟机》读书笔记——GC原理

概述

Java内存运行时区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,在方法结束或者线程结束时,内存自然就被回收了。

而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在运行时才知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器关注的也是这部分内存。

可达性分析算法

主流商用程序语言(Java,C#甚至包括Lisp)都是通过可达性分析来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots的没有任何引用链相连时,则证明此对象是不可用的。

可作为GC Roots的对象包括下面几种:

  1. 虚拟机栈中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI引用的对象

强引用,软引用,弱引用,虚引用

  • 强引用:垃圾收集器永远不会回收掉强引用对象
  • 软引用:用来描述一些有用但非比需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收
  • 弱引用:弱引用关联的对象只能生存到下一次垃圾收集发生之前,无论内存是否足够,都会被回收掉
  • 虚引用:为一个对象设置虚引用关联的唯一目的就是在这个对象被收集器回收时收到一个系统通知

垃圾收集算法

标记-清除算法

该算法分为”标记”和”清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。

不足:1. 标记和清除效率低;2. 清除之后产生大量不连续的内存碎片,容易频繁出发GC。

复制算法

该算法将可用内存按容量分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,九江还存活着的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。
现在的商业虚拟机都采用这种收集算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。

标记-整理

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率变低,因此老年代一般不直接选用这种算法。根据老年代的特点,提出了标记-整理算法,标记过程和标记-清理算法一样,后序步骤是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

一般把Java堆分成新生代和老年代,新生代选用复制算法,老年代算用标记-清理或者标记-整理算法。

内存分配策略

大多数情况下,对象在新生代Eden区中分配。当Eden区中没有足够空间进行分配时,虚拟机将发起一次Minor GC。大对象直接进入老年代,所谓大对象是指需要大量连续内存空间的Java对象,典型的就是那种很长的字符串以及数组。此外,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor对象中,并且对象年龄设为1。对象在Survivor区中每经过一次Minor GC,年龄就加1,当年龄增加到一定程度(默认15岁),就将会进入老年代。

Minor GC:指发生在新生代的垃圾收集动作。

Major GC/Full GC:指发生在老年代的GC,Major GC的速度一般比Minor GC慢10倍以上。