Java 引入了垃圾回收机制(GC),更好的应对内存泄漏问题。JVM 专门指派一些线程,这些线程周期性地扫描你代码中已经申请的内存(new出来的对象),自动判定当前这个内存是否已经不再使用了。如果不再使用,释放掉对象/内存。
一、Java的GC要负责回收的内存
程序计数器是很小的区域,只保存一个数字,GC不需要负责回收。栈上的局部变量会随着栈帧自动释放,GC不需要负责。元数据区的类对象通常只需要加载,不需要卸载。因此,堆是GC的主战场。堆上存放的是对象,GC是以对象为单位进行内存回收的,不是以字节为单位。一个对象,要么整个释放,要么不释放,不会出现 "释放一半"。

二、垃圾回收的流程
1. 找出谁是垃圾(不再使用的对象)
Java 使用对象只有一种方式,通过 "引用" 的方式。把对象想象成 "房间",引用是打开房间的 "钥匙"。如果某个对象,没有引用指向,此时就可以认为这个对象不再使用了。判定对象是否是垃圾,从引用入手。

- 如何判定对象有没有引用指向呢?
第一种方案:引用计数
引用计数不是Java的方案,而是Python/PHP的方案。面试题:谈一谈Java的垃圾回收机制(可以不谈引用计数);谈一谈垃圾回收机制(就可以谈引用计数)。引用计数给每个对象身上再安排一个空间,这个空间存储一个整数,表示指向这个对象 引用的个数。围绕对象进行 "引用复制" 就会更新这个计数。如果计数是0了,此时就可以释放了。

引用计数方案,存在两个缺点:
(1) 消耗更多的内存空间。如果计数器使用2个字节的,万一整个对象就只有4个字节,且这样的对象数量非常多,此时引用计数就占据了50%;如果对象比较大,引用计数所占用的内存就可以忽略不计。
(2) 产生循环引用,会导致出现误判

Python/PHP 采用引用计数这样的方案,同时也引用了一些环路检测的机制,识别出上述的循环引用。
第二种方案:可达性分析

