JVM垃圾收集与内存分配

博客内容为深入<<理解理解Java虚拟机>>第三章读书笔记。

一、判断对象存活算法

判断对象存活的算法书主要包括引用计数算法可达性分析算法

引用计数算法

引用计数算法就是给对象添加一个引用计数器,每当一个地方引用对象时计数器值加一,引用失效时计数器值减一,计数器值为0的对象就是比克使用的。引用计数算法实现简单、判断效率很高,但很难解决对象间的相互循环引用问题。

可达性分析算法

基本思路是通过一系列称为”GC Roots”的对象作为起始点,从这些节点开始往下搜索,当一个对象与GC Roots是不可达时,则证明此对象是不可用的。Java中可作为GC Roots对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法中引用的对象

垃圾收集算法

标记-清除算法

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

1、一个是标记和清除过程效率低;
2、另一个是在标记清除之后会产生大量不连续的内存碎片(可能因分配较大对象时无法找到足够大的连续内存而不得不提前触发垃圾回收)。

复制算法

算法既将可用内存按容量划分为大小相等的两块,每次只使用其中的一块;当这一块内存用完了,就将还存活的对象复制到另一块上面,然后将这一块已使用的内存空间一次性清理掉。复制算法常用来回收新生代,由于新生代中对象大部分是”朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块的Survivor。回收时候将Eden空间和Survivor中还存活的对象复制到另一块Survivior空间上。HotSpot虚拟机默认Eden和Survivor大小比例为8:1。注意由于无法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时,需要依赖老年代进行分配担保,对象直接进入老年代。

标记-整理算法

标记-整理算法在标记后,让所有的存活的对象向一端移动,然后直接清理掉边界以外的内存。标记-整理算法用于老年代的垃圾回收。

垃圾收集器

垃圾收集器是内存回收的具体实现,下图是七种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们之间可以搭配使用。
garbage collect

Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,主要的特点包括:

1、Serial收集器为单线程收集器,在其进行垃圾收集时,必须暂停其他所有的工作线程;由虚拟机在后台自动发起和完成的,在用户不可见的情况下把用户正常工作的线程全部停掉。
2、虚拟机在Client模式下的默认新生代收集器;因为Serial收集器简单高效(与其他收集器的单线程相比),同时在用户的桌面应用场景中,分配给虚拟机管理的内存不会太大(分配给新生代的就更少),所以停顿时间通常是可以接受的。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,虽然与Serial相比没有太多的创新之处,但ParNew收集器是运行在Server模式下的虚拟机中首选新生代收集器,其中一个重要原因是目前除了Serial收集器,只有它能与CMS收集器配合工作。

Parallel Scavenge收集器

Parallel Scavenge 收集器是一个新生代收集器,是使用复制算法能够并行的多线程收集器。CMS等收集器的关注点是尽可能地缩短垃圾收集时的用户停顿时间,而Parallel Scavenge的目标是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。GC停顿时间缩短是牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB肯定比收集500MB肯定要快,这也导致垃圾收集更频繁一些,原来10S收集一次、每次停顿100毫秒,现在变成5s收集一次、每次停顿70毫秒;停顿时间缩短,但吞吐量也降下来了。

Serila Old收集器

Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用”标记-整理”算法,主要意义也是在Client模式下的虚拟机使用。如果是在Server模式下Serial Old收集器还有两种用途:一中是在JDK1.5以及之前佛如版本中与Parallel Scavenge收集器搭配使用;另一种用途是作为CMS 收集器的预备方案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和”标记-整理”算法,在JDK1.6之后才开始提供使用。

CMS收集器

CMS(Concurrent Mark Sweep)收集器基于”标记-清除”算法实现,以最短时间回收停顿时间为目标。CMS收集器的整个运作过程分为四个部分,包括:

1、初始标记
2、并发标记
3、重新标记
4、并发清除

其中初始标记和重新标记过程仍需要”Stop The World”。初始标记仅仅只是标记一下与GC Roots崩直接关联到的对象,速度很快,并发标记阶段进行GC Roots Tracing的过程,而重新标记是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分的对象的标记记录。整个过程中耗时最长的并发标记和并发清除阶段都可以和用户线程一起工作。CMS收集器存在三个明显的缺点:

1、CMS收集器对CPU资源非常敏感。在并发阶段,CMS收集器虽然不会导致用户线程停顿,但是会占用一部分CPU资源而使用户线程变慢,总吞吐量会降低。
2、CMS收集器无法处理浮动垃圾,可能会出现”Conncurrent Mode Failure”出现而导致另一次的Full GC的产生。浮动垃圾是指在并发清理阶段用户线程产生的垃圾,由于其产生在标记过程之后,所以只好留到下一次GC时再清理掉,所以称为浮动垃圾。注意的是垃圾收集阶段用户线程还需要运行,那也就还要留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器一样等到老年代几乎全部填满时候再进行收集。在JDK1.6中CMS收集器的默认启动阀值提高到了92%。如果CMS运行期间预留的内存无法满足程序的需要,就会出现”Concurrent Mode Failure”失败,这时候虚拟机将启动预备方案:临时启动 Serial Old收集器来重新进行老年代的垃圾回收,这样停顿时间就长了。
3、CMS基于”标记-清除”算法,会有大量的空间碎片产生。

G1收集器

G1是一款面向服务端应用的垃圾收集器,与其他GC收集器相比具有如下的特点:

1、并行与并发:充分利用多CPU、多核环境的硬件优势,使用多CPU来缩短Stop-The-World的时间
2、分代收集
3、空间整合:从整体来看是基于”标记-整理”算法,从局部(两个Region之间)上是基于”复制”算法实现
4、可预测的停顿:除追求低停顿之外,还能建议可预测的停顿时间模型,能让使用者明确指定在长度为M毫秒的时间内消耗在垃圾收集的时间不得超过N毫秒。

在G1之前的其他收集器进行收集的范围都是整个新生代和老年代。使用G1收集器的整个Java内存布局和其他收集器有很大的差别,它将整个Java堆划分为大小相等的独立Region,虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。