jvm随笔4-垃圾收集器

1.可达性分析时一致性确保

1.1 GC停顿

在确定对象是死亡还是存活时,需要进行可达性分析,而在进行可达性分析时需要进行GC停顿来确保一致性(如果不停定,导致引用持续变化,就无法准确定位对象的存活和死亡情况)。
而在进行GC停顿以后进行检查时,不需要一个不漏全部检查所有上下文和引用(不需要挨着查询全文来找对象索引),虚拟机使用一个OopMap的数据结构使虚拟机直接得知什么地方存放着对象引用(类加载完成后HotSpot把对象内在什么偏移上存放着怎样的数据计算出来),也会在特定位置上记录栈和寄存器中那些位置是索引。这样就保证了GC在扫描时可以直接得到这些索引,不需要自己去遍历全文寻找。(就像文中的重点已经被勾画出来并使用数据结构逐个记录重点在哪里,使用时就不需要读者自己再去读全文找重点,可以通过数据结构标识的地方直接找到。耗费空间让时间复杂度:O(n)–>O(1))

1.2 安全点

但很现实的一个问题,实际使用时,导致引用关系变化的指令非常多(从而导致OopMap内容发生变化),如果为每一条指令都生成一个OopMap显然是不合理的,这样需要大量的额外空间,使得GC空间成本极高(jvm并不打算引用变化一次就修改一次OopMap,而是每累计一段时间统一修改一次)。
事实上,jvm只在特殊的位置生成OopMap,这些地方称为安全点,所以说程序不是到任意位置都能进行GC,只有在安全点上才能开始GC,而安全点的数量要适宜(过少导致GC执行间隔过大,过多导致运行负荷增加),一般在方法调用,循环跳转,异常跳转这样的指令复用的地方产生安全点
而在多线程中,进行GC时要确保所有线程执行的程序都在安全点上,有两种方案来确保:

  • 抢先式中断:所有线程全部中断,不在安全点的线程恢复执行,直到跑到安全点(基本不用抢先式中断)(要GC时全部立即喊停,没到安全点的让自己再跑一下)
  • 主动式中断:为所有安全点设立标志,让线程主动去询问标志,若标志为真(要进行GC就把标志置真)则将自己中断挂起(要GC时让在安全点的工作人员开始工作,拦住过路的线程)

1.3 安全区域

使用安全点并不能完美解决问题,若线程处于阻塞或者挂起状态,无法响应中断,这时候就需要安全区域(安全点是点,安全区域就是线),安全区域内是一段代码片段,在这个片段内,引用关系都不会发生变化,进行GC都是安全的。
在线程进入安全区域后,会标记自己进入安全区域,在GC时不需要管这些线程,在线程出安全区域时,会主动询问GC是否完成,若完成就直接出去,未完成就阻塞等待。

2.垃圾收集器

垃圾收集器是内存回收的具体实现(各种垃圾收集器没有最好只有最合适)

2.1 Serial收集器

最基本的收集器,单线程的收集器, 采用复制算法,不仅是只使用单cpu单线程去回收垃圾,并且在回收时要暂停所有的工作线程,Serial收集器的优点是其简单而高效(同其他收集器的单线程相比,因为Serial收集器没有线程交互等的开销)。

2.2 ParNew收集器

多线程版本的Serial收集器,其目前只能配合Serial收集器和CMS收集器,由于其相对于Serial收集器,存在线程交互开销,所以在cpu较少时,其效率是低于Serial收集器的,当然随着CPU的增加,效率也会提高。

2.3 Parallel Scavenge收集器

使用复制算法,前面两种收集器,都有一个目标,就是减少每次GC的时间,但都是从软件方面降低时间,而Parallel Scavenge收集器通过减少GC一次回收的量来降低一次GC的时间,Parallel Scavenge收集器可以设置一次GC的停顿时间和吞吐量,在执行时通过控制每次垃圾的回收量来确保一次GC的时间,但**每次的回收的量过少会导致吞吐量的降低和空间不足。**吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)

2.4 Serial Old收集器

Serial的老年代版本,也是单线程的,使用标记-整理算法。
用途:

  • 与Parallel Scavenge收集器搭配使用
  • 作为CMS的后备预案(CMS回收会导致可用空间零散分布)

2.5 Parallel Old收集器

