JVM垃圾收集器与内存分配策略

JVM系列文章索引:
一、HotSpot简要概述
二、JVM体系结构概述
三、虚拟机类加载机制
四、虚拟机字节码执行引擎
五、JVM运行时数据区域
六、内存溢出故障分析
七、垃圾收集器与内存分配策略
八、JVM调优



JVM垃圾收集器与内存分配策略
Java内存运行时区域中的程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模型的讨论里,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了

而Java堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,本文后续讨论中的“内存”分配与回收也仅仅特指这一部分内存。


1 如何判断对象已死

垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了。

1.1 引用计数算法

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

应用:微软COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer、Python语言等。

优点:实现简单,判定效率高
缺点:难以处理很多特殊情况,例如对象之间相互循环引用的问题

Java虚拟机并不是通过引用计数算法来判断对象是否存活的。

1.2 可达性分析算法

通过根对象“GC Roots”作为起始节点集,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,即从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

例如下图,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。
JVM垃圾收集器与内存分配策略

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
·所有被同步锁(synchronized关键字)持有的对象。
·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

Java虚拟机正是通过可达性分析算法来判断对象是否存活的。

1.3 四种引用类型

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

  • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Objectobj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
  • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。 在JDK 1.2版之后提供了PhantomReference类来实现虚引用。

1.4 拯救不可达对象

即使在可达性分析算法中被判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:可达性分析判断是否有与GC Roots相连接的引用链,如果没有,将会被第一次标记,判断此对象是否执行finalize()方法,如果没有,将会被第二次标记,至此虚拟机将真正将该对象判定为死亡。

通过finalize()来拯救不可达对象的这种方法已被官方明确声明为不推荐使用的语法

1.5 方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件比较苛刻了。需要同时满足下面三个条件:
·该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
·加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景。
·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,而由于上面提到的那些苛刻的判定条件,方法区进行垃圾收集的性价比通常比较低的。因此可以不要求虚拟机在方法区中实现垃圾收集


2 四种垃圾收集算法

以下所有算法均属于追踪式垃圾收集的范畴。

2.1 分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论,它包括三个分代假说:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

设计原则
收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

这样做的好处:
如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;
如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有
效利用。

概念定义:
■ 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。(另外请注意“Major GC”这个说法在不同资料上常有不同所指,需按上下文区分到底是指老年代的收集还是整堆收集)
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。

■ 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

2.2 标记-清除算法

主要思想:
分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。

标记-清除算法的执行过程如图所示:
JVM垃圾收集器与内存分配策略

缺点:
1.执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
2.内存空间的碎片化,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.3 标记-复制算法

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代

主要思想:
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
当Survivor空间不足以容纳一次Minor GC之后存活的对象时,将通过分配担保(Handle Promotion)机制将它们移入老年代。

如下图所示,HotSpot虚拟机默认Eden和Survivor0、Survivor1的大小比例是8∶1:1,新生代和老年代的大小比例是1:2.
JVM垃圾收集器与内存分配策略
标记-复制算法的执行过程如图所示:
JVM垃圾收集器与内存分配策略

优点:
实现简单,运行高效,不存在空间碎片。

由于每次多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

缺点:
这样将可用内存缩小为了原来的一半,空间浪费较多。
对象存活率较高时效率下降。

2.4 标记-整理算法

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

主要思想:
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

标记-整理算法的执行过程如图所示:
JVM垃圾收集器与内存分配策略
优点:
解决了空间碎片化问题,降低了内存分配的难度。

缺点:
移动过程造成了额外的开销。


3 七种垃圾收集器

JVM垃圾收集器与内存分配策略
各款经典收集器之间的关系如上图所示。如果两个收集器之间存在连线,就说明它们可以搭配使用。

新生代:
JVM垃圾收集器与内存分配策略
老年代:
JVM垃圾收集器与内存分配策略
Garbage First(G1):
JVM垃圾收集器与内存分配策略