『Java面经』Java中对象何时需要回收?常见的 GC 回收算法有哪些?


hello
今天我们就从头到尾完整地聊一聊 Java 的垃圾回收。

1、什么是垃圾回收

垃圾回收(Garbage Collection),释放垃圾占用的空间,防止内存泄露,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

2、如何定义垃圾

既然要做垃圾回收,那么就得知道垃圾的定义是什么,哪些内存需要回收。

2.1 引用计数算法

通过在对象头部分配一个空间,来保存该对象被引用的次数。对象被其他对象引用,则它的计数加1,删除该对象的引用,计数减1,当该对象计数为0时,会被回收。

String m = new String("方糖算法");

创建一个字符串m,这时候"方糖算法"字符串被m引用了,"方糖算法"字符串计数加1。
引用计算算法
此时将m设置为null,则"方糖算法"的引用次数就变为0,意味着要被回收了。

m = null;

引用计数算法
引用计数算法将垃圾回收分摊到整个程序运行中,而不是在垃圾收集时,不属于严格意义上的"Stop-The-World"的垃圾收集机制。

JVM放弃了引用计数算法,这是为什么?我们看下面的例子。

public class ReferenceCountingGC {

    public Object instance;

    public ReferenceCountingGC(String name){}
}

public static void testGC(){

    ReferenceCountingGC a = new ReferenceCountingGC("objA");
    ReferenceCountingGC b = new ReferenceCountingGC("objB");

    a.instance = b;
    b.instance = a;

    a = null;
    b = null;
}
  1. 定义2个对象a,b
  2. 相互引用
  3. 声明引用置空
    引用计数缺点
    从图中,ab置空后,这两个对象已经不能被访问了,但是他们相互引用对方,导致他们两个的计数永远不为0,永远不会被回收。

2.2 可达性分析算法

通过引用链(GC Root)作为起点,向下搜索,搜索过的路径被称为(Reference Chain)。当一个对象不能被引用链搜索到,说明该对象不可用,被回收。
可达性算法
通过可达性算法,可以解决引用计数算法无法解决的循环依赖问题,只要不能被GC Root搜索到,就会被回收。

那么哪些属于GC Root?往下看鸭!

2.3 Java内存区域

在Java中,GC Root 对象包括四种:

  • 虚拟机栈中的引用对象
  • 方法区静态属性引用的对象
  • 方法区常量引用的对象
  • 本地方法栈JNI引用的对象
    内存区域

虚拟机栈中的引用对象
此时的 s,即为GC Root。 当s置空时,localParameter对象也断掉与GC Root 的引用链,将被回收。

public class StackLocalParameter {
    public StackLocalParameter(String name){}
}

public static void testGC(){
    StackLocalParameter s = new StackLocalParameter("localParameter");
    s = null;
}

方法区静态属性引用的对象
s 为 GC Root,s 置空后,s 指向的 properties对象被回收。
m 为类静态属性,也属于GC Root,parameter对象依旧与 GC Root 连接着,所以不会被回收。

public class MethodAreaStaicProperties {
    public static MethodAreaStaicProperties m;
    public MethodAreaStaicProperties(String name){}
}

public static void testGC(){
    MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties");
    s.m = new MethodAreaStaicProperties("parameter");
    s = null;
}

方法区常量引用的对象
m 为常量引用,是GC Root,s 置空后,final对象也会被回收。

public class MethodAreaStaicProperties {
    public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final");
    public MethodAreaStaicProperties(String name){}
}

public static void testGC(){
    MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties");
    s = null;
}

本地方法栈中引用的对象
任何 Native 接口都会使用某种本地方法栈,实现的本地方法是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。
当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈,然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,虚拟机只是简单地动态连接并直接调用指定的本地方法。

3、怎么回收垃圾

在确定哪些垃圾可以回收后,我们来讨论一下如何高效的回收垃圾呢?

Java虚拟机没有规定实现垃圾收集器,所以各个厂商的虚拟机可以采用不同的方法实现垃圾收集器。

3.1 标记清除算法

标记清除
标记清除算法(Mark - Sweep),分为 2 部分,①先把内存中可回收的标记出来再把这些清理掉 ,清理完的区域变成未使用,等待下次使用。

但是存在一个很大的问题,那就是内存碎片

假设图中 中等方块是 2M,小的是 1M,大的是 4M。等回收完,内存就会被切成很多段。而开辟内存需要的是连续区域,需要一个 2M 的内存,用 2个 1M 是没法用的。这样就导致,其实内存还挺多,但是分散了无法使用。

