今天我们就从头到尾完整地聊一聊 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;
}
- 定义2个对象a,b
- 相互引用
- 声明引用置空
从图中,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 个区域,年轻代和老年代。年轻代分为 Eden 和 Survivor ,其中 Survivor 又分为 From 和 To ,老年代分为 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、微信关注『方糖算法』
各类面试资料、内推资源,关注微信公众号获取哦。
|