「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
前言
- 关于作者:励志不秃头的一个CURD的Java农民工
- 关于文章:以下内容单纯为作者觉得面试八股文中比较经常遇到的总结,同时会穿插一些作者面试遇到的问题作为记录,打*号的都是作者主观认为比较重要的
垃圾回收的目的是更好地回收内存,或者更快地分配内存,主要针对堆
为什么需要有GC
-
因为如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
-
除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。
-
随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。
如何判断哪些对象需要被回收**
判断方法:引用计数法,可达性分析算法
-
引用计数法——Python语言使用
- 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的;
- 容易发生相互引用问题,导致内存泄露
-
可达性分析算法——JVM目前使用
- 通过一系列的称为 "GC Roots" 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的
- "GC Roots" 是一组必须活跃的引用,只要跟 "GC Roots"没有直接或者间接引用相连,那就是垃圾
可以被作为GC Roots的对象:*
- 虚拟机栈中栈桢中的局部变量(也叫局部变量表)中引用的对象
- 方法区中类的静态变量、常量引用的对象
- 本地方法栈中 JNI (Native方法)引用的对象
判断流程:
-
找到GC Roots不可达的对象,如果没有重写finalize()或者调用过finalize() ,则将该对象加入到F-Queue中
finalize()方法:是Object里面的一个方法,当一个堆空间中的对象没有被栈空间变量指向的时候,这个对象会等待被java回收;JDK里面是这样实现的:
protected void finalize() throws Throwable { } -
再次进行标记,如果此时对象还未与GC Roots建立引用关系,则被回收
不可达的对象并非一定回收
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程:
- 可达性分析法中不可达的对象被**第一次标记**并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
- 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
JVM回收核心参数
- -Xms:Java堆内存的大小
- -Xmx:Java堆内存的最大大小
- -Xmn:Java堆内存中新生代的大小,扣除新生代后剩下的就是老年代的大小
- -XX:PermSize:永久代大小
- XX:MaxPermSize:永久代最大的大小
- -Xss:每个线程的内存大小
- -XX:SurvivorRatio,新生代内存比例,默认为8,即Eden区占新生代的80%
- -XX:PretenureSizeThreshold,大对象阈值
- -XX:CMSInitiatingOccupancyFraction:老年代内存达到该设置阈值,触发Full GC
- -XX:+HeapDumpOnOutOfMemoryError:在OOM的时候自动dump内存快照出来
- -XX:HeapDumpPath=/usr/local/app/oom:在OOM的时候内存快照存放的路径
JVM的垃圾回收算法
-
标记-清除
-
过程:
- 将需要回收的对象标记起来
- 清除对象
-
缺陷:标记和清除的效率都不高,会产生大量的不连续的内存碎片
-
-
复制算法
新生代使用的是复制算法
- 复制算法是将内存划分为两块大小相等的区域,每次使用时都只用其中一块区域,当发生垃圾回收时会将存活的对象全部复制到未使用的区域,然后对之前的区域进行全部回收。
- 优点:简单高效,不会出现内存碎片问题
- 缺点:内存利用率低,存活对象较多时效率明显会降低
-
标记-整理
老年代使用的是标记-整理算法
- 原理和标记清除算法类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。
- 缺陷:需要移动大量对象,效率不高
-
分代回收算法
- 根据各个年代的特点选取不同的垃圾收集算法
- 新生代使用复制算法
- 老年代使用标记-整理或者标记-清除算法
JVM中的垃圾回收器
目前主流的是:ParNew收集器、CMS 收集器、Serial Old收集器、G1回收器
G1适合有超大堆(16G、32G) 或者 业务上不能有太高延时的场景
ParNew + CMS面临超大堆时,每一次回收的STW时间都会比较长
-
ParNew收集器
- 多线程回收
- 是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
-
CMS
流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW) 。
- 并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW) 。
- 并发清除:不需要停顿。
缺陷:
-
吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高
-
无法处理浮动垃圾,可能出现 Concurrent Mode Failure
- 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收
- 由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。一般老年代内存空间达到80%,便触发Full GC
- 如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
-
会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片
-
G1收集器
可以设置一个垃圾回收的预期停顿时间,-XX:MaxGCPauseMillis;G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
-
JVM最多有2048个Region,每个Region的大小都是2的倍数,然后是堆的大小除以2048 或者 通过参数指定:-XX:G1HeapRegionSize
-
根据参数-XX:G1NewSizePercent来设置新生代的占比,默认是5%
-
在系统运行中,JVM会不断的给新生代增加更多的Region,但是占比不超过参数“-XX:G1MaxNewSizePercent”的设置,默认是60%
注意:新生代一样有Eden和Survivor的概念
- 大对象可能会横跨多个Region存放
过程:
- 初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程(STW),但耗时很短。
- 并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
- 触发新生代 + 老年代的混合垃圾回收:-XX:InitiatingHeapOccupancyPercent,默认值是45%
- 如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。
-
GC回收流程
流程图
流程文字描述
流程文字描述
年轻代无法放入新的对象,触发Minor GC
-
检查老年代可用空间是否大于新生代全部对象
a. 否,检查参数“-XX:HandlePromotionFailure”参数是否被打开(一般情况下被打开)
aa. 参数打开,检查可用空间是否大于历次Minor GC过后进入老年代的对象的平均大小 bb. 参数未设置,进入Full GC,先回收老年代一批对象,再次执行Minor GCb. 是,进入Minor GC
Minor GC
执行Minor GC,有轻微的毫秒的Stop the Word
- Eden和Survivor区基于GC Roots遍历,寻找存活对象
- 遍历对象进行copy动作移动到另外一个Survivor区中
- 判断Survivor区能否存入存活对象大小
- 判断存活对象年龄是否大于晋升年龄,参数: -XX:MaxTernuringThreshold ,默认为15
- 动态年龄判断:如果 survivor 空间中年龄1 + 年龄2 +年龄3 +…. + 年龄n (n>1)的这些对象大小的总和大于 survivor 空间的一半, 年龄大于或等于该年龄 n 的对象就会直接进入到老年代,不需要熬过那么多年龄
- 清空Eden区、其中一个Survivor区
G1垃圾回收过程
- 触发一个“初始标记”的操作,进入Stop the World,但是仅仅标记一下GC Roots直接能引用的对象
- 进入并发标记,这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象
- 最终标记阶段,这个阶段会进入“Stop the World”,系统程序是禁止运行的,但是会根据并发标记 阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象
- “混合回收“阶段,这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我指定的范围内。
参数说明:
-
-XX:G1MixedGCCountTarget,在一次混合回收过程中,最后一个阶段执行几次混合回收,默认值是8次
-
面试题:为什么要反复回收多次?*
因为尽可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下
-
-
-XX:G1HeapWastePercent,默认是5次
- 在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会 立即停止混合回收,意味着本次混合回收就结束了。
结语
本篇文章涉及到的都是JVM的一些理论知识,在接下来的文章,作者会分析一些JVM高频的面试题以及一些生产上的简单JVM调优见解
如有不对,欢迎留言一起交流成长