写点什么

架构师训练营 1 期 -- 第九周总结

用户头像
曾彪彪
关注
发布于: 2020 年 11 月 22 日

这一周的总结,是整理了关于JVM的垃圾回收原理,这本是这一周的作业,但是我也是花了好些时间,才整理出来这些笔记,算是对JVM垃圾回收原理的一些总结。



  • 请简述 JVM 垃圾回收原理。

为什么要进行垃圾回收?

JVM的内存分为堆,栈,程序计数器这些区域,其中栈和程序计数器的生命周期是和线程相关的,线程结束时这些区域空间就释放了。并且栈在方法结束时就清空了,所以这些内存区域不需要考虑回收的问题。

堆区是不一样的,堆区存储了对象的实例,以及类的定义等信息,这些信息是在线程之间共享的,并且随着线程的执行,不断会有新的对象创建,所以要对这些对象进行垃圾回收,释放内存资源。



哪些对象需要进行垃圾回收?

上面说,堆区的对象需要进行回收,也不是所有对象都要进行回收的,只有不再使用的对象才进行回收。JVM是通过可达性分析来判断一个对象是否是可用的。

可达性分析是指,从GC Roots(一般是栈中的本地变量表中的引用变量,静态属性或常量引用等)开始向下搜索,所有出现过的对象都是可达的,而如果一个对象到GC Roots没有任何引用链相连,则认为是不可达的,在GC时,这些对象会被回收掉。

这里有一个知识点,就是对象的finalize方法,在GC之前,GC线程会把实现了finalize方法的对象放进一个队列,另一个Finalizer线程会从队列中取出该对象,执行其finalize方法。当finalize方法执行结束后,如果该对象还是不可达,则将在下次GC时被回收。



什么时候进行垃圾回收?

可以为JVM设置一些参数,达到某些条件时触发GC,也可以手动调用Symtom.gc()方法进行垃圾回收。



怎样进行垃圾回收?



垃圾回收算法

垃圾回收的方法有多种,不同的垃圾回收器采用了不同的回收算法:

标记-清除算法,对垃圾对象先进行标记,然后清楚垃圾对象。该算法的优点是实现简单,缺点是会产生大量的内存碎片。

复制算法,将内存空间分成两部分,一部分用于创建对象,另一部分用作备份。进行垃圾回收时,将存活的对象复制到备份区域,再对之前的内存区域做清理。优点是实现简单,运行高效,缺点是内存空间利用率低(50%)。在新生代中,因为对象存活率低,所以适合使用复制算法,因为只需复制少量对象。

标记-整理算法,先对内存区域的存活对象做标记,然后将存活对象以到内存的一段,然后清理掉另一端。这种算法的优点是可以提高空间利用率,同时也弥补了复制算法的不足,那就是当对象存活率高时,需要进行大量的对象复制。这种算法适合老年代回收。



垃圾收集器

不同代的回收,采用了不同的垃圾收集器。JVM实现了以下收集器:

串行收集器,进行垃圾回收时,stop the world,只有一个线程区执行垃圾回收。优点是简单高效,对于客户端程序来说,一般内存比较小,GC时间短,所以串行收集器是很好的选择,缺点是停顿时间长,无法使用多线程,用户体验差,不适合server端程序。

并行收集器,是串行收集器的多线程版本。他是server端新生代收集器比较好的选则,因为只有它能和CMS收集器同时工作。

CMS(Concurrent Mark Sweep )收集器,是一种试图获得最短GC停顿时间的收集器,它将GC分为4个步骤,

  • 初始标记,stop the world,快速标记GC Roots能直接关联到的对象。

  • 并发标记,通过之前初始标记的对象,寻找关联对象。这个操作是可以和应用程序同时进行的。

  • 重新标记,在并发标记阶段,可能产生新的垃圾对象,此时需要重新再进行一次标记。这个操作也是stop the world。

  • 并发清除,开始清除垃圾对象,和应用程序同时执行。

CMS可以极大减少GC停顿时间,但也有其缺点:

  • 并发标记和并发清除时,会影响应用程序的性能。降低吞吐量。

  • 无法清理浮动垃圾,并发清除阶段,还会产生垃圾,如果这个阶段产生的垃圾比较多,则会造成清除失败,GC回退到并发标记,此时停顿时间会比较长。

  • 因为他是并发清除,不是并发整理的垃圾回收策略,所以会产生垃圾碎片。不过默认是在GC之前,先进行内存整理,但这也是stop the world的。所以JVM又提供了一些参数,指明多少次垃圾回收,进行一次内存空间整理。

G1收集器,是比较新的垃圾回收器,它将整个堆分为多个region,每个region都包含新生代,老年代,在垃圾回收时,虚拟机会选择回收性价比较高的区域进行回收。因为进行GC时,是对部分region进行垃圾回收,所以它的回收时间短。

G1收集器垃圾回收分为4个步骤,初始标记,并发标记,最终标记和筛选回收。其回收过程与CMS回收过程相似,只是在最后一步最筛选回收时,是stop the world的。因为在做筛选回收时,只对部分region做回收,GC停顿时间短,并且只执行GC线程的效率高,所以没必要并发执行GC(虽然也可以并发执行)。



堆内存分配策略

以上所有的垃圾收集器,因为垃圾收集只在部分内存区域进行,就存在垃圾对象判断不准的问题。以minor GC为例,minor在新生代进行,在进行垃圾回收时,老年代的对象可能引用了新生代的对象,如果忽略了这一点,那么对象就会被错误回收。如果再对老年代进行一次扫描,那么GC时间会大大增加。

为了解决这个问题,HotSpot虚拟机采用了一种叫做卡表的技术。HotSpot将整个堆空间分成大小为512的卡表,并记录这些卡表的状态。如果卡表所在的内存区域对象引用发生了变化(通过写屏障技术实现),就将该区域的卡表设置成DIRTY。在新生代回收时,只需要读取卡表为DIRTY的内存区域,将其设置成GC Roots,进行可达性分析,变无需再对整个老年代进行扫描。



实际上,在JVM的垃圾回收中,其内存分配策略是将整个堆分成年轻代和老年代。年轻代又分为Eden和Surviver区,Surviver有两块区域,from和to。在新生代,使用复制算法,进行垃圾回收时,将Eden区和from区的存活对象复制到to区,然后交换from和to区的指针,并清空from和Eden区。

在老年代,采用标记清除或标记整理的算法,因为老年代进行垃圾回收后,存活的对象比较多,如果使用复制算法,则需要进行大量的对象拷贝。

大对象直接放在老年代,因为大对象放在新生代,需要进行不断的拷贝,这个会影响性能。

如果新生代的空间不足时,会进行minor GC,在minor GC之前,系统会判断老年代的最大连续内存空间是否大于新生代所有对象总和,如果大于则进行minor GC,否则则比较老年代最大连续空间是否大于新生代进入老年代对象的平均大小,如果大于则尝试进行minor GC,这个过程可能会失败,如果失败,则需要进行full GC,释放老年代空间。



用户头像

曾彪彪

关注

还未添加个人签名 2019.03.23 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营 1 期 -- 第九周总结