Parallel Old是Parallel Scavenge的用于回收老年代的版本使用多线程和标记-整理算法,产生的原因是因为Parallel Scavenge只能与Serial Old配合,而Serial Old单线程拖累了性能,所以就产生了Parallel Old来配合Parallel Scavenge。在注重吞吐量及CPU资源敏感的场合,适合使用Parallel Old+Parallel Scavenge的组合。

2.6 CMS收集器

CMS收集器使用标记-清除算法,整个过程分为四步,其中两步(并发标记与并发清除)允许工作线程一起,且这两步工作时间较长,使得GC真正停顿的时间显著减少。

  • 初始标记:只标记GC Roots直接关联到的对象,这个过程要使得所有工作线程停顿
  • 并发标记:进行GC Roots Tracing,允许工作线程一起运许
  • 重新标记:工作线程的运行会导致标记发生变动,这一步用于修正这些变动的标记。
  • 并发清除:开始垃圾回收,可以与工作线程一起执行

CMS的缺点:

  • 并发标记和并发清除的时候占用了一部分线程从而导致应用程序变慢,在cpu数量较少时,回收会占用大量的cpu资源,在cpu数量增加以后,占用资源百分比会减少。在cpu较少时,会让GC任务与用户任务,在cpu中交替执行(模拟多任务但cpu抢占式),从而减少GC对程序的影响。
  • 由于并发清除阶段运行用户程序一起执行,所以新产生的垃圾在本次无法回收,只有等到下一次,称为无法回收浮动垃圾,所以每次需要预留空间给用户线程使用(因为GC和用户程序一起执行,如果等到满了再GC,那么用户程序就没有空间了),若预留空间较多,就会使得GC发生频率变高,若预留空间过少,可能导致剩余内存无法满足程序需要。就会导致虚拟机启动后备预案,临时使用Serial Old收集器来进行垃圾收集,但这个收集是单线程的,且会使得用户程序全部停止,会使得性能下降。
  • 使用标记-清除算法导致可用空间零碎分布,若出现大的对象无法分配内存时,导致提前触发Full GC,而CMS收集器在进行FullGC是会进行碎片整理。

2.7 G1收集器

特点:

  • 并发与并行:G1充分利用多CPU、多核环境下的硬件优势,可以缩短GC停顿的时间,并且部分步骤(其他收集器要进行停顿)可以与工作进程并发
  • 分代收集:分代概念在G1中仍有保留,独立管理整个GC堆。
  • 空间整合:使用标记-整理算法,回收时不会产生可用空间零碎分布的现象。
  • 可预测的停顿:与CMS一样,都追求降低停顿,但其还能预测停顿的时间模型,预测停顿的时间

G1的内存布局和其他收集器的差别很大,把java堆划分为了很多大小相等的独立区域(Region)(类似分页),同时也保留了老年代与新生代的隔离,但不再是物理隔离。

GC时G1还会对各个Region进行优先级划分(优先级列表),回收时根据优先级来确定回收顺序。而这样化整为零的思想实现细节比较困难,一个区域里面的对象是可以与其他区域的对象建立引用关系的,那么就不能以一个区域为单位进行了。

即使如此,在可达性分析时也不用扫描整个java堆。虚拟机使用Remembered Set来避免全堆扫描,每个区域都有自己的Remembered Set。在对reference类型数据进行写时,先中断写操作,然后检测,引用的对象是不是其他Region的,如果是就会把被引用对象的区域记录在本Region的Remembered Set中。所以进行可达性分析时,根据优先级选择Region,进行扫描,接着扫描该Region的Remembered Set中存储的区域信息。

G1的步骤分为四步

  • 初始标记:类似CMS,标记GC Roots能直接关联到的对象,需要进行GC停顿,然后标记区域使得下一次并发时,用户进程找到正确的Region进行新对象创建。
  • 并发标记:类似CMS,进行可达性分析,寻找要回收的对象,耗时较长,可并发执行用户进程
  • 最终标记:修正并发标记期间,用户进程并发执行引起的引用关系变化,变化消息记录在线程Remembered Set Log中,需要把数据合并到Remembered Set中,这阶段要停顿,可并行执行。
  • 筛选回收:根据优先级来逐个进行区域的回收。

猜你喜欢

转载自blog.csdn.net/maniacxx/article/details/86585457