我是这样跟面试官讲垃圾回收的

垃圾回收机制是什么?我们为什么要学习垃圾回收机制?今天我们就带着这两个问题一起来看看。

在我们日常的开发过程中,并不会过多的关注对象的回收和释放,JVM就可以帮助我们来完成垃圾,减少了我们很多的工作量,仿佛垃圾回收离我们很远,其实垃圾回收机制是我们从初级到中高级开发必须掌握的。把回收对象的任务完全交给JVM,看似解放了,其实也增加了不确定性,事情并不是什么时候都是完美的,在现如今各种复杂业务场景下,不合适的垃圾回收算法及策略,往往是导致我们系统性能瓶颈的主要原因。

垃圾回收也不能一概而论,不同的业务场景采取不同的措施,如果业务场景对内存的要求比较高,就需要提高对象的回收效率,如果是CPU使用率高,这个时候就要降低垃圾回收频率。

我们都知道,JVM的内存中有多个区域,垃圾回收主要是看堆和方法区的内存,因为其他区域如程序计数器、虚拟机栈和本地方法栈等区域的内存具有确定性,所以我们要把目光主要放在堆中的对象回收和方法区的废弃常量的回收。

JVM如何判断一个对象可以回收的?

最开始接触垃圾回收的时候,应该都听过,对象没有被引用的时候就可以被回收,但是怎么判断对象是否被引用,主要有两种方式:引用计数算法和可达性分析算法。

引用计数算法:所谓的引用计数算法,就是通过一个对象的引用计数器来判断该对象是否被引用,对象被引用的时候,计数器就加1,引用失效计数器就减1。计数器的值为0 的时候就说明这个对象没有被引用了,可以被JVM回收了。需要注意的是,引用计数算法虽然实现方式简单,但是会出现循环引用的问题。

可达性分析算法:可达性分析算法的基础是GC Roots,是所有对象的跟对象,在JVM加载时,会创建一些对象引用正常对象,这些对象作为这些正常对象的起始点,在垃圾回收时,JVM会从GC Roots开始向下搜索,如果一个对象到GC  Roots没有任何引用链相连时,就证明这个对象可以回收了。

垃圾回收线程是如何回收对象的?

JVM去回收对象主要遵从两个特性:自动性、不可预期性。

自动性:JVM会创建一个系统级的线程来跟踪每一块被分配出去的内存,在JVM空闲时,就会自动的检查每一块分配出去的内存空间,然后自动回收每一块内存。

不可预期性:不可预期性主要是一个对象没有被引用的时候,是立马就被回收的吗,这个答案是未知的,有可能立马就被回收,有可能隔了很久依然在内存中。

GC算法

JVM给我们提供了多种回收算法来实现回收机制,一般来说,市面上常见的垃圾收集器的回收算法主要分为四类:

标记-清除算法(Mark-Sweep)

优点:不需要移动对象,简单高效

确定:标记-清除的过程效率低,会产生内存碎片。

复制算法(Copying)

优点:简单高效,不会产生内存碎片

缺点:内存使用率低,还有可能产生频繁复制的问题。

标记-整理算法(Mark-Compact)

优点:不需要移动对象,效率高,不产生内存碎片

缺点:需要移动局部对象

分代收集算法(Gennerational Collection)

优点:分区回收

缺点:对于长期存活对象的回收效果不太好。

了解了四种垃圾收集器的回收算法之后,我们再来看看基于这些算法实现的回收器,简单介绍几种常见的:
我是这样跟面试官讲垃圾回收的_第1张图片

衡量GC性能的标准?

垃圾收集器各种各样的,不同的场景适用不同的回收器,如何挑选合适的垃圾收集器,主要取决于垃圾收集器的三个指标:吞吐量、卡顿时间、垃圾回收频率。

吞吐量:指系统应用程序花费的时间和系统运行总时长的比值,GC 的吞吐量=GC耗时/系统总运行时间。GC的吞吐量一般不低于95%。

卡顿时间:卡顿时间是垃圾收集器在工作的时候,应用程序暂停的时间。一般串行收集器的卡顿时间较长,并发收集器的卡顿时间因为收集器和应用程序交替运行,所以卡顿时间会比较短,但是效率不如串行的,系统吞吐量会有所下降。

垃圾回收频率:垃圾回收频率时间和卡顿时间是互相影响的,我们可以通过增大内存的方式来降低垃圾回收发生的频率,但是内存增大后,堆积的对象就更多,当垃圾回收时,卡顿的时间就会增加。所以我们要把握增加内存的这个度,来保证正常的垃圾回收频率即可。

