什么是GC?
GC 是 garbage collection 的缩写,意思是垃圾回收——把内存(特别是堆内存)中不再使用的空间释放掉;清理不再使用的对象。
为什么要GC?
堆内存是各个线程共享的空间,不能无节制的使用。服务器运行的时间通常都很长。累积的对象也会非常多。这些对象如果不做任何清理,任由它们数量不断累加,内存很快就会耗尽。所以GC就是要把不使用的对象都清理掉,把内存空间空出来,让项目可以持续运行下去。
什么样的对象是垃圾对象?
不再使用或获取不到的对象是垃圾对象。
如何把垃圾对象找出来?
办法1:引用计数法(不采用,不能解决循环引用问题)
办法2:可达性分析(从GC Roots对象出发,不可达的对象就是要清理的对象)
找到垃圾对象如何执行清理?
具体的GC算法
想要回收垃圾,必须得先知道,哪些对象可以被认定为垃圾。关于垃圾确定方式,主要有两种,分别是引用计数法与可达性分析法,其原理分别如下:
- 引用计数法是在对象每一次被引用时,都给这个对象专属的『引用计数器』+1。
- 当前引用被取消时,就给这个『引用计数器』-1。
- 当前『引用计数器』为零时,表示这个对象不再被引用了,需要让GC回收。
- 可是当对象之间存在交叉引用的时候,对象即使处于应该被回收的状态,也没法让『引用计数器』归零。
Member member01 = new Member();
Member member02 = new Member();
member01.setFriend(member02);
member02.setFriend(member01);
member01 = null;
member02 = null;
引用计数法的关键问题:该清理的对象清理不掉。
简单的说就是,在 Java 中,引用与对象相关联,如果要操作对象,则必须使用引用。因此,可以通过引用计数来确定对象是否可以回收。实现原则是,如果一个对象被引用一次,计数器 +1,反之亦然。当计数器为 0 时,该对象不被引用,则该对象被视为垃圾,并且可以被 GC 回收利用。
核心原理:判断一个对象,是否存在从『堆外』到『堆内』的引用。
为了解决引用计数法的循环引用问题,Java 采用了可达性分析的方法。其实现原理是,将一系列"GCroot"对象作为搜索起点。如果在"GCroot"和一个对象之间没有可达的路径,则该对象被认为是不可访问的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
了解了垃圾的确定方法后,我们将继续了解垃圾是怎么被回收的,即垃圾回收算法。在Java中主要有四中垃圾回收算法,分别是标记清除算法、复制算法、标记整理算法 和 分代收集算法。
引用计数算法很简单,它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
引用计数垃圾收集机制,它只是在引用计数变化为0时即刻发生,而且只针对某一个对象以及它所依赖的其它对象。所以,我们一般也称呼引用计数垃圾收集为直接的垃圾收集机制。垃圾收集的开销被分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。
优点:
- 实时性较高,不需要等到内存不够时才回收
- 垃圾回收时不用挂起整个程序,不影响程序正常运行
缺点:
- 回收时不移动对象, 所以会造成内存碎片问题
- 不能解决对象间的循环引用问题
“标记-清除”算法是最基础的算法,它的做法是当堆中的有效内存空间被耗尽的时候,就会暂停、挂起整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记:标记的过程其实就是,从根对象开始遍历所有的对象,然后将所有存活的对象标记为可达的对象。
清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
优点:实现简单
缺点:
- 效率低,因为标记和清除两个动作都要遍历所有的对象
- 垃圾收集后有可能会造成大量的内存碎片
- 垃圾回收时会造成应用程序暂停
由标记清除算法的实现我们可以看出,其主要存在两个缺点:
- 效率问题。标记和清除过程的效率都不高
- 空间问题。标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
为了解决标记清除算法内存碎片化严重的缺陷,提出了复制算法。复制算法主要思想是,按内存容量将内存划分为大小相等的两块区域。每次只使用其中一块,当这一块内存满后将其中存活的对象复制到另一块上去,然后把该内存中的垃圾对象清理掉,其实现过程如图:
优点1:在垃圾多的情况下(新生代),效率较高
优点2:清理后,内存无碎片
缺点:浪费了一半的内存空间,在存活对象较多的情况下(老年代),效率较差
复制算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
结合了以上两个算法,为了避免缺陷而提出。标记阶段和标记清理算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:
在结合以上三种算法的综合分析及 JVM 内存对象生命周期的特点,诞生了一种新的垃圾回收算法——分代收集算法。其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老年代(Tenured/Old Generation)和新生代(Young Generation)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
分代算法其实就是这样的,根据回收对象的特点进行选择。
串行:在一个线程内执行垃圾回收操作。
并行:在多个线程中执行垃圾回收操作。
CMS全称 (Concurrent Mark Sweep),是一款并发的、使用标记-清除算法的垃圾回收器。对CPU资源非常敏感。
启用CMS回收器参数 :-XX:+UseConcMarkSweepGC。
使用场景:GC过程短暂停顿,适合对时延要求较高的服务,用户线程不允许长时间的停顿。
优点:最短回收停顿时间为目标的收集器。并发收集,低停顿。
缺点:服务长时间运行,造成严重的内存碎片化。算法实现比较复杂。
G1(Garbage-First)是一款面向服务端应用的并发垃圾回收器, 主要目标用于配备多颗CPU的服务器,治理大内存。是JDK1.7提供的一个新收集器,是当今收集器技术发展的最前沿成果之一。
G1计划是并发标记-清除收集器的长期替代品。
启用G1收集器参数:-XX:+UseG1GC启用G1收集器。
G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合。
每块区域既有可能属于Old区、也有可能是Young区,因此不需要一次就对整个老年代/新生代回收。而是当线程并发寻找可回收的对象时,有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程,但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源)。这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率。
特点:
一整块堆内存被分成多个独立的区域Regions
存活对象被拷贝到新的Survivor区
新生代内存由一组不连续的堆heap区组成,使得可以动态调整各个区域
多线程并发GC
young GC会有STW(Stop the world)事件
名称 | 串行/并行/并发 | 回收算法 | 适用场景 | 可以与CMS配合 |
---|---|---|---|---|
SerialGC | 串行 | 复制 | 单CPU | 是 |
ParNewGC | 并行 | 复制 | 多CPU | 是 |
ParallelScavengeGC | 并行 | 复制 | 多CPU且关注吞吐量 | 否 |
名称 | 串行/并行/并发 | 回收算法 | 适用场景 |
---|---|---|---|
SerialOldGC | 串行 | 标记压缩 | 单CPU |
ParNewOldGC | 并行 | 标记压缩 | 多CPU |
CMS | 并发,几乎不会暂停用户线程 | 标记清除 | 多CPU且与用户线程共存 |
java.lang.Object 类中有一个方法:
protected void finalize() throws Throwable { }
方法体内是空的,说明如果子类不重写这个方法,那么不执行任何逻辑。
public class FinalizeTest {
// 静态变量
public static FinalizeTest testObj;
@Override
protected void finalize() throws Throwable {
// 重写 finalize() 方法
System.out.println(Thread.currentThread().getName() + " is working");
// 给待回收的对象(this)重新建立引用
testObj = this;
}
public static void main(String[] args) {
// 1、创建 FinalizeTest 对象
FinalizeTest testObj = new FinalizeTest();
// 2、取消引用
testObj = null;
// 3、执行 GC 操作
System.gc();
// ※ 让主线程等待一会儿,以便调用 finalize() 的线程能够执行
try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {}
// 4、判断待回收的对象是否存在
if (FinalizeTest.testObj == null) {
System.out.println("待回收的对象没有获救,还是要被 GC 清理");
} else {
System.out.println("待回收的对象被成功解救");
}
// 5、再次取消引用
FinalizeTest.testObj = null;
// 6、再次执行 GC 操作
System.gc();
// 7、判断待回收的对象是否存在
if (FinalizeTest.testObj == null) {
System.out.println("待回收的对象没有获救,还是要被 GC 清理");
} else {
System.out.println("待回收的对象被成功解救");
}
}
}
执行效果:
Finalizer is working
待回收的对象被成功解救
待回收的对象没有获救,还是要被 GC 清理