3.2 复制算法

复制算法
复制算法(Copying),是在MS算法上演变而来,解决了内存碎片问题。它将内存按容量平分成两块,每次使用其中的一块。当一块用完了,将其存活的对象复制到另一块上,再把这一块内存清理掉。保证了内存连续可用,不会产生内存碎片,逻辑清晰,运行高效。

但是明显暴露了一个问题,合着我 300 平的别墅,只能当 150 平的小三房来使?代价太高了

3.3 标记整理算法

标记整理
标记整理算法(Mark - Compact)标记过程与MS算法一样,但后续不是直接回收对象,而是让存活的对象向一端移动,再清理端边界以外的内存

不仅解决了内存碎片,也规避了只能使用一半的内存。但是问题又来了,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比MS算法差很多。

3.4 分代收集算法

分代收集算法(Generational Collection),融合上面 3 种思想。 根据对象存活周期的不同划分为几块,一般分为新生代老年代,根据年代的特点采用适当的收集算法。

新生代:每次回收发现有大量对象死去,少量存活,则使用复制算法,付出少量存活对象复制的成本完成回收。
老年代:存活率高,没有额外空间分配,则使用MS,MC算法来回收。

问题又来了,内存区域被分为哪几块,每一块又用什么算法合适?

4、内存模型与回收策略

内存模型
Java 堆(Heap)是 JVM 管理的内存最大的一块,堆又是垃圾收集器管理的主要区域,我们来分析一下堆的结构。

堆主要分为 2 个区域,年轻代和老年代。年轻代分为 EdenSurvivor,其中 Survivor 又分为 FromTo ,老年代分为 Old

Eden
研究表名,98%对象是朝生夕死,所以大部分情况,对象会在新生代的 Eden 区分配,当 Eden 内存不足时,虚拟机发起一次 Minor GC, Minor GC 比 Major GC 更频繁,回收更快。

通过 Minor GC 后,Eden会被清空,绝大部分对象被回收,而那些存活对象会进入 Survivor 的 From 区(From 区不够,则进入 Old 区)。

Survivor
Survivor 相当于 Eden 区和 Old 区的一个缓冲区。通过 Minor GC 后,会将 Eden 区和 From 区的存活对象放到 To 区(To 区不够,则进入 Old 区)。

为啥需要缓冲区?

不就是新生代到老年代吗,直接 Eden 到 Old 不就好了?非要这么复杂。
如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到 Old 区,老年代很快被填满。而且很多对象虽然一次 Minor GC 后没有死,可能一会后就死了,直接把它送入老年代,明显不合适。

总结:Survivor 存在的意义就是减少被送到老年代的对象,减少 Major GC 的发送。Survivor 的预筛保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

为啥需要两个缓冲区?
两个 Survivor 最大的好处是解决内存碎片化

如果 Survivor 有1个区域,Minor GC 执行后,Eden 区被清除,存活对象放入 Survivor 区,而之前 Survivor 区的对象可能也有一部分要清除。此时只能使用MS算法,那就会产生内存碎片,尤其是在新生代这种经常死亡的区域,产生严重碎片化。

如果 Survivor 有2个区域,每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责切换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。(有点复制算法的感觉)

这种机制最大的好处就是,永远有一个 Survivor 区是空的,另一个 区是无碎片的。那为啥不分更多的 Survivor 区呢?分的越多,每个区就越小,两块是经过权衡的最佳方案。

Old
老年代占据着 2/3 的堆内存,只有 Major GC 才会清理,每次 GC 都会触发 “Stop-The-World”。内存越大 STW时间越长,所以内存不是越大越好。老年代对象存活时间较长,采用MC算法

除了上面说的,无法安置的对象会直接送入老年代,以下情况也可以。

  • 大对象
    大对象是需要大量连续空间的对象,不管是不是"朝生夕死",都会直接进入老年代。避免在 Eden 和 Survivor 中来回复制。
  • 长期存活对象
    虚拟机给每个对象定义了对象年龄计数器。对象在 Survivor 区每经历一次 Minor GC ,年龄加1,当年龄为15岁,直接进入老年代。这里的15可以设置。
  • 动态对象年龄
    虚拟机不关注年龄必须到15岁才可以进入老年代,如果Survivor 区相同年龄的所有对象大小超过 Survivor 空间一半,则年龄大于相同年龄的对象就进入老年代,无需成年。

5、微信关注『方糖算法』

各类面试资料、内推资源,关注微信公众号获取哦。

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值