如何查看并分析GC日志?

前边废话这么多,估计很多大兄弟都看烦了,接下来我们来看看如何收集GC日志,并分析GC日志,我们需要JVM参数来设置GC日志,需要关注以下几个参数:

-XX:+PrintGC  #输出GC日志
-XX:+PrintGCDetails #输出GC的详细日志
-XX:+PrintGCTimeStamps #输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps #输出GC的时间戳(以日期的形式,如 2020-12-08T23:59:59.234+0800)
-XX:+PrintHeapAtGC #在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log #日志文件的输出路径

我们按需配置参数即可,打印后的日志,例如下图:
我是这样跟面试官讲垃圾回收的_第2张图片
很短时间的GC日志我们可以用记事本打开去查看,如果是分析长时间的GC日志,再用记事本打开去看就有点困难,我们就需要借助工具来分析,一般省事的可以用GCViewer来打开日志文件,就可以图形化的查看GC性能。通过工具我们可以看到吞吐量、卡顿时间、GC频率,很直观的查看GC的性能情况。

GCeasy也是一个更好用的GC日志分析工具,只需要把日志文件压缩一下,上传官网就可以在线分析,下边是我使用一个本地的GC日志分析的结果:
我是这样跟面试官讲垃圾回收的_第3张图片

我是这样跟面试官讲垃圾回收的_第4张图片

GC调优

上边通过分析GC日志,找出影响性能的问题,接下来就该有针对性的调优了,简单介绍几种常用的调优策略,主要是降低Minor GC和Full GCd 频率。

降低Minor GC频率

我们首先来看,Minor GC主要是针对Eden区的对象回收,由于新生代空间一般比较小,Eden区很块就会满,就会导致Minor GC的频率比较高,我们的解决办法通常是增大新生代空间来降低Minor GC的频率。在前边讲衡量GC性能指标的时候,我们提到增大内存会增加回收时候的卡顿时间。Minor GC也会导致应用程序的卡顿,只是时间非常短暂,那么扩大Eden区会不会导致Minor GC的时间增长,还得深入看一下一次Minor GC发生了什么。

每次Minor GC主要做了两件事,扫描新生代(A)和复制存活对象(B)。其中复制对象的耗时是远高于扫描对象的。我们举个例子,如果一个对象在Eden区域存活500ms,Minor GC的频率是300ms一次,正常情况下,在一次Minor GC中用时就说A+B的时间,这个时候我们通过gc日志分析,把Eden扩容,变成了600ms才进行一次Minor GC,此时这个对象在Eden区中已经被回收,就不用复制对象了,就省去了复制存活对象的时间,在这一次Minor GC中只是增加了扫描新生代的时间。

总结:单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。如果堆内存中存活时间比较长的对象多,增加年轻代的空间,单次Minor GC的时间反而会增加,如果是堆内存中短期对象多,那么扩容后,单词Minor GC的时间不会明显的增加,还降低了Minor GC频率。
降低Full GC频率

Full GC的触发通常是因为堆内存空间不足或者老年代对象太多造成的,Full GC又会带来上下文切换,前边的文章我们已经专门介绍过上下文切换,都知道上下文切换会降低系统的性能。我们可以通过下边几个方向来降低Full GC的频率。

减少创建大对象:有时候因为一些编程习惯的问题,为了省事就一次性从数据库查询一个大对象用于web端显示,这种大对象会被直接创建在老年代,哪怕是创建在新生代,由于新生代的空间一般很小,通过一次Minor GC就会进入老年代,这样的大对象攒多了就会触发Full GC,所以还是要养成良好的习惯,减少一些不必要字段的查询。

增大对内存空间:堆内存不足这种情况就直接增大堆内存的空间,把初始化内存空间就设置成最大堆内存空间,这样就可以显著降低Full GC频率/

合适的GC回收器:上边我们也介绍了多种回收器,根据我们的业务场景,选择合适的回收器往往可以达到不错的效果。

总结

垃圾回收是一门复杂的学问,需要不断地去练习,去实践。看完这篇文章想必对垃圾回收有了一定了解了吧,赶快行动起来,先拿公司的开发环境练练手。

学习更多Java技术,我推荐我的:Java学习园地,更多Java技术资料总结,技术交流与讨论,开发经验总结分享,更适合零基础喔!!

你可能感兴趣的:(垃圾回收机制,面试,程序员,java,编程)