垃圾收集
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
判断一个对象是否可被回收
- 引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
}
在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
- 可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
- 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
- finalize()
类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 提供了四种强度不同的引用类型。
- 强引用
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
- 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
- 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
使用 WeakReference 类来创建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
- 虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
使用 PhantomReference 来创建虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
注意:
关于引用的解释:
每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。
在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。
//创建一个引用,引用可以独立存在,并不一定需要与一个对象关联
String s;
通过将这个叫“引用”的标识符指向某个对象,之后便可以通过这个引用来实现操作对象了。
String str = new String("abc");
System.out.println(str.toString());
下面来测试一下4种引用类型:
在运行下面的Java代码之前,需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M。
首先先来测试一下强引用,在限制了 JVM 内存的前提下,下面的代码运行正常。
public class TestOOM {
public static void main(String[] args) {
testStrongReference();
}
private static void testStrongReference() {
// 当 new byte为 1M 时,程序运行正常
byte[] buff = new byte[1024 * 1024 * 1];
}
}
但是如果我们将
byte[] buff = new byte[1024 * 1024 * 1];
替换为创建一个大小为 2M 的字节数组
byte[] buff = new byte[1024 * 1024 * 2];
则内存不够使用,程序直接报错,强引用并不会被回收
接着来看一下软引用会有什么不一样,在下面的示例中连续创建了 10 个大小为 1M 的字节数组,并赋值给了软引用,然后循环遍历将这些对象打印出来。
public class TestOOM {
private static List<Object> list = new ArrayList<>();
public static void main(String[] args) {
testSoftReference();
}
private static void testSoftReference() {
for (int i = 0; i < 10; i++) {
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 = ((SoftReference) list.get(i)).get();
System.out.println(obj);
}
}
}
打印结果:
我们发现无论循环创建多少个软引用对象,打印结果总是只有最后一个对象被保留,其他的obj全都被置空回收了。
这里就说明了在内存不足的情况下,软引用将会被自动回收。
值得注意的一点 , 即使有 byte[] buff 引用指向对象, 且 buff 是一个strong reference, 但是 SoftReference sr 指向的对象仍然被回收了,这是因为Java的编译器发现了在之后的代码中, buff 已经没有被使用了, 所以自动进行了优化。
如果我们将上面示例稍微修改一下:
private static void testSoftReference() {
byte[] buff = null;
for (int i = 0; i < 10; i++) {
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 = ((SoftReference) list.get(i)).get();
System.out.println(obj);
}
System.out.println("buff: " + buff.toString());
}
则 buff 会因为强引用的存在,而无法被垃圾回收,从而抛出OOM的错误。
如果一个对象惟一剩下的引用是软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
我们以与软引用同样的方式来测试一下弱引用。
private static void testWeakReference() {
for (int i = 0; i < 10; i++) {
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 = ((WeakReference) list.get(i)).get();
System.out.println(obj);
}
}
打印结果:
可以发现所有被弱引用关联的对象都被垃圾回收了。
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
垃圾收集算法
- 标记 - 清除
在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。
不足:
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
- 标记 - 整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:
- 不会产生内存碎片
不足:
- 需要移动大量对象,处理效率比较低。
- 复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
- 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
- 新生代使用:复制算法
- 老年代使用:标记 - 清除 或者 标记 - 整理 算法