可达性分析,就是从树根节点出发(根节点可能有多个),尝试遍历这个对象树,遍历过程中凡是经过的对象,都标记成 "可达"。另一方面,JVM自身知道自己一共都有哪些对象,除去可达的,剩下的就是 "不可达"(当成垃圾回收了)。随着代码的运行,对象之间的引用关系是实时变化的,上述可达性分析需要周期性进行。
- GC Roots 通常包括以下几种元素(都在堆外):
(1)虚拟机栈(栈帧中的本地变量表)中的引用:
-
通俗讲,就是正在被执行的各个方法中的局部变量(包括参数)。
-
为什么是 Root? 这些方法正在运行,它们引用的对象显然是程序当前需要的、活跃的对象。如果这些对象都被回收了,程序肯定会出错。
(2)本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象:
- 为什么是 Root? 和虚拟机栈类似,这些 Native 方法正在执行,它们引用的 Java 对象必须存活。
(3)方法区中类静态属性(static fields)引用的对象:
-
例如:
public static MyClass singleton; -
为什么是 Root? 静态变量属于类,而类本身是由类加载器加载的。只要类加载器存活,它的静态变量引用的对象就应该是存活的,因为它们可以被任何地方访问到。
(4)方法区中常量(final static constants)引用的对象:
-
例如:
public static final String NAME = "Java"; -
为什么是 Root? 常量和静态属性类似,是全局可访问的,其生命周期与类相同。
-
举例 :Integer值 JVM把 -128~127 这个范围的数字提前创建好 Integer 对象. 提前创建并缓存,是为了避免频繁的 new Integer() 操作;缓存对象长期存在,减少GC压力。
java
public class Integer {
private static class IntegerCache {
static final Integer cache[]; // 这个静态常量数组在方法区
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
}
(5)常量池引用指向的对象
- 字符串常量池本身就是一个GC Root ,它不是通过"方法区中的常量引用"来保持对象存活的,字符串常量池直接持有对String对象的引用
java
public void example() {
String s = "hello"; // "hello"被字符串常量池引用,是GC Root
// 即使s是局部变量,当方法结束时,"hello"依然不会被GC
// 因为它被字符串常量池这个系统级的GC Root引用着
}
一轮 GC 就要把所有的 GCRoots 尽可能遍历到,标记为可达。
不可达对象:
java
public void createGarbage() {
Object temp = new Object(); // 创建对象
// 方法结束,temp引用消失,new Object()变得不可达,成为垃圾
}

可达性分析的优点:没有引入额外的内存空间,没有循环引用问题。缺点:需要消耗更多的CPU资源,更多的时间(用时间换空间),这是C++不愿意引入GC的原因,C++追求极致的性能。
总结:如何识别对象是垃圾?
方法一 通过引用识别。方法二 通过可达性分析的方式,从所有的GCRoots出发,尽可能遍历,访问到的对象都标记为可达,剩下的就是不可达。
2. 释放垃圾对象对应的内存
(1)标记---清除
把标记出来的垃圾,直接释放掉

这些被释放的内存的地址是 "离散"的,不连续的,申请内存都是申请连续 的内存空间,可能会导致总的空闲空间充足,但是尝试申请一个大块内存,申请失败!进程中,哪些对象为垃圾是随机的,这种方案很可能会在频繁释放中产生大量的内存碎片。
(2)复制算法
可以有效解决内存碎片问题

缺点:1. 空间利用率很低(最多只能用到50%)2. 如果当前存活的对象很多,复制开销就很大
(3)标记---整理
对于空间利用率,得到了改善。
类似于顺序表删除中间元素 => 搬运(开销同样可能很大)

(4)分代回收
Java 的实际情况是综合了上面的策略,构成了一个更复杂的方案。分代回收:根据不同对象的情况/特点,采取不同的方案。GC周期性地进行扫描,根据扫描的轮次 决定 对象的年龄。如果某个对象经过很多轮GC,都没有被释放,说明年龄比较大了。
经验规律:如果某个对象年龄比较大,有很大概率继续存活下去,因为要G早G了。C语言从诞生到现在已经50多年了,同辈的语言有很多,目前存活下来的除了C语言只有SQL,能活下来说明确实很有东西 => 很大概率继续存活50年
整个堆,分成两个大的部分:

- 新 new 的对象,放到伊甸区中。
- 伊甸区对象经过第一轮GC的时候,把绝大部分淘汰掉(经验规律:大部分新对象的生命周期非常短 "朝生夕死"),没有淘汰的对象 通过复制算法进入到幸存区,幸存区有两个部分,每次只用其中的一部分。
- 下一轮GC不仅扫描伊甸区,也会针对幸存区的对象扫描,即使是幸存区的对象还会再次淘汰掉一大批。没有淘汰的对象,通过复制算法进入到另一个幸存区。由于经验规律发现,每轮GC淘汰掉大部分的新对象,因此触发复制的对象就很少,解决了复制算法拷贝开销大的问题;另一方面,幸存区只是占据新生代的10%,浪费的内存空间比较小了。
- 随着每一轮GC的进行,对象在新生代的幸存区来回拷贝,每经过一次拷贝,对象的年龄就+1。
- 经过一定时间之后,对象的年龄达到一定的阈值,此时就会把这个对象拷贝到老年代。
- 对象进入老年代之后,针对老年代的GC频率比新生代低很多了,减少了扫描的开销。老年代发现对象是垃圾,采取标记整理的方式处理,虽然整理一次比较消耗资源,但是频率比较低。
- 如果某个对象的内存非常大,可能直接进入老年代,避免在幸存区来回拷贝。
分代回收的过程,和找工作的过程是完全一样。投递简历,进入了伊甸区,此时淘汰掉绝大部分人 => 简历筛选通过,进入笔试/面试环节,公司的面试通常有很多轮,不同的公司不一样 2到5轮都有 => 经过了所有面试后,拿到offer进入公司了 => 进入公司,还有一个绩效考核,采取末位淘汰制,这个就没那么频繁了,可能一年一次
面试题:请解释 Minor GC 和 Full GC 之间的区别
- Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。[ 开销比较大,频率低 ]
- Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。[ 开销比较小,频率高 ]
上面的分代回收过程,严格地说不是真实情况,而是一个简化版,是一种思想方法。垃圾回收器实现的时候,需要考虑到效率问题,对业务代码是否有影响。
垃圾回收器和垃圾回收算法之间的关系:垃圾回收算法是垃圾回收的方法论,垃圾收集器是算法的落地实现。比如:新生代的收集器:serial,ParNew,Parallel Scaveng,都使用的是复制算法;老年代的收集器:SerialOld,Parallel Old,使用的是标记整理算法,CMS是基于标记清除算法实现的。
