Java对象回收
垃圾收集器在对堆进行回收前,第一件事就是要确定这些对象之中哪些还“存活”,哪些已经“死去”。
1引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器+1,当引用失效时,计数器-1。任何时刻计数器为0的对象就是不可能再被使用的。
1.1 算法缺陷
对象之间相互循环引用的问题。
public class CrossReference {
private static class A {
B reference = null;
}
private static class B {
A reference = null;
}
public static void main(String[] args) {
A a = new A();
B b = new B();
a.reference = b;
b.reference = a;
// a 和 b 引用设置为空后,按照引用计数法,A 与 B 的实例的引用也都不为0,为1
a = null;
b = null;
}
}
2 可达性分析
当前主流的商用程序语言(Java、C#等)的内存管理子系统都是通过可达性分析算法来判定对象是否存活的。
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走的路径称为“引用链”,如果某个对象到GC Root间没有任何引用链相连,则证明此对象是不可能再被使用的。
图 可达性分析判断对象是否可回收
2.1 固定作为GC Roots的对象
1)在虚拟机栈中引用的对象,比如各线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2)在方法区中类静态属性引用的对象。
3)在方法区中常量引用的对象,比如字符串常量池里的引用。
4)在本地方法栈中JNI(Native方法)引用的对象。
5)Java虚拟机内部的引用。
6)被同步锁(synchronized关键字)持有的对象。
7)反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除了这些固定的集合外,根据用户所选用的垃圾收集器以及当前回收区域的不同,还可以有其他对象“临时性”地加入。
3 引用类型
类型 | 说明 |
强引用 Strongly Reference | 指程序中普遍存在的引用赋值。任何情况下,只有该引用关系存在,垃圾收集器就永远不会回收掉被引用的对象。 |
软引用 Soft Reference | 描述一些还有用,但非必须的对象。只要软引用关联着对象,在系统将要发送内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没足够内存,才会抛出内存溢出异常。 |
弱引用 Weak Reference | 描述那些非必须对象,比软引用引用更弱些。被弱引用关联的对象只能生存到下一次垃圾收集发生为止。 |
虚引用 Phantom Reference | 是最弱的引用关系,不会对其生成时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。 |
表 Java四种引用类型,强度依次减弱
3.1 软引用场景
软引用的非必须属性,很适合用来做缓存。如果该对象被回收,我们可以在业务线程上抛出自定义的异常或者记录到日志。
public class SoftReferenceService {
private static SoftReference<List<Object>> cacheList;
// 该方法会在Service最开始执行
public void setResultCacheList() {
//从数据库中读取大量的数据放入到resultCacheList作为缓存
List<Object> objects = new ArrayList<>();
objects.add("数据1");
cacheList = new SoftReference<>(objects);
}
public List<Object> readDate() {
List<Object> result = null;
// 先从缓存中找数据,如果没有则抛出错误
if (cacheList != null) {
List<Object> objectList = cacheList.get();
if (objectList == null) {
System.out.println("没有数据了");// 记录该缓存以被回收
result = new ArrayList<>(); 没有缓存则获取新的数据
} else result = objectList;
} else result = new ArrayList<>();
return result;
}
}
3.2 弱引用使用场景
通过Map存储键值对,当某个key不需要时(这个key被GC回收时),应该把这个条目从map中移除,否则会导致内存泄漏。WeakHashMap(非线程安全)封装好了这个,以下是对其源码进行分析:
图 WeakHashMap底层数据结构Entry的部分源码
其Entry并没有直接存储key,而是调用了WeakReference(T,ReferenceQueue)的构造函数。该构造函数的作用是创建一个WeakReference,并且设置其回收时放入的引用队列(即该引用被回收时,会被放入引用队列中)。
图 WeakHashMap类中的expungeStaleEntries函数
该函数主要作用是清除过期元素(弱引用)作为key的Entry的value。
图 expungeStaleEntries函数被使用的场景
在获取size、resize(扩容)及getTable时会调用该函数。
WeakHashMap 能自动清除无强引用时的key。value会在相应的map操作中删除。
3.3 虚引用使用场景
如果GC在某个时间点确定虚引用的所指对象只有虚引用可达,届时或稍后将其加入引用队列。虚引用点引用对象不会被释放直到所有指向该对象的虚引用被清除。所以虚引用通常用来跟踪对象被垃圾回收的活动:
public class PhantomReferenceNotice { // vm配置: -Xms1m -Xmx1m
private static ReferenceQueue<PhantomReferenceNotice> queue = null;
// 监控引用队列的线程
static class QueueMonitorThread extends Thread {
@Override
public void run() {
while (true) {
if (queue != null) {
Reference<? extends PhantomReferenceNotice> remove = queue.poll();
if (remove != null) {
System.out.println("追踪对象垃圾回收活动(对象GC前执行):" + remove + "该实例对象被GC了");
}
}
}
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("对象执行了finalize()");
}
public static void main(String[] args) throws InterruptedException {
queue = new ReferenceQueue<>();
QueueMonitorThread thread = new QueueMonitorThread();
thread.setDaemon(true);
thread.start();
PhantomReferenceNotice instance = new PhantomReferenceNotice();
PhantomReference<PhantomReferenceNotice> reference = new PhantomReference<>(instance,queue);
System.out.println("reference.get():" + reference.get());
System.out.println("第一次gc");
System.gc();
TimeUnit.SECONDS.sleep(2);
instance = null;
System.out.println("第二次gc");
System.gc();
String tempStr = ""; //为了占满堆内存,让JVM好执行垃圾回收
for (int i = 0; i < 10000; i++) {
tempStr += System.currentTimeMillis() + "";
}
TimeUnit.SECONDS.sleep(2);
System.out.println("main线程执行完毕,instance = null");
}
}
/*
运行结果:
reference.get():null
第一次gc
第二次gc
对象执行了finalize()
追踪对象垃圾回收活动(对象GC前执行):java.lang.ref.PhantomReference@30535167该实例对象被GC了
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at day02.article.PhantomReferenceNotice.main(PhantomReferenceNotice.java:49)
Process finished with exit code 1
*/
4 对象回收判定
即使在可达性分析算法中判定为不可达对象,也不是“非死不可”,这时它们暂时处于“缓行”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:1)进行可达性分析后发现没有与GC Roots相连接的引用链,则第一次标记;2)随后进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法(加入对象没有覆盖finalize()方法,或者该方法已被虚拟机调用过,则没必要执行)。将判定为“有必要执行finalize()方法” 的对象放置于F- Queue队列中,并且稍后由Finalizer线程去执行finalize()方法。finalize()方法是逃脱死亡的最后一次机会。稍后收集器将对F- Queue中的对象进行第二次小规模标记。
图 对象的二次标记
public class SecondLabel {
private static SecondLabel instance = null;
@Override
protected void finalize() throws Throwable {
System.out.println("SecondLabel的对象执行了finalize()方法");
instance = this;
}
public static void main(String[] args) throws InterruptedException {
instance = new SecondLabel();
instance = null;
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("第一次gc()");
System.out.println("instance:" + instance);
instance = null;
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("第二次gc()");
System.out.println("instance:" + instance);
}
}
/* 运行结果:
SecondLabel的对象执行了finalize()方法
第一次gc()
instance:day02.article.SecondLabel@1540e19d
第二次gc()
instance:null
Process finished with exit code 0
*/
不鼓励通过这个方法拯救对象,可以用try-finally或者其他方式替代。
5 回收方法区
方法区垃圾收集的“性价比”通常比较低。
废弃的常量和不再使用的类型。判断一个常量是否“废弃”与回收Java堆中的对象非常类似。而判断“不再使用的类型”的条件就比较苛刻,需要满足以下三个条件:
1)该类所有的实例都已经被回收。
2)加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替代类加载器的场景,如OSGi、JSP的重加载等,否则通常很难达成的。
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法。