JVM 的 垃圾回收(GC)超全解析,面试官看了直呼内行!!还不快收藏起来
Posted 小乔不掉发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM 的 垃圾回收(GC)超全解析,面试官看了直呼内行!!还不快收藏起来相关的知识,希望对你有一定的参考价值。
我是目录:
Garbage Collection(GC),Java进程在 启动后 会 创建垃圾回收线程,来对 内存中无用的对象 进行 回收
1、垃圾回收的时机:
(1)System.gc()
显示的调用 System.gc():此方法的调用是 建议 JVM进行 FGC(Full GC),虽然只是建议而非一定,但很多情况下它会触发 FGC,从而增加FGC的频率。一般不使用此方法,让虚拟机自己去管理它的内存。
(2)JVM 垃圾回收机制决定
- 创建对象时需要分配内存空间,如果空间不足,触发GC
java.lang.Object 中有一个 finalize() 方法,当 JVM 确定不再有指向该对象的引用时,垃圾收集器在对象上调用该方法。finalize() 方法有点类似对象生命周期的临终方法,JVM 调用该方法,表示该对象即将“死亡”,之后就可以回收该对象了。注意 回收还是在JVM 中处理 的,所以手动调用某个对象的finalize() 方法,不会造成对象的“死亡”。
2、垃圾回收机制 ------ 如何判断对象已死?
需要垃圾回收某一个对象时,需要判断这个对象是否可以回收,怎么判断一般有两种算法:
(1)引用计数算法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就 加1;当引用失效时,计数器值就 减1;任何时刻计数器为 0 的对象就是不可能再被使用的。 引用计数算法的 实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是它 很难解决对象之间相互循环引用的问题。
(Python、ActionScript 等语言都是基于引用计数法)
(2)可达性分析算法:
通过一系列的称为 “GC Roots” 的对象作为 起始点,从这些节点开始向下搜索,搜索所走过的路径 称为 GC Roots引用链,当一个对象到GC Roots没有任何引用链相连(从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
如下图,object5和object6虽然相互引用,但是由于他们到GC Roots都不可达,因此会被判定为可回收的对象。
( Java、C# 等语言都是使用可达性分析算法进行垃圾回收)
- 强引用:最传统的引用定义(Object obj = new Object()),无论在何种情况下,只要强引用关系还在,垃圾收集器就永远不会回收这引用的对象
- 软引用:描述一些还有用,但非必须的对象。只被软引用关联的对象,在系统将要发生 内存溢出异常前,会把这些对象列进回收范围之内进行 二次回收,如果这次回收还没有足够的内存,才会抛出内存异常。
(适用场景:适合于创建缓存,当系统内存不足的时候,缓存中的内容是可以被释放的)(网页缓存、图片缓存等)
(能防止内存泄露,增强程序的健壮性) - 弱引用:也是描述非必须对象(强度弱于软引用)。无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
(作用:在于解决强引用所带来的对象之间在存活时间上的耦合关系)
(最常见的用处是在集合类中,在哈希表中一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其所包含的键和值对象是不会被回收的。) - 虚引用:“幽灵引用”,最弱的引用关系。他的存在不会影响对象的生存时间,他的存在只是为了在这个对象被垃圾回收时收到一个系统通知。
3、需要垃圾回收的内存:
(1)方法区(jdk 1.7)/ 元空间(jdk 1.8)
- JDK1.7的 方法区 在GC中一般称为 永久代(Permanent Generation)。
- JDK1.8的 元空间 存在于本地内存,GC也是即对元空间垃圾回收。
- 永久代或元空间的垃圾收集主要回收两部分内容:废弃常量和无用的类。此区域进行垃圾收集的“性价比”一般比较低
(2)堆:
- Java堆 是垃圾收集器管理的主要区域,因此很多时候也被称做 “GC堆”(Garbage Collected Heap)。
- 从内存回收的角度来看,由于现在收集器基本都采用 分代收集算法,所以Java堆中还可以细分为:
- 1、新生代(Young Generation):又可以分为Eden空间、From Survivor空间、To Survivor空间。
新生代的垃圾回收又称为 Young GC(YGC)、Minor GC。
指发生在新生代的垃圾收集动作,因为Java对象大多都具备 朝生夕灭 的特性,所以 Minor GC非常频繁,一般回收速度也比较快。
(朝生夕灭:方法调用,在方法返回后,方法栈帧出栈,局部变量没了,局部变量的对象不可达) - 2、老年代(Old Generation、Tenured Generation)。
老年代垃圾回收又称为 Major GC。
指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
Major GC的速度一般会比 Minor GC慢10倍以上。 - 3、Full GC:在不同的语义条件下,对Full GC的定义也不同,有时候指老年代的垃圾回收,有时候指全堆(新生代+老年代)的垃圾回收,还可能指有用户线程暂停(Stop-The-World)的垃圾回收(如GC日志中)。
- 1、新生代(Young Generation):又可以分为Eden空间、From Survivor空间、To Survivor空间。
4、垃圾回收算法:
(1)标记 -- 清除算法(Mark - Sweep 算法)
- 最基础的收集算法,老年代 收集算法
- 如同它的名字一样,算法分为 “ 标记 ” 和 “ 清除 ” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
- 它的主要不足有两个:
- 1、 效率问题,标记 和 清除两个过程的效率都不高。
- 2、 空间问题,标记清除之后会产生 大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
(类似:遍历文件夹下的子文件,标记废弃文件,删除)
(2)标记 -- 整理算法(Mark-Compact算法)
- 老年代收集算法
- 标记过程仍与 " 标记-清除 " 过程一致,但后续步骤 不是直接对可回收对象进行清理,而是让所有 存活对象都向一端移动,然后直接清理掉端边界以外的内存
- 回收后,内存空间是连续的(没有内存碎片)
(3)复制算法(Copying 算法)
- 新生代 收集算法
- 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
- 缺陷:将内存缩小为了原来的一半
(类似:先找到文件夹下的有用子文件,复制到另一个文件夹,把之前的文件夹全部删除)
(4) 分代收集算法
- 当前 JVM垃圾收集 都采用的是 " 分代收集 " 算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。
(不同的内存划分,采用不同的收集算法) - 一般是把 Java堆 分为 新生代 和 老年代。
- 新生代中98%的对象都是"朝生夕死"的,所以并不需要按照复制算法所要求1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From 区,另一个称为To区域)。HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden :Survivor From : Survivor To = 8 : 1 : 1。所以每次新生代可用内存空间为整个新生代容量的90%,只有10%的内存会被”浪费“。
- 在新生代 中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用 复制算法;而 老年代 中对象存活率高、没有额外空间对它进行分配担保,就必须采用 "标记-清理"或者"标记-整理"算法。
5、垃圾回收过程:
- Eden空间不足,触发Minor GC:由对象优先在Eden分配可知,用户线程创建的对象优先分配在Eden区,当Eden区空间不够时,会触发Minor GC:将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
- 垃圾回收结束后,用户线程又开始新创建对象并分配在Eden区,当Eden区空间不足时,重复上次的步骤进行Minor GC
- 年老对象晋升到老年代: 长期存活的对象将进入老年代。
- Survivor空间不足,存活对象通过分配担保机制进入老年代: 空间分配担保
- 老年代空间不足,触发Major GC:由大对象直接进入老年代、长期存活的对象将进入老年代可知,当老年代空间不足时,也需要对老年代进行垃圾回收,也就是触发Major GC
内存分配与回收策略:
(1)对象优先在Eden分配:
- 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发生一次Minor GC。
(2)大对象直接进入老年代:
- 所谓的大对象是指,需要 大量连续空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
- 大对象对虚拟机的内存分配是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来放置大对象。
(3)长期存活的对象将进入老年代:
- 既然虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该放在新生代,哪些对象应该放在老年代中。
- 为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且把对象年龄设为1。对象在Survivor空间中每"熬过"一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将晋 升到老年代中。
(4)动态对象年龄判定:
- 为了能更好的适应不同程序的内存状况,JVM并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代。
- 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
6、垃圾收集器:
前置知识:
用户线程 和 gc线程 的执行的关系:
- 并行: 指多条垃圾收集线程并行工作,用户线程 仍处于 等待状态。
- 并发: 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运 行,而垃圾收集程序在另外一个CPU上
评价垃圾回收器的标准:
- 吞吐量:用户线程执行的时间 / (用户线程执行的时间 + 用户线程暂停的时间)
判断垃圾回收器的性能指标(CPU的利用率) - 用户体验:单次用户线程暂停时间(STW)尽量越短越好
判断垃圾回收器的用户体验
(1)Serial 收集器(新生代收集器,串行 GC)
特性:
- 单线程
- 复制算法
- Stop The World(STW)
(2) ParNew 收集器(新生代收集器,并行 GC)
(Serial收集器的 多线程版本)
特性:
- 多线程
- 复制算法
- Stop The World(STW)
应用场景:
- 搭配 CMS 收集器,在 用户体验优先 的程序中使用:ParNew是运行在Server模式下的虚拟机中首选的新生代收集器,除了Serial收集器外,目前只有它能与 CMS 收集器配合工作。
(3)Parallel Scavenge 收集器(新生代收集器,并行 GC)
特性:
- 多线程
- 复制算法
- 可控制的吞吐量: Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量
- 自适应的调节策略:Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
应用场景:
- “吞吐量优先” 收集器,适用吞吐量需求高的任务型程序
(4)Serial Old收集器(老年代收集器,串行 GC)
(Serial Old是 Serial 收集器的老年代版本)
特性:
- 单线程
- “标记-整理”算法
应用场景:
- 给 Client模式下的虚拟机使用。
- 在 Server模式下,那么它主要还有两大用途:
- 1、 与Parallel Scavenge收集器搭配使用
- 2、 作为 CMS 收集器 的后备预案,在并发收集发生Concurrent Mode Failure时使用
(5)Parallel Old收集器(老年代收集器,并行 GC)
(Parallel Scavenge收集器的 老年代版本)
特性:
- 多线程
- “标记-整理”算法
应用场景:
- “吞吐量优先”:在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
(6)CMS收集器(老年代收集器,并发 GC)(面试)
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。
特性:
- 并发收集、低停顿
- “标记-清除”算法
整个过程分为4个步骤:
- 1、初始标记:初始标记仅仅只是标记一下GC Roots能 直接关联到的对象,速度很快, 需要“Stop The World”。
- 2、并发标记: 并发标记阶段就是进行GC Roots Tracing的过程。(找引用链)
- 3、重新标记: 重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生 变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标 记的时间短,仍然需要“Stop The World”。
- 4、 并发清除:并发清除阶段会清除对象。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的 内存回收过程是与用户线程一起并发执行的。
应用场景:
- 目前很大一部分的Java应用集中在 互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
缺陷:
- 1、 CMS的“标记-清除”算法,会导致 大量空间碎片 的产生
- 2、无法处理 浮动垃圾
(并发清理的过程中,由于用户线程还在执行,因此就会继续产生对象和垃圾,这些新的垃圾没有被标记,CMS只能在下一次收集中处理它们。这也导致了CMS不能在老年代几乎完全被填满了再去进行收集,必须预留一部分空间提供给并发收集时程序运作使用)
(7) G1收集器(全区域 的垃圾回收器)(面试)
- 用在 堆内存很大 的情况下,把 堆划分为很多很多的 区域块,然后并行的对其进行垃圾回收。
- G1垃圾回收器回收 区域块 的时候 基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部即两个region之间基于"复制"算法)的策略来对region进行垃圾回收的。
- 用户体验优先
- 无论如何,G1收集器采用的算法都意味着 一个region有可能属于Eden,Survivor或者Tenured内存区域。
- G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。
新生代:E:Eden内存区域;S:Survivor内存区域;
老年代:T:Tenured内存区域。
H:Humongous内存区域。(这种内存区域主要用于存储 大对象-即大小超过一个region大小的50%的对象)
年轻代垃圾收集:
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域。
老年代垃圾收集:
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:
- 初始标记: 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是 跟minor gc一同发生的。也就是说在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了。
- 并发标记:在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,就是如果在Concurrent Mark阶段中,发现哪些 Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。
- 最终标记:在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在这个阶段更快的标记可达对象。
- 筛选回收:在G1中,没有CMS中对应的并发清除阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和 minor gc一同发生
以上是关于JVM 的 垃圾回收(GC)超全解析,面试官看了直呼内行!!还不快收藏起来的主要内容,如果未能解决你的问题,请参考以下文章