Java虚拟机(JVM)面试专题 下(初级程序员P6)
Java虚拟机(JVM)面试专题 下(初级程序员P6)
六、四种引用
1. 强引用
普通变量赋值即为强引用,如 A a = new A();
通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收
当之后内存不足的时候,JVM就会发生垃圾回收,如果通过GC Root引用链找到的对象(强引用关系)就不能被回收!
2. 软引用(SoftReference)
例如:SoftReference a = new SoftReference(new A());
如果仅有软引用该对象时,首次垃圾回收不会回收该对象;如果内存仍不足,再次回收时才会释放对象(只是a对象,并不是软引用本身)
软引用自身需要配合引用队列来释放
典型例子是反射数据!我们可以通过反射获取类变量,然后再通过类变量获取成员变量、方法信息,这些数据都是软引用数据!
3. 弱引用(WeakReference)
例如:WeakReference a = new WeakReference(new A());
如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
弱引用自身需要配合引用队列来释放
典型例子是 ThreadLocalMap 中的 Entry 对象!内存不足的时候,它会把 Entry对象的key删除!
软引用和弱引用的回收时机对比
软引用首次GC是不会回收该对象,再次回收才会
弱引用只要发生垃圾回收,就会释放该对象
public class SoftReferenceDemo {
public static void main(String[] args) {
// 强引用
String str = new String("abc");
// 软引用
SoftReference<String> softRef = new SoftReference<String>(str);
str = null; // 去掉强引用
System.gc(); // 垃圾回收器进行回收
System.out.println(softRef.get());//abc
// 强引用
String abc = new String("123");
// 弱引用
WeakReference<String> weakRef = new WeakReference<String>(abc);
abc = null; // 去掉强引用
System.gc(); // 垃圾回收器进行回收
System.out.println(weakRef.get());//null
}
}
运行结果
4. 虚引用(PhantomReference)
例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理。
也就是说,当a对象、b对象被垃圾回收了,虚引用对象A、B就会被放到引用队列里面,由Reference Handler 线程释放该对象和其关联的外部资源!
【BUG-ThreadLoadMap】弱引用的问题
ThreadLoadMap有点特殊,如果我们使用不当,就可能会造成内存泄漏,它里面的key是弱引用,而value是强引用!
我们这里可以采用引用队列的思路来释放,看下面代码
public class WeakReferenceDemo {
public static void main(String[] args) {
MyWeakMap map = new MyWeakMap();
map.put(0, new String("a"), "1");
map.put(1, "b", "2"); //
map.put(2, new String("c"), "3");
map.put(3, new String("d"), "4");
System.out.println(map);
// Entry是强引用,new String("a")是弱引用!
System.gc();
// 垃圾回收后,只有b在!
System.out.println("a:" + map.get("a"));
System.out.println("b:" + map.get("b"));
System.out.println("c:" + map.get("c"));
System.out.println("d:" + map.get("d"));
System.out.println(map);
map.clean();
System.out.println(map);
}
// 模拟 ThreadLocalMap 的内存泄漏问题以及一种解决方法
static class MyWeakMap {
// 引用队列
static ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 这个Entry和ThreadLocalMap类似,key是弱引用,value是强引用!
static class Entry extends WeakReference<String> {
String value;
public Entry(String key, String value) {
// 加入引用队列
super(key, queue); //弱引用
this.value = value;
}
}
// 清除Entry中元素占用内存
public void clean() {
Object ref;
// 从队列里面取
while ((ref = queue.poll()) != null) {
System.out.println(ref);
for (int i = 0; i < table.length; i++) {
if(table[i] == ref) {
table[i] = null;
}
}
}
}
Entry[] table = new Entry[4];
public void put(int index, String key, String value) {
table[index] = new Entry(key, value);
}
// 遍历数组,获取值
public String get(String key) {
for (Entry entry : table) {
if (entry != null) {
String k = entry.get();
if (k != null && k.equals(key)) {
return entry.value;
}
}
}
return null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (Entry entry : table) {
if (entry != null) {
String k = entry.get();
sb.append(k).append(":").append(entry.value).append(",");
}
}
if (sb.length() > 1) {
sb.deleteCharAt(sb.length() - 1);
}
sb.append("]");
return sb.toString();
}
}
}
运行结果
为什么“b”不会被清除?
map.put(0, new String("a"), "1");
map.put(1, "b", "2");
使用new String(" ")创建的对象,在堆里面,当没有别的地方引用它的时候,垃圾回收时,它就会被清除掉!
如果不用new,它就会在字符串常量池中存储一份,这个引用时强引用,所以不会被垃圾回收清除!
小结
前面的弱引用、虚引用可以配合引用队列,实现对垃圾的清理,它们的目的其实都是差不多的,都是为了找到那些被清除的Java对象,然后再释放它们所关联的外部资源!
另外,在JDK9中,提供了Cleaner类,用于清理相关资源!
Java之Cleaner - 时间完全不够用啊 - 博客园 (cnblogs.com)https://www.cnblogs.com/0099-ymsml/p/15861500.html
七、finalize
什么是finalize方法?
它是 Object 中的一个方法,如果子类重写它,垃圾回收时此方法会被调用,可以在其中进行资源释放和清理工作
将资源释放和清理放在 finalize 方法中非常不好,非常影响性能,严重时甚至会引起 OOM,从 Java9 开始就被标注为 @Deprecated,不建议被使用了(建议使用上面讲的Cleaner!)
finalize 原理
(1)对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
(2)当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中
(3)Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
(4)但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
(5)FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开并真正调用 finallize 方法
(6)由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了
finalize 缺点【非常不好】
非常不好
-
无法保证资源释放:FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了
-
无法判断是否发生错误:执行 finalize 方法时,会吞掉任意异常,有的时候甚至不能判断有没有在释放资源的时候发生错误!
影响性能
-
内存释放不及时:重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后,第二次 gc 时才能真正释放内存
-
gc本来就是内存不足所引发的,但是调动finalize又非常慢!要等待,所以非常影响性能!