上篇文章简单介绍了JVM的内存模型。而本文要介绍的java垃圾回收,两者之间具有非常紧密的关系。Java中的垃圾回收的机制,使得程序员不再需要像写C++程序那样手动的申请和释放内存空间,提高了编程的效率和正确率。作为一名技术控,有必要了解Java内存回收的机制。
2 可达性检测可达性检测,指的是,JVM如果判断一个对象应该被回收掉,即是判断对象是否存活的标准。目前的可达性检测主要有2种方式,分别是引用计数 和根搜算法。
2.1 引用计数内存中每一个对象都会有一个counter计数器。当该对象新增加一个引用时候,counter计数器的值加1。当释放一个引用的时候,counter计数器减1。当该对象的counter计数器的值为0的时候,就可以进行回收。这种方法非常简单,但是有一个缺陷,就是无法解决循环引用的问题。
2.2 根搜算法从GC root根据引用关系来对堆内存空间进行遍历,找到存活的对象并对其进行标记(mark)。整个标记过程结束之后,对未标记的内存空间进行回收。这种方式能够解决对象循环引用的问题。其中GC root包含如下内容:
- Java虚拟机栈中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法区中JNI的引用的对象
目前的垃圾回收算法,本质上有3类,分别是标记-清除,复制,标记-整理。有人把分代垃圾回收也当成一种算法,其本质上是前边集中算法的组合。
3.1 标记-清除(Mark-Sweep)算法该算法分为标记阶段和清除阶段。在标记阶段中,将内存中需要进行回收的区域进行标记。之后对所有标记的区域进行清除。该算法是最基本的一种算法,也为其他算法提供了思路。
标记-清除算法的缺点就是会导致内存空间物理上不连续,产生大量的碎片。如果之后需要分配较大的连续空间,会容易产生内存不足的情况,从而触发新的垃圾回收。
3.2 复制算法复制算法就是将区域A中的存活的对象复制到区域B中,之后对区域A进行整体的清除。
这种方法对于新生代的内存区域有较好的效果,因为其中存活的对象不多,复制操作的成本较低,性能较好。如果在老年代中使用,则会大打折扣,因为存活对象较多,每次回收都要复制很多的对象。
3.3 标记-整理算法标记-整理算法是基于标记-清除的思想。与之不同的是,在标记阶段完成后不是直接进行清理操作,而是把存活的对象向内存区域的一端进行移动,之后清除掉边界之外的内存区域。
这种方法解决了标记-清除算法中产生内存碎片的问题,也避免了复制算法中内存利用率不高的问题。
4 垃圾回收器 4.1新生代收集器4.1.1 Serial收集器
Serial收集器属于新生代收集器,串行收集器。该收集器使用复制回收算法,只使用一个线程进行GC。该GC线程工作时候,其他业务线程无法进行,因为会产生较长的停顿时间,即Stop The World。
4.1.2 ParNew收集器
ParNew收集器属于新生代收集器,并行收集器。该收集器更关注STW时间。使用复制回收算法,使用多个线程进行GC,是Serial收集器的多线程版本。其GC线程运行时也会产生STW,但是相对Serial收集器性能有所提高。
4.1.3 Parallel Scavenge收集器
Parallel Scavenge收集器属于新生代收集器,并行收集器。相对与ParNew收集器,更加关注GC的吞吐量。该收集器中,可以通过参数-XX:+UseAdaptiveSizePolicy
开启自适应调节,JVM会根据当前系统的运行情况,动态对参数(Eden/Survivor比例,老年代对象年龄,新生代大小等)进行调整,从而获取最大的吞吐量或者合适的STW时间。还可以通过参数-XX:MaxGCPauseMillis
来控制GC的最大停顿时间,通过参数-XX:GCTimeRatio
来设置用户时间占总时间的比例(默认是99%)。
4.2.1 Serial Old收集器
Serial Old收集器属于串行收集器,是Serial收集器的老年代版本,使用一个线程进行GC。使用标记-整理算法,并且GC过程会产生停顿时间,其他工作线程无法运行。
4.2.2 Parallel Old收集器
该收集器属于并行收集器,使用多个线程进行GC,是Serial Old的多线程版本,Parallel Scavenge收集器的老年代版本。同Serial Old收集器一样,使用标记-整理算法,GC过程会产生停顿时间,其他工作线程无法运行。
4.2.3 CMS收集器
CMS(Concurrent Mark Sweep)收集器采用标记-清除回收算法,该收集器的目标是实现最短的回收停顿STW时间。所以该收集器非常适合于对响应速度要求严格的场景中。
该收集器的工作过程包含4个部分:
1. 初始标记
标记GC root能够直接关联到的对象。该过程存在回收停顿,但是运行的时间非常短。
2. 并发标记
从GC Root 开始对堆中对象进行可达性分析,耗时较长,但是与工作线程可以并发执行。
3. 重新标记
用来标记在并发标记过程中新产生的对象或者变动的对象,该过程存在回收停顿,其运行时间稍长与初始标记,但是远远小于并发标记过程的时长。
4. 并发清理
根据前3步的标记结果,对非存活的对象进行清理,释放内存空间。该过程与工作线程可以并发执行。
CMS收集器具有非常鲜明的特点,即并发收集,停顿时间短。但是也存在不足,因为并发过程中需要占用CPU的运行时间,从而对应用程序的性能存在短暂的影响。由于其采用标记-清理算法,会导致内存空间碎片的产生。
4.3 G1(Garbage First)收集器G1收集器是一种跨代收集器,能够管理新生代和老年代。原因是因为他将堆内存分成多个大小相等的独立区域(Region),在进行垃圾收集的时候都是按照Region进行的。收集器的优先列表和Remembered Set均是基于Region的思想。
收集器会跟踪每个Region中垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值)。G1会根据价值大小在后台维护一个优先列表。在进行GC的时候,收集器根据时间,会选择价值最大的Region进行回收。这种机制能够保证在有限的回收时间中获取最高的收集效率。
G1收集器的另外一个机制就是Remembered Set
,主要作用是避免全堆扫描。每个Region区域都有一个对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
该收集器的工作过程包含4个部分:
1. 初始标记
标记GC root能够直接关联到的对象。该过程存在回收停顿,但是运行的时间非常短。
2. 并发标记
从GC Root 开始对堆中对象进行可达性分析,耗时较长,但是与工作线程可以并发执行。
3. 最终标记
用来标记在并发标记过程中新产生的对象或者变动的对象。虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行(多线程)执行。
4. 筛选回收
对各个Region区域的回收价值和时间进行排序,结合用户提供的GC时间,确定最终GC的Region。该过程可以是并发的,但是因为时间是用户可控的,所以一般会进行回收停顿从而获得最大的GC吞吐量。
5 总结参考资料:
https://crowhawk.github.io/2017/08/15/jvm_3/
http://www.ityouknow.com/jvm/2017/08/29/GC-garbage-collection.html
https://www.jianshu.com/p/0f25565e1049