关于Java垃圾回收机制

1.概述

最近在看《深入Java虚拟机》一书,书中讲到关于垃圾回收这块很详细,于是自己做一个关于垃圾回收这块的读书总结。
Java开发一定对Java垃圾回收机制不陌生,面试的时候也会被问到GC相关的问题。那么你是否真的理解GC呢?
不了解GC机制之前肯定有以下这样的疑问:

  • 那些对象会被回收?
  • 什么时候回收?
  • 如何回收?

2.判断对象是否存活的方法

首先Java回收机制主要作用在Java堆区(方法区也会有GC),因为java堆里面存放着几乎所有的对象实例。在进行垃圾回收之前,要先判断这些对象那些是“存活”的,那些是已经“死去”的。

2.1.引用计数法

概念:给对象添加一个引用计数器,每当有个地方引用它时,计数器就加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能在被使用的。
这样看来引用计数法:实现简单,判定效率也很高。
但是如果对象出互相循环引用的问题,那么就无法通知GC收集器回收它们。如:

public class Test{
	public Object mObject = null;
	public static void testGC(){
		Test testA = new TestA();
		Test testB = new TestA();
		testA.mObject = testB;
		testB.mObject = testA;
	}
}

2.2.可达性分析算法

在Java的主流实现中,都是通过可达性分析算法来判断对象是否存活的。
概念:通过一系列的“GC Roots”的对象作为起始点,从该节点向下搜索,搜索所走的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图:
可达性分析算法判断对象是否回收
那么Java中那些可以作为GC Roots对象呢?

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中Native方法引用对象。

2.3.什么样的对象才是真正死亡

即使在可达性分析中不可达对象,也并非是非死不可的。要真正宣告对象死亡,至少要经历两次标记过程。

  1. 第一次标记:可达性分析算法中对象不可达。
  2. 第二次标记:对象没有覆盖finalize()方法或者finalize()已经被虚拟机调用过。(任何对象的finalize()方法都只会被系统自动调用一次,尽量别使用finalize()方法。)

3.垃圾回收算法

3.1.标记-清除(Mark-Sweep)算法

是最基础的收集算法,算法和它名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它有两个不足:

  1. 效率问题,标记和清除两个过程效率都不高
  2. 标记清除后会产生大量不连续的内存碎片,空间碎片过多导致以后分配较大对象时,没有足够的连续内存存储,导致提前触发另一次GC。
    标记-清除算法执行过程如下图:
    标记-清除

3.2.复制(Copying)算法

为了提升标记清除算法效率,复制算法出现了。复制算法:将可用内存按容量大小划分为大小相等的两块,每次只使用其中一块,当一块内存用完了,就将还存活的对象负责到另一块上面去,然后把已使用过的内存空间一次全部清理掉。
不足:

  1. 将内存缩小为原来的一半。
  2. 对象存活率较高时,就要进行较多次的复制操作,效率会降低。(所以新生代内存区采用复制算法,因为新生代中对象98%是“朝生夕死”的)
    复制算法法执行过程如下图:
    复制算法

3.3.标记-整理(Mark-Compact)算法

标记-整理算法和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
复制算法法执行过程如下图:
标记-整理算法

3.4.分代收集(Generational Collection)算法

当前商业虚拟机的垃圾收集都采用“分代收集”,这种算法并没有什么新思想。只是根据对象存活周期的不同将内存划分为几块(新生代、老年代、永久代),针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。

4.JVM内存分配

JVM内存主要由新生代、老年代、永久代构成。

4.1.新生代

新生代中将内存划分为一块较大的Eden空间和两块较小的Survivor空间(默认比例 8:1:1),每次使用Eden和其中一块Survivor。大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。如果另一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象会直接通过分配担保机制进入年老代。
新生代

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。

4.2.老年代

4.2.1. 大对象直接进入老年代

大于3M的对象,如:很长的字符串和数组。

4.2.2. 长期存活的对象将进入老年代

如果对象在Eden出生并经历第一次Minor GC后仍然存活,并且能被Survivor所容纳的话,将被移动到Survivor空间中,此时对象年龄设置为1岁。对象在Survivor区中每“熬过”一次Minor GC,年龄都会加1岁,当年龄增加到年龄阈值(默认15)将会进入老年代。

4.2.3. 动态对象年龄判定

如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无需达到年龄阈值。

4.2.4. 空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可连续空间是否大于新生代所有对象空间总和,如果条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置值不允许冒险,那这时也要进行一次Full GC。
在这里插入图片描述
如果Eden存活下来的对象大小大于Survivor空间大小,对象会直接通过分配担保机制直接进入年老代。前题是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会存活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均值大小作为经验值,与老年代剩余空间进行比较,决定是否进行Full GC来让老年代腾出空间。

老年代FC(Major GC/Full GC):指发生在老年代的GC,出现Major GC至少会伴随一次Minor GC(但非绝对)。Major GC速度一般比Minor GC慢10倍以上。
Major GC等价Full GC会对整个java堆进行垃圾收集操作。

4.3.永久代

很多人认为方法区(或者当代主流虚拟机(HotSpot虚拟机)中的永久代)是没有垃圾收集的,Java虚拟机规范也确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集“性价比”一般比较低。
永久代主要回收两个部分:废弃常量和无用的类。

5.JVM发生GC时,为什么会出现卡顿?(GC停顿

GC会导致堆内存块状整理,堆内存的整理必须暂停所有Java执行线程,禁止分配对象空间。
在垃圾回收过程中经常涉及到对对象的挪动(比如新生代内存中的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”。

发布了27 篇原创文章 · 获赞 187 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/Mr_wzc/article/details/95868992