垃圾回收需要完成3件事:
-
哪些内存需要回收?
-
什么时候回收?
-
如何回收?
Java虚拟机内存运行时区域中程序计数器、虚拟机栈、本地方法栈这3个区域随着线程而生、随线程而灭,这几个区域的内存分配和回收都具备确定性,在这几个区域不需要过多考虑回收的问题,因为方法结束或线程结束,内存自然就跟着回收。
Java堆和方法区则不同,一个接口中的多个实现类需要的内存可能不同,一个方法的多个分支需要的内存也可能不同,程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收是动态的,因此垃圾收集器主要关注这部分内存。
引用
垃圾收集器对堆进行回收前,首先要确定堆中对象是否存活,判定对象是否存活与引用有关。
在JDK1.2之前,Java中的引用的定义为:若reference类型数据中存储的数值代表的是另外一块内存的起始地址,就称其代表着一个引用。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。
-
强引用:在程序代码之中普遍存在的,类似"Object obj=new Object()"这类引用,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
-
软引用:描述一些还有用但并非必需的对象,对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类实现软引用。
-
弱引用:描述非必需对象,它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
-
虚引用:也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
下面用程序来说明各个引用类型的区别:
/**
* VM Args:
* -verbose:gc
* -Xms8m
* -Xmx8m
* -Xmn4m
* -XX:+PrintGCDetails
*/
public class ReferenceTest {
static class TestObj{
private final int _1M = 1024*1024;
private String name;
private String field;
private byte[] bigSize = new byte[_1M];//占内存,以便在GC日志中看清楚是否被回收过
public TestObj(String name, String field) {
super();
this.name = name;
this.field = field;
}
public String toString() {
return "{name="+name+",field="+field+"}";
}
}
public static void main(String[] args) {
TestObj testObjSoft = new TestObj("softReference", "value");
TestObj testObjWeak = new TestObj("WeakReference", "value");
TestObj testObjPhan = new TestObj("PhantomReference", "value");
//软引用
SoftReference<TestObj> softReference = new SoftReference<ReferenceTest.TestObj>(testObjSoft);
//弱引用
WeakReference<TestObj> weakReference = new WeakReference<ReferenceTest.TestObj>(testObjWeak);
//虚引用
ReferenceQueue<TestObj> phReferenceQueue = new ReferenceQueue<ReferenceTest.TestObj>();
PhantomReference<TestObj> phantomReference = new PhantomReference<ReferenceTest.TestObj>(testObjPhan, phReferenceQueue);
testObjSoft = null;
testObjWeak = null;
testObjPhan = null;
System.gc();
/*
* byte[] block1 = new byte[2*1024*1024];
* byte[] block2 = new byte[1024*1024];
* byte[] block3 = new byte[1024*1024];
*/
if(softReference.get() != null)
{
System.out.println("softReference reference object is still alive!" +
softReference.get().toString());
}
else {
System.out.println("softReference reference object is not alive!");
}
if(weakReference.get() != null)
{
System.out.println("WeakReference reference object is still alive!" +
weakReference.get().toString());
}
else {
System.out.println("WeakReference reference object is not alive!");
}
Reference<TestObj> phantomReferenceQueue =(Reference<ReferenceTest.TestObj>) phReferenceQueue.poll();
if(phantomReferenceQueue != null)
{
if(phantomReferenceQueue.get() != null)
{
System.out.println("PhantomReference reference object is still alive!"+phantomReferenceQueue.get().toString());
}
else {
System.out.println("PhantomReference reference object is not alive!");
}
}
else {
System.out.println("phantomReferenceQueue is null!");
}
}
}
输出结果:
[GC (Allocation Failure) [PSYoungGen: 2833K->480K(3584K)] 2833K->2648K(7680K), 0.0016638 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 480K->0K(3584K)] [ParOldGen: 2168K->2568K(4096K)] 2648K->2568K(7680K), [Metaspace: 2573K->2573K(1056768K)], 0.0052472 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (System.gc()) [PSYoungGen: 1084K->128K(3584K)] 3653K->3720K(7680K), 0.0010806 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 128K->0K(3584K)] [ParOldGen: 3592K->2568K(4096K)] 3720K->2568K(7680K), [Metaspace: 2574K->2574K(1056768K)], 0.0048682 secs] [Times: user=0.09 sys=0.02, real=0.01 secs]
softReference reference object is still alive!{name=softReference,field=value}
WeakReference reference object is not alive!
PhantomReference reference object is not alive!
Heap
PSYoungGen total 3584K, used 121K [0x00000000ffc00000, 0x0000000100000000, 0x0000000100000000)
eden space 3072K, 3% used [0x00000000ffc00000,0x00000000ffc1e7a8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 4096K, used 2568K [0x00000000ff800000, 0x00000000ffc00000, 0x00000000ffc00000)
object space 4096K, 62% used [0x00000000ff800000,0x00000000ffa82388,0x00000000ffc00000)
Metaspace used 2583K, capacity 4490K, committed 4864K, reserved 1056768K
class space used 283K, capacity 386K, committed 512K, reserved 1048576K
输出结果表明软引用关联的对象存活,并进入了老年代,而弱引用和虚引用关联着的对象被回收。
将上述代码中注释部分
byte[] block1 = new byte[2*1024*1024];
byte[] block2 = new byte[1024*1024];
byte[] block3 = new byte[1024*1024];
加入到代码中,得到结果如下:
[GC (Allocation Failure) [PSYoungGen: 2833K->480K(3584K)] 2833K->2676K(7680K), 0.0017886 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 480K->0K(3584K)] [ParOldGen: 2196K->2568K(4096K)] 2676K->2568K(7680K), [Metaspace: 2573K->2573K(1056768K)], 0.0056902 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 1084K->128K(3584K)] 3653K->3720K(7680K), 0.0009335 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 128K->0K(3584K)] [ParOldGen: 3592K->2568K(4096K)] 3720K->2568K(7680K), [Metaspace: 2574K->2574K(1056768K)], 0.0045907 secs] [Times: user=0.13 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 2108K->2048K(3584K)] [ParOldGen: 2568K->2568K(4096K)] 4676K->4616K(7680K), [Metaspace: 2574K->2574K(1056768K)], 0.0016733 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 2048K->2048K(3584K)] [ParOldGen: 3592K->3592K(4096K)] 5640K->5640K(7680K), [Metaspace: 2574K->2574K(1056768K)], 0.0016471 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 2048K->2048K(3584K)] [ParOldGen: 3592K->2556K(4096K)] 5640K->4604K(7680K), [Metaspace: 2574K->2574K(1056768K)], 0.0047354 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
softReference reference object is not alive!
WeakReference reference object is not alive!
PhantomReference reference object is not alive!
Heap
PSYoungGen total 3584K, used 2148K [0x00000000ffc00000, 0x0000000100000000, 0x0000000100000000)
eden space 3072K, 69% used [0x00000000ffc00000,0x00000000ffe19398,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 4096K, used 3580K [0x00000000ff800000, 0x00000000ffc00000, 0x00000000ffc00000)
object space 4096K, 87% used [0x00000000ff800000,0x00000000ffb7f3f8,0x00000000ffc00000)
Metaspace used 2581K, capacity 4490K, committed 4864K, reserved 1056768K
class space used 283K, capacity 386K, committed 512K, reserved 1048576K
上述结果表明当堆内存不够用时,软引用关联着的对象就会被系统回收。
引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用,从而在垃圾收集时被回收。Java虚拟机没有选用引用计数法来管理内存,因为它很难解决对象之间相互循环引用的问题。
/**
* VM Args:-verbose:gc
* -Xms20m
* -Xmx20m
*/
public class ReferenceContingGC {
public Object instance = null;
private static final int block = 1024 * 1024;//1M
private byte[] bigSize = new byte[2*block];//占内存,以便在GC日志中看清楚是否被回收
public static void main(String[] args) {
ReferenceContingGC objA = new ReferenceContingGC();
ReferenceContingGC objB = new ReferenceContingGC();
objA.instance = objB;//此时引用次数为2
objB.instance = objA;//此时引用次数为2
objA = null;//此时引用次数为1
objB = null;//此时引用次数为1
//objA和objB是否能被回收?
System.gc();
}
}
运行结果如下所示:
[GC (System.gc()) 4998K->592K(19968K), 0.0007613 secs]
[Full GC (System.gc()) 592K->520K(19968K), 0.0045985 secs]
可以看出虚拟机回收了这两个对象,从而说明虚拟机不是通过引用计数算法来判断对象是否存活。
可达性分析算法
主流的商用语言(Java、C#、Lisp)都是通过可达性分析来判定对象是否存活。其基本思想是通过一系列“GC Roots”的对象作为起始点,从这些点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,也就是说从GC Roots到这个对象不可达,则证明此对象不可用,从而在垃圾收集时被回收。
Java中,可作为GC Roots的对象包括以下几种:
-
虚拟机栈中引用的对象。
-
方法区中类静态属性引用的对象。
-
方法区中常量引用的对象。
-
本地方法栈中JNI(即Native方法)引用的对象。
垃圾收集算法
标记-清除算法
算法分“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
其不足之处:
-
效率低:标记和清除两个过程的效率都不高;
-
空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。
复制算法
IBM公司专门研究表明,新生代的对象98%是“朝生夕死”,因此将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor空间中还存活的对象一次性地复制到另一块Survivor中,最后清理Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor空间大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%。但我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
标记-整理算法
老年代中因为对象存活率高、没有额外空间对它进行分配担保,因此不适合用复制收集算法。标记-整理算法的标记过程与标记-清除算法一样,但在后续步骤中是将存活的对象向一端移动,然后直接清理掉端边界以外的内存。
参考
- 《深入理解Java虚拟机JVM高级特性与最佳实践》
- 博客:java中的强引用(Strong reference),软引用(SoftReference),弱引用(WeakReference),虚引用(PhantomReference) https://blog.csdn.net/tyrroo/article/details/99609500