JVM学习笔记(4)---垃圾收集器
了解垃圾收集器,我们需要搞清三个问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
哪些内存需要回收?
垃圾收集(Garbage Collection,GC),即常说的GC。
尽管目前“自动化”时代,内存由JVM自动回收,但需要了解其回收机制。当出现内存溢出、泄漏问题时方便排查,当垃圾收集成为系统达到更高并发量的瓶颈时,方便监控调节。
由于程序计数器、虚拟机栈、本地方法栈随线程生灭,且每个栈帧分配多少内存都是在类结构确定下来就已知,所以这几个区域的内存分配和回收都有确定性。
而堆和方法区的内存分配是 动态 的,这里所讨论的GC即指 堆和方法区 的内存。
什么时候回收?
内存的回收时机主要在于确定 对象是否还通过任何途径被使用。
引用计数算法(已经淘汰的算法)
引用计数算法是给每个对象添加一个引用计数器,每当一个地方引用它,计数器就加一;引用失效时,计数器就减一。
其特点是:算法很简单,判定效率高。但它不能解决相互引用的问题。
看如下示例代码:
public class ReferenceCountingGC {
public Object instance = null;
//这个成员属性单纯用于内存占用,方便查看是否回收
private byte[] bigSize = new byte[2*1024*1024];
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
//清除引用
objA = null;
objB = null;
//GC 观察objA与objB能否被回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
对象ObjA与ObjB互相引用,但再无其他引用。如果采用引用计数算法,GC就无法回收他们。
主流的JVM中,没有采用引用计数法,就是因为它无法解决对象间循环引用的问题。
运行上段代码,我们可以看到内存 4328k->644k 的变化,说明对ObjA和ObjB的内存进行了回收,也说明Java虚拟机中没有采用引用计数算法。
可达性分析算法
主流商用程序语言(Java,C#等)中,都是通过 可达性分析(Rechability Analysis) 判断对象存活。
可达性分析是通过一系列的"GC Roots"作为起始点,向下开始搜索,所经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何链相连,则对象不可用。
Java中,可作为GC Roots的对象包括以下几种:
虚拟机栈中引用的对象 (栈帧中的局部变量表)
本地方法栈中JNI引用的对象 (JNI即Native方法)
方法区中静态属性引用的对象 (static)
方法区中常量引用的对象 (final)
对象引用
判断对象的存活,无论是引用计数法计算数量,还是可达性分析法判断引用的可达性,都与 引用 有关。
引用可分为 4 类:
- 强引用(Strong Reference):形如 Object obj = new Object(),只要引用在,GC不回收对象。
- 软引用(SoftReference):内存溢出前,会进行二次回收,适合做缓存
- 弱引用(WeakReference):下一次GC前,会直接进行回收
- 虚引用(PhantomRefence):仅用于对象回收时发送系统通知
强引用
强引用的对象,不会被GC回收;删除强引用后,GC才会回收。
示例代码如下:
/**
* 强引用:GC不会回收对象
*/
public class StrongRef {
public static void main(String[] args) throws InterruptedException {
Referred strong = new Referred();
System.gc();
Thread.sleep(2000);
//删去引用
strong = null;
System.out.println("删除强引用后");
System.gc();
Thread.sleep(2000);
}
static class Referred {
@Override
protected void finalize() throws Throwable {
System.out.println("GC时引用对象被收集");
}
}
}
finalize()函数:如果一个对象覆盖了finalize()函数,则在对象被回收时,finalize()方法会被GC收集器触发。每个对象的finalize()函数只会被系统调用一次。
运行结果如下:
删除强引用后
GC时引用对象被收集
当强引用存在时,GC时对象没被回收;当强引用被删除,GC时对象被回收。
软引用
软引用的对象,在内存溢出前,会进行二次回收,这种特性非常适合做缓存。
示例代码如下:
/**
* VM args: -Xmx100m -Xms100m
*
*/
public class SoftRef {
public static void main(String[] args) throws InterruptedException {
SoftReference<Referred> soft = new SoftReference<Referred>(new Referred());
System.gc();
Thread.sleep(2000);
System.out.println("开始堆占用");
try {
List<SoftRef> heap = new ArrayList<>();
while (true) {
heap.add(new SoftRef());
}
} catch (OutOfMemoryError e) {
// 软引用对象应该在这个之前被收集
System.out.println("内存溢出");
}
}
static class Referred {
@Override
protected void finalize() throws Throwable {
System.out.println("GC时引用对象被收集");
}
}
}
运行结果如下:
开始堆占用
GC时引用对象被收集
内存溢出
当内存足够,GC时不回收软引用对象;当内存快溢出时,自动二次回收软引用对象。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用
被弱引用的对象,如果只有弱引用与之关联,则GC时,对象会被回收掉;
如果存在强引用同时与之关联,则GC时,不会回收该对象。(对一条对软引用也适用)
示例代码如下:
代码1:对象只有弱引用,gc直接被回收
/**
* 弱引用,对象只有弱引用,gc直接被回收
*
*/
public class WeakRef1 {
public static void main(String[] args) {
WeakReference<String> weak = new WeakReference<>(new String("weakRef"));
System.out.println(weak.get());
System.gc();
System.out.println(weak.get());
}
}
运行结果为:
weakRef
null
代码2:对象有弱引用,也有强引用,gc不被回收;删除强引用后,gc回收
/**
* 弱引用,下一次gc即回收对象
*
*/
public class WeakRef2 {
public static void main(String[] args) throws InterruptedException {
String s = new String("weakRef");
WeakReference<String> weak = new WeakReference<>(s);
System.out.println(weak.get());
System.gc();
Thread.sleep(2000);
System.out.println(weak.get());
s = null;
System.gc();
Thread.sleep(2000);
System.out.println(weak.get());
}
}
运行结果为:
weakRef
weakRef
null
只有弱引用的对象,GC时直接回收
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用
虚引用不影响对象的生命周期,如果一个对象仅持有虚引用,有它和没有任何引用一样。
它需要和引用队列联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
示例代码如下:
/**
* 虚引用 与引用队列联合使用
*/
public class PhantomRef {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
运行结果为:
null
方法区回收
之前所谈的对象回收主要在堆内存,方法区尽管回收效率低,也是有垃圾需要回收的。主要分为两部分:废弃常量 和 无用的类。
回收常量池和回收堆中的对象类似,比如“abc”加入常量池,没有任何String对象引用常量池中的“abc”,那么就要回收。常量池中的其他类(接口)、方法、字段符号引用也与此类似。
无用类的判断需要满足3个条件:
- 堆中不存在该类的任何实例,所有该类实例都被回收。
- 加载该类的ClassLoader已经被回收。
- 类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
在大量使用反射、动态代理、Cglib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
如何回收?
标记-清除算法
“标记-清除”(Mark-Sweep)算法:先通过可达性分析算法 标记 需要回收的对象,再统一 清除 标记对象。
缺点是:过程效率低;会产生大量不连续的空间碎片
复制算法
“复制”(Copying)算法:将内存分为两块,每次只使用一块,当一块用完了,将存活对象复制到另一块上面。
优点是:简单高效;没有碎片
缺点是:内存缩小一半,硬件代价高
由于复制效率考虑,适用于对象存活率低的新生代。
标记-整理算法
“标记-整理”(Mark-Compact)算法:先标记回收对象,之后让存活对象向一端移动,直接清理掉端边界以外的内存。适合于对象存活率高的老年代。
分代算法
根据对象存活周期,把Java堆分为新生代和老年代,根据各个年代的特点采用最合适的收集算法。在新生代中,每次垃圾收集时有大批对象死去,只有少量存活,可以选用复制算法。而老年代对象存活率高,使用标记清除或者标记整理算法。
HotSpot的算法实现
由于本篇已经写了很长了,所以把目前主流使用的HotSpot JVM如何实现垃圾收集的,放到下一篇文章进行单独分析。