JVM——垃圾回收机制
一、垃圾回收概述
垃圾回收三问:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
1、什么是垃圾
垃圾是指在运行程序中,没有任何指针指向的对象,这个对象就是需要被回收的垃圾。(没人要的野孩子)
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间,无法被其他对象使用,甚至可能导致内存溢出。
2、Java垃圾回收机制
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄露和内存溢出的风险
自动内存管理机制,将程序员从繁重的那种管理中释放出来,可以更专心专注地进行业务开发。
二、垃圾回收算法
标记阶段:判断对象是否存活
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中,哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会执行垃圾回收,释放掉所占用的内存空间,因此这个过程,我们称为垃圾标记阶段。
判断对象存活有两种方式:引用计数算法和可达性分析算法
1、引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
优点:
实现简单垃圾对象,便于辨识判定效率高回收没有延迟性。
缺点:
引用计数器有一个严重的问题,及无法处理循环引用的情况。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();//对比GC前后是否被垃圾回收
//未被回收,并不采用这种标记方法
//-XX:+PrintGCDetails
}
}
垃圾回收前:
垃圾回收后:
12124k—>655k说明垃圾被回收,并未采取引用计数法进行标记。
2、可达性分析算法(根搜索算法)
相对于引用计数算法而言,可达性分析算法,不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环应用的问题,防止内存泄漏的发生。(买葡萄)
实现思路:
通过GC ROOT
的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收(可作为GC ROOT
的对象:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI
(本地方法)引用的对象,所有被synchronized持有的对象)
如果从根到某个对象是可达的,则该对象称为“可达对象”(存活对象,不可回收对象)。否则就是不可达对象,可以被回收。
对象Object5、Object6、Object7虽然互相引用,但他们的GC Roots是不可到达的,所以它们将会被判定为是可回收的对象。
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是他自己又不存放在堆内存里面,那他就是一个Root。
清除阶段:
1、标记-清除(Mark-Sweep)算法:
当推中的有效内存空间(avaiilable memory)被耗尽的时候,就会停止整个程序(stop the world),然后进行两项工作。第一项则是标记,第二项则是清除。
标记:从引用根节点开始便利,标记所有被引用的对象一般是在对象的Header中记录为可达对象。
清除;对堆内存从头到尾进行线性的遍历,如果发现某个对象其header中没有标记为可达对象则将其回收。
标记-清除算法的缺点:
1、标记和清除效率不高;
2、在进行GC时,需要停止整个应用程序;
3、产生大量不连续的内存碎片,导致有大量内存剩余的情况下,由于,没有连续的空间来存放较大的对象,从而触发了另一次垃圾收集动作。
2.复制(Copying)算法:
将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。
优点:
- 没有标记和清楚的过程,实现简单运行高效
- 复制过去以后保证空间的连续性不会出现碎片问题。
缺点:
- 需要两倍的内存空间
3.标记-整理(Mark-Compact)算法:
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下,这种情况在新生代经常发生,但是在老年代更常见的情况是,大部分对象都是存活对象,如果依然使用复制算法。由于存活对象较多复制的成本也将很高,因此基于老年的垃圾回收的特性需要使用其他的算法。
执行流程:
同样的该算法分为两个阶段:标记、整理。标记阶段同“标记-清除”算法。整理阶段,不是直接对标记对象进行清理,而是让所有存活的对象都移动到一端,然后,直接把边界以外的内存清空。解决“标记-清除”算法会造成大量不连续内存碎片的问题。
标记整理算法解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
对比三种算法:
标记-清除(Mark-Sweep | 复制(Copying)算法 | 标记-整理(Mark-Compact) | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(会堆积碎片) | 少(不会堆积碎片) | 需要活对象的两倍大小(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
分代收集算法:
分代收集算法分代收集算法严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合算法,根据对象存活周期的不同将内存划分为几块。
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或者标记-整理算法来进行回收。
三、年轻代与老年代
存储在JVM中的对象可以分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
- 另外一类对象的生命周期非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
YoungGen(Eden:150,000M+Survivor0:25,000M+Survivor1:25,000M)=200,000M
OldGen=400,000M
年轻代(Eden:From:to=8:1:1):老年代=1:2
JVM参数设置:
- -XX:-UseAdaptiveSizePolicy 关闭自适应的内存分配策略
会根据GC的情况自动计算计算 Eden、From 和 To 区的大小; - -XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)
- -XX:SurvivorRatio=8 Eden区与Survivor区的大小比值
几乎所有Java对象都在Eden区被new出来
绝大部分Java对象的销毁都在新生代进行
对象分配与回收:
新创建的对象一般放在新生代的Enden区
- 绿色代表的是"存活对象",红色的代表的是"待回收对象"。
- 当Enden中被使用完的时候,就会发生新生代GC,也就是Minor GC。
- 首先会把存活对象复制到Survivor0中。然后把Enden清空。移
- 动到Survivor0空间后,设置对象年龄(Age)为1。
- 这样第一次GC就完成了。当Enden区再次被使用完的时候,就会再次进行GC操作。
- 下面Enden和Survivor0中,绿色表示存活对象,红色表示"待回收对象",因为在堆内存使用分配的过程中,也会不断有对象变得引用不可达。
- 再次GC的过程中,跟上面一样,将Enden区和Survivor1中的存活对象复制到Survivor2中。需要注意的是目前还是处于新生代的GC,因为新生代分为Enden、Survivor0、Survivor1三个区,使用的其实就是复制算法。
- 接着将Enden和Survivor1进行清空
- 然后将Enden中复制到Survivor2中的对象年龄设置为1,将Survivor0中复制到Survivor1中的对象年龄加1
- 这样新生代第二次GC就完成了。当Enden再一次被使用完的时候,就会发生第三次GC操作了。
如果对象在GC过程中没有被回收,那么它的对象年龄(Age)会不断的增加,对象在Survivor区每熬过一个Minor GC,年龄就增加1岁,当它的年龄到达一定的程度(默认为15岁),就会被移动到老年代,这个年龄阀值可以通过-XX:MaxTenuringThreshold
设置。
Minor GC 、Major GC、Full GC
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partia Gc),一种是整堆收集(Full GC)
部分收集:不是完整收集整个ava堆的垃圾收集。其中又分为:
新生代收集(Minor GC/ Young GC):只是新生代的垃圾收集
老年代收集(Major GC/ Old GC):只是老年代的垃圾收集。
目前,只有 CMS GC会有单独收集老年代的行为。
注意,很多时候 Major GC会和Fu11GC淆使用,需要具体分辨是老年代回收还是整堆回收。
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
目前,只有G1GC会有这种行为
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
年轻代GC(Minor GC)触发机制:
当年轻代空间不足时,就会触发 Minor GC,这里的年轻代满指的是Eden代满, Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。)
因为ava对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代GC(Major GC/full GC)触发机制:
- 指发生在老年代的GC,对象从老年代消失时,我们说“Major GC”或“Full GC”发生了。
- 出现了Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Paralle1 ScavengeMajor收集器的收集策略里就有直接进行 GC的策略选择过程)
也就是在老年代空间不足时,会先尝试触发 Minor GC。如果之后空间还不足,则触发 Major GC - Major GCMinor的速度一般会比 GC慢10倍以上,STW的时间更长。
- 如果Major GC后,内存还不足,就报OOM了。
- Major GCMinor的速度一般会比 GC慢10倍以上。
Full GC触发机制:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小