关于垃圾回收机制:我是这样跟面试官讲垃圾回收的

2次阅读

共计 3429 个字符,预计需要花费 9 分钟才能阅读完成。

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

在咱们日常的开发过程中,并不会过多的关注对象的回收和开释,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)

长处:分区回收

毛病:对于长期存活对象的回收成果不太好。

理解了四种垃圾收集器的回收算法之后,咱们再来看看基于这些算法实现的回收器,简略介绍几种常见的:

掂量 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 #日志文件的输入门路 

咱们按需配置参数即可,打印后的日志,例如下图:

很短时间的 GC 日志咱们能够用记事本关上去查看,如果是剖析长时间的 GC 日志,再用记事本关上去看就有点艰难,咱们就须要借助工具来剖析,个别省事的能够用 GCViewer 来关上日志文件,就能够图形化的查看 GC 性能。通过工具咱们能够看到吞吐量、卡顿工夫、GC 频率,很直观的查看 GC 的性能状况。

GCeasy 也是一个更好用的 GC 日志剖析工具,只须要把日志文件压缩一下,上传官网就能够在线剖析,下边是我应用一个本地的 GC 日志剖析的后果:




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 技术材料总结,技术交换与探讨,开发经验总结分享,更适宜零根底喔!!

正文完
 0