垃圾 是指运行程序中 没有任何引用指向的对象,需要被回收。
内存溢出:经过垃圾回收之后,内存仍旧无法存储新创建的对象,内存不够溢出。
内存泄漏:又叫 “存储泄漏”,对象不会再被程序使用了,但是 GC 又不能回收它们。例如:IO 流不适用了但是没有被 Close、数据库连接 JDBC 没有被 Close。这些对象不会被回收就会占据内存,大量的此类对象存在,也是导致内存溢出的原因。
垃圾回收(Garbage Collection
,简称 GC
)是内存管理的核心组成部分,它负责自动回收不再使用的内存空间。在 Java 中,程序员不需要手动释放对象占用的内存,一旦对象不再被引用,垃圾回收器就会在适当的时机回收它们所占用的内存。这样可以避免 内存泄漏 和 野指针,从而大大减轻了程序员的负担,也使得 Java 成为一个相对安全、易于开发的编程语言。
垃圾回收的基本步骤分两步:
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}
通过 GC Roots
作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
JNI
(Java Native Interface
)引用的对象public void method() {
Object localVariable = new Object(); // localVariable 是 GC Roots
}
public class MyClass {
private static Object staticObject = new Object(); // staticObject 是 GC Roots
}
public class MyClass {
private static final String CONSTANT_STRING = "constant"; // CONSTANT_STRING 是 GC Roots
}
public synchronized void synchronizedMethod() {
// 当前对象(this)在执行同步方法时是 GC Roots
}
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 中有四种类型的引用,它们对垃圾回收的影响不同:
Strong Reference
):最常见的引用类型,只要对象有强引用指向,它就不会被垃圾回收。Soft Reference
):软引用可以帮助垃圾回收器回收内存,只有在内存不足时,软引用指向的对象才会被回收。Weak Reference
):弱引用指向的对象在下一次垃圾回收时会被回收,不管内存是否足够。Phantom Reference
):虚引用的主要用途是跟踪对象被垃圾回收的状态,虚引用指向的对象总是可以被垃圾回收。import java.lang.ref.*;
public class ReferenceTypes {
public static void main(String[] args) {
Object strongRef = new Object(); // 强引用
SoftReference<Object> softRef = new SoftReference<>(new Object()); // 软引用
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>()); // 虚引用
System.gc(); // 触发垃圾回收
System.out.println("Strong Reference: " + strongRef);
System.out.println("Soft Reference: " + softRef.get());
System.out.println("Weak Reference: " + weakRef.get());
System.out.println("Phantom Reference: " + phantomRef.get());
}
}
它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
算法分为“标记”和“清除”阶段:
标记清除算法分为两个主要步骤:标记 和 清除。
GC Roots
开始,遍历所有可达的对象,并标记它们为活动对象。有两个明显的问题:
标记整理算法是标记清除算法的改进版本。它在标记和清除的基础上增加了整理阶段,将所有活动对象向一端移动,从而消除内存碎片。
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
Young Generation
):使用复制算法,因为新生代中的对象生命周期较短。Tenured Generation
):使用标记整理或标记清除算法,因为老年代中的对象生命周期较长,且数量较少。eden
区和两个 survivor
(survivor0
,survivor1
)区(一般而言)。大部分对象在 eden
区中生成。回收时先将 eden
区存活对象复制到一个 survivor0
区,然后清空 eden
区,当这个 survivor0
区也存放满了时,则将 eden
区和 survivor0
区存活对象复制到另一个 survivor1
区,然后清空 eden
和这个 survivor0
区,此时 survivor0
区是空的,然后将 survivor0
区和 survivor1
区交换,即保持 survivor1
区为空, 如此往复。survivor1
区不足以存放 eden
和 survivor0
的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次 Full GC
(Major GC
),也就是新生代、老年代都进行回收。Minor GC
,Minor GC
发生频率比较高(不一定等 eden
区满了才触发)。2️⃣ 老年代的回收算法(以 标记-清除、标记-整理 为主)
Major GC
,Major GC
发生频率比较低,老年代对象存活时间比较长,存活率标记高。3️⃣ 永久代(Permanet Generation
)的回收算法
JDK 1.8 及以后方法区的实现变成了元空间。
用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。永久代也称 方法区。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过根搜索算法来判断,但是对于无用的类则需要同时满足下面 3 个条件:
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。标记复制 | 标记清除 | 标记压缩 | |
---|---|---|---|
速率 | 最快 | 中 | 最慢 |
空间开销 | 两个大小相同的空间 | 少(会堆积碎片) | 少(不会碎片堆积) |
移动对象 | 是 | 否 | 是 |