Java面试之JVM(3)垃圾回收机制

2021/4/11 12:25:56

本文主要是介绍Java面试之JVM(3)垃圾回收机制,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

以下内容来自网络整理,侵删

JVM 垃圾回收机制

什么是垃圾?

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾

对象什么时候进入老年代?
  • 大对象直接进入老年代
    • 大对象是指需要大量连续内存空间的对象,例如很长的字符串以及数组。
    • 虚拟机设置了一个-XX:PretenureSizeThreshold参数,令大于这个设置的对象直接在老年代分配。目的就是为了防止大对象在Eden空间和Survivor空间来回大量复制。
  • 长期存活的对象进入老年代
    • 对象在Survivor区中每熬过一次Minor GC,年龄就加一,当他的年龄增加到一定程度,就会被移动到老年代(年龄值默认为15)。
    • 对象晋升老年代的阈值可以通过-XX:MaxTenuringThreshold设置。
  • 动态年龄判断并进入老年代
    • 为了更好的适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才会晋升到老年代。
    • 如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到MaxTensuringThreshold的要求年龄。
如何判断一个对象是否可被回收?
引用计数算法
  • 为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
  • 但是在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
可达性分析算法

以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。Java 虚拟机使用该算法来判断对象是否可被回收。
GC Roots 一般包含以下内容:
(1)虚拟机栈中引用的对象
(2)本地方法栈中引用的对象
(3)方法区中静态成员或常量引用的对象

垃圾收集算法介绍下
标记—清除算法(Mark-Sweep)
  • 标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象;
  • 清除阶段:清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header信息),则将其回收。
    缺点:
    (1)两次遍历,效率不高
    (2)在进行GC的时候,要停止整个程序,用户体验差
    (3)会产生大量碎片。需要维护一个空闲列表(联系对象创建的过程)
复制算法(Copying)
  • 对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时,这些对象就复制到老年代。默认是 15 岁,通过-XX:MaxTenuringThreshold 来设定参数
  • 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。
  • 缺点:复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。所以老年代一般不采用
  • 复制算法使用简单高效,所以新生代采用。
  • 但是需要的内存过多,老年代不适合采用
标记-整理算法
  • 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用的对象
  • 第二阶段将所有存活的对象压缩到内存的另一端,按顺序排放。之后,清理边界外所有空间。
  • 缺点:
    (1)效率不高,不仅要标记存活对象,还要整理所有存活对象的引用地址,在效率上不如复制算法。
    (2)移动过程中,要全程暂停用户应用程序。
分代收集算法
  • 新生代:由于新生代产生很多临时对象,大量对象需要进行回收,所以采用复制算法是最高效的
  • 老年代:回收的对象很少,都是经过几次标记后都不是可回收的状态转移到老年代的,所以仅有少量对象需要回收,故采用标记清除或者标记整理算法。
垃圾收集器介绍下
  • 如何查看默认的垃圾收集器?
    -XX:+PrintCommandLineFlags
  • Serial 串行收集器
    • 单线程收集器,只使用一个线程回收垃圾。
    • 需要停掉其他所有的线程。Stop the world
    • Client模式下默认的新生代垃圾收集器
    • 对应的JVM参数是 –XX:+UseSerialGC,开启后是Serial和Serial Old的组合,新生代使用复制算法,老年代使用标记整理算法
    • Serial Old也作为CMS收集器的后备垃圾收集方案
  • ParNew收集器
    • 属于serial的多线程版本。其余方面相同。
    • 对应的JVM参数是 –XX:+UseParNewGC,
    • 开启上述参数后,会使用:ParNew(Young区用) + Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法;但是会出现警告,即 ParNew 和 Serial Old 这样搭配,Java8已经不再被推荐
  • Parallel scavenge收集器
    • 因此现在新生代默认使用的是Parallel Scavenge,也就是新生代和老年代都是使用并行
    • 可控制的吞吐量,通过JVM参数 -XX:GCTimeRatio 设置
    • 自适应调节策略。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio参数(更关注吞吐量)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。
    • 常用JVM参数:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器。开启该参数后:新生代使用复制算法,老年代使用标记-整理算法
  • CMS(Concurrent Mark Sweep)垃圾收集器:
    • 一种以获取最短回收停顿时间为目标的收集器。
    • 开启该收集器的JVM参数: -XX:+UseConcMarkSweepGC 开启该参数后,会自动将 -XX:+UseParNewGC打开,开启该参数后,使用ParNew(young 区用)+ CMS(Old 区用) + Serial Old 的收集器组合,Serial Old将作为CMS出错的后备收集器
    • 整个过程分为四个步骤:
      (1)初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
      (2)并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。(在这个阶段也可能产生新的垃圾对象,从而没有被标记)
      (3)重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
      (4)并发清除:不需要停顿。
      优点:并发收集、低停顿
      缺点:A. 并发执行,对CPU资源压力大;B. 标记清除算法,产生大量空间碎片
  • G1 垃圾收集器
    • 主要改变是:Eden,Survivor和Tenured等内存区域不再是连续了,而是变成一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。
    • 特点
      空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的
      可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内
    • G1收集器之所以能建立可预测的停顿时间模型,G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region (贪心?)
    • JVM参数:-XX:+UseG1GC,-XX:MaxGCPauseMillis=100,-XX:G1HeapRegionSize=n,其中-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM尽可能停顿小于这个时间;G1HeapRegionSize可指定分区大小,默认将整堆划分为2048个分区


这篇关于Java面试之JVM(3)垃圾回收机制的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程