GC基础
# 概述
# 什么是GC
垃圾收集(Garbage Collection)通常被称为“GC”。
每个程序员都遇到过内存溢出的情况,程序运行时,内存空间是有限的,那么如何及时的把不再使用的对象清除将内存释放出来,这就是GC要做的事。
GC需要完成的3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
目前内存的动态分配与内存回收技术已经相当成熟,那为什么我们还要去了解GC和内存分配呢?
答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。
# GC主要针对的区域
前面介绍过Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。虚拟机栈中每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题。因为方法结束或者线程结朿时,内存自然就跟随着回收了。
Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样。我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。尤其是Java堆。
# GC日志输出参数
- -XX:+PrintGC 输出GC日志
- -XX:+PrintGCDetails 输出GC的详细日志
- -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
- -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- -Xloggc:gc.log 日志文件的输出路径。如果未设置,默认输出到控制台
示例:
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:D:/gc.log
# 怎样判断对象已死
Java堆里面存放着Java世界中几乎所有的对象实例。垃圾收集器在对堆进行回收前第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。通常有两种算法:引用计数算法和可达性分析算法。
# 引用计数算法(Reference Counting)
引用计数算法(Reference Counting)的基本思路为:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法的实现简单,判定效率也很髙,在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,比如微软的COM(Component Object Model)、使用ActionScript3的FlashPlayer、Python、游戏领域的Squirel都使用此种算法。
但是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之相互循环引用的问题。
举个简单的例子:
public class ReferenceCountDemo {
public static void main(String[] args) {
GcObject obj1 = new GcObject();// step1
GcObject obj2 = new GcObject();// step2
obj1.instance = obj2;// step3
obj2.instance = obj1;// step4
obj1 = null;// step5
obj2 = null;// step6
}
}
class GcObject {
public Object instance;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
如果采用的是引用计数算法,执行完step4之后,引用状况应当如下图所示:

可以发现GcObject实例1和实例2引用计数均为2,但当执行完step6之后,栈帧中的obj1和obj2不再指向Java堆,GcObject实例1和实例2引用计数均为1,都不为0,那么这两个实例所占的内存得不到释放,这便产生了内存泄漏。
# 可达性分析算法(Reacffability Analysis)
可达性分析算法(Reacffability Analysis)的基本思路为:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
如图,虽然对象object 5、object 6、object 7互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

在Java语言中,可作为GC Roots的对象包括下面几种:
- 栈帧中的本地变量表中的引用对象
- 方法区中的静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
# 可达性分析---准确式GC
从可达性分析中从GC Roots节点找引用链这个操作为例,如果要逐个进行枚举检査所有可作为GC Roots对象的引用链,那么必然会消耗很多时间。如何解决这个问题呢?
HotSpot采用了准确式GC以提升GC roots的枚举速度。所谓准确式GC,就是让JVM知道内存中某位置数据的类型什么。比如当前内存位置中的数据究竟是一个整型变量还是一个引用类型。这样JVM可以很快确定所有引用类型的位置,从而更有针对性的进行GC roots枚举。
HotSpot是利用OopMap来实现准确式GC的。当类加载完成后,HotSpot 就将对象内存布局之中什么偏移量上数值是一个什么样的类型的数据这些信息存放到 OopMap 中;在 HotSpot 的 JIT 编译过程中,同样会插入相关指令来标明哪些位置存放的是对象引用等,这样在 GC 发生时,HotSpot 就可以直接扫描 OopMap 来获取对象引用的存储位置,从而进行 GC Roots 枚举。
# 可达性分析---安全点(Safepoint)
可达性分析对执行时间的敏感还体现在GC停顿上,因为在整个可达性分析期间要保证对象之间的引用关系不再变化,否则准确性就无法保证。这就导致在GC进行时必须停顿所有的JAVA执行线程(Sun将其称为Stop The World)。
通过OopMap,HotSpot可以很快完成GC Roots的查找,但是,如果在每一行代码都有可能发生GC,那么也就意味着得为每一行代码的指令都生成OopMap,这样将占用大量的空间。实际上,HotSpot也不会这么做。
HotSpot只在特定的位置记录了OopMap,这些位置就叫做安全点(Safepoint),也就是说,程序并不能在任意地方都可以停下来进行GC,只有到达安全点时才能暂停进行GC。
在安全点中,HotSpot也会开始记录虚拟机的相关信息,如OopMap信息的录入。安全点的选择不能太少,否则GC等待时间太长;也不能太多,否则会增大运行负荷,其选择的原则为“是否具有让程序长时间执行的特征”,最明显特征就是指令序列复用,例如循环体的结尾、方法返回前、调用方法的call之后、抛出异常的位置,所以具有这些功能的指令才会产生Safepoint。
安全点暂停线程运行的手段有两种:抢先式中断和主动式中断。
- 抢先式中断:不需要线程的执行代码主动配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上再暂停。不过现在的虚拟机几乎没有采用此算法的
- 主动式中断:GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时去主动轮询查询此标志,发现中断标志为真时就中断自己挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方
# 可达性分析---安全区域(Safe Region)
安全点机制保证了程序执行时进入GC的问题。但是对于非执行态下,如线程Sleep或者Block下,由于此时程序(线程)无法响应JVM的中断请求,JVM也不太可能一直等待线程重新获取时间片,此时就需要安全区域(Safe Region)了。
安全区域是指在一段代码片段内,引用关系不会发生变化,在这段区域内,任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region。当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了;当线程要离开Safe Region时,如果整个GC完成,那线程可继续执行,否则它必须等待直到收到可以安全离开Safe Region的信号为止。
# 再谈引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用(reference)”有关。
在 JDK1.2 之前,Java中的引用定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。但一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象。比如我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中:如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference) 4种。这4种引用强度依次逐渐减弱。
# 强引用(Strong Reference)
强引用就是指在程序代码之中普遍存在的,类似Object obj = new Object()这类的
引用。
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。哪怕内存不足时,JVM也会直接抛出OOM,不会去回收。
# 软引用(Soft Reference)
软引用是用来描述一些还有用但并非必需的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
我们不妨做个实验,先配置参数-Xms3M -Xmx3M将堆容量设置为3M,然后循环创建10个大小为1M的字节数组加入到list中,如果是正常的强引用,那必然会发生内存溢出,接着来看一下软引用会有什么不一样:
@Test
public void testSoftReference() {
List<SoftReference> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
// 创建1M大小字节数组
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
list.add(sr);
}
System.gc(); //主动通知垃圾回收
for (int i = 0; i < list.size(); i++) {
Object obj = list.get(i).get();
System.out.print(obj + "\t");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
打印结果为:
null null null null null null null null null [B@6aa8ceb6
我们发现无论循环创建多少个软引用对象,打印结果总是只有最后一个对象被保留,其他的obj全都被置空回收了。 这里就说明了在内存不足的情况下,软引用将会被自动回收。
# 弱引用(Weak Reference)
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
在 JDK1.2 之后,用java.lang.ref.WeakReference来表示弱引用。
我们以与软引用同样的方式来测试一下弱引用:
@Test
public void testWeakReference() {
List<WeakReference> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
// 创建1M大小字节数组
byte[] buff = new byte[1024 * 1024];
WeakReference<byte[]> sr = new WeakReference<>(buff);
list.add(sr);
}
System.gc(); //主动通知垃圾回收
for (int i = 0; i < list.size(); i++) {
Object obj = list.get(i).get();
System.out.print(obj + "\t");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
打印结果为:
null null null null null null null null null null
可以发现所有被弱引用关联的对象都被垃圾回收了,这里就说明只要 JVM 开始进行垃圾回收,弱引用关联的对象都会被回收。
# 虚引用(Phantom Reference)
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 java.lang.ref.PhantomReference类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
虚引用存在的唯一的目的就是在垃圾回收的时候可以收到一个系统通知。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
# 两次标记与finalize
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
- 确认GCRoot到此对象已经没有任何可达路径
- 判断此对象是否有必须要执行finalize方法,当对象没有覆 盖finalize()方法或者finalize()已经被虚拟机调用过,则虚拟机都不会再去执行finalize
如果一个对象确认需要执行finalize方法,那么会将此对象放入一 个F-Queue的队列中,并且由一个虚拟机自动创建的、低优先级的Finalizer线程去执行。
但虚拟机执行finalize()方法,可能不会等到它运行结束;因为如果某一个对象的finalize执行缓慢、死循环等都会影响后续的 finalize()方法执行;还会导致垃圾回收系统混乱。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Qucue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己---只要重新与引用链上的任何一个对象建立关联即可。譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合:如果对象这时候还没有逃脱,那基本上它就真的被回收了。
但是极其不推荐使用这中方法来拯救对象,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时。
# 回收方法区
很多人认为方法区是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集。回收方法区的性价比远远低于回收JAVA堆,常规的一次GC可以回收Heap的70%~95%的空间,而对于方法区则非常有限。
方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类。
如何判定一个常量是否是“废弃常量”?
常量池里的对象没有任何引用,也没有任何地方引用这个字面 量。如果这时发生内存回收,而且必要的话,这个常量就会被系统清理出常量池。
而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:
- 该类的所有实例都已经被回收,不存在任何这个类的实例
- 加载这个类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方引用,无法在任何地方通过反射访问该类
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。