1.java中的引用类型有几种?
NormalReference
SoftReference
WeakReference
PhantomReference(phantom:虚幻,鬼,幻象,幽灵)
2.每种引用类型的特点
NormalReference:没有引用指向的时候,gc根据算法回收 SoftReference:内存不够时会被回收 WeakReference:gc看到就回收 PhantomReference:gc看到就回收
3.每种引用类型的应用场景是什么?
NormalReference:使用最普遍的引用,一个对象具有强引用,不会 被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError 错误,使程序异常终止,也不回收这种对象。 SoftReference: WeakReference:用来管理内存泄漏问题 PhantomReference:用来管理堆外内存
4.具体实例演示
NormalReference:
public class T1_NormalReference {
public static void main(String[] args) throws IOException {
M m = new M();
m = null;
System.gc();
//gc()并不是在main线程中回收,这个操作时阻止main线程提前结束
System.in.read();
}
}
这里添加System.in.read();是为了组织主线程提前结束。
SoftReference:
package com.kali.jvm.reference;
import java.lang.ref.SoftReference;
//弱引用 看内存够不够用
public class T2_SoftReference {
public static void main(String[] args) {
SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
//get()方法取的是软引用中引用对象的地址,如果被GC回收,return:null
System.out.println(m.get());
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
/*
分配一个15M大小的数组,heap装不下,先回收一次,如果不够,会把软引用回收
需要设置VM参数-Xmx20M,堆内存大小,非常适合
*/
byte[] b = new byte[1024*1024*12];//return null
System.out.println(m.get());
}
}
WeakReference:
package com.kali.jvm.reference;
import java.lang.ref.WeakReference;
/**
* 弱引用只要遇到gc就会被回收
* 应用在内存泄露
*/
public class T3_WeakReference {
public static void main(String[] args) {
WeakReference<M> m = new WeakReference<>(new M());
System.out.println(m.get());
System.gc();
System.out.println(m.get());
ThreadLocal<M> tl = new ThreadLocal<>();
//set将对象存入该线程的map中,ThreadLocal作为key
//如果将tl设置为null,那么map中的key就为null,那么对应的value就不会回收
tl.set(new M());
/*务必调用,使用完ThreadLocal后删除entry,防止内存泄漏,因为threadlocals
的key是弱引用指向ThreadLocal的,一旦ThreadLocal的强引用消失,key为null
造成内存对象不能回收*/
tl.remove();
}
}
注意:关于内存泄漏以ThreadLocal 为例,用来测试一个线程设置在ThreadLocal中的对象,另一个线程是否可以拿到。思考一下,能否拿到呢?
解释一下ThreadLocal,ThreadLocal是用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景下。是用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。那么解释完了ThreadLocal后,上面的demo为什么会出现内存泄漏呢?
解答:由于Thread中有一个map类型的数据结构ThreadLocalMap用来存储该线程独有的数据的,这个map的key是弱引用指向的是ThreadLocal对象,value就是想作为独享的对象,因此ThreadLocalMap与Thread的生命周期是一样长。源码是这样做的:
public class ThreadLocal<T> {
//....
1 public void set(T value) {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null)
5 map.set(this, value);
6 else
7 createMap(t, value);
8 }
//....
}
这里的第3行ThreadLocalMap map = getMap(t);拿的是当前线程的map,这个map是Thread类中的一个属性
ThreadLocal.ThreadLocalMap threadLocals = null;
在调用getMap(t)时,拿的就是当前现成的map,拿到之后,执行map.set(this,value),this指的是ThreadLocal,value指的是想独享的对象。以上操作之后,线程独有的数据已经设置完成,既然是独享,另一个线程当然是无法拿到刚才set进去的对象的。
那么ThreadLocal为什么会产生内存泄漏?
因为上文已经提到map中的key是弱引用,gc看到就会回收。一旦外部指向ThreadLocal(以下简称tl)的引用消失,gc就会回收key这个弱引用指向的tl。意外情况就是外部ThreadLocal的引用被改变或者被设置为null了,那么key也就成了null,在map结构中key一旦成了null,对应的value就查找不到,更何况删除,就会导致内存泄漏。所以经常在调用tl的set()之后要调用它的remove()方法。
内存泄漏:其实说白了就是该内存空间使用完毕之后无法回收,即所谓内存泄漏。
内存溢出:其实说白了就是该内存空间使用完毕之后未来得及回收,导致内存不够。
PhantomReference:
package com.kali.jvm.reference;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;
/**
* 虚引用传参时需要一个队列,因为通过get方法是永远拿不到虚引用指向的对象的
* 所以,当jvm回收时会给这个队列发一个消息
* 用来管理堆外内存
* 新版的jvm支持(NIO)DirectByteBuffer jvm直接管理堆外内存,是操作原来操作系统
* 管理的内存只要有队列理由消息,那么jvm的DirectByteBuffer就会将这个内存回收,
* 效率提升
*
* 设置-Xmx10M
*/
public class T4_PhantomReference {
//List用于占据内存空间,用于测试虚引用
private static final List<Object> List = new LinkedList<>();
private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();
public static void main(String[] args) {
PhantomReference<M> pr = new PhantomReference<>(new M(),QUEUE);
new Thread(() -> {
while(true){
List.add(new byte[1024*1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
//永远都拿不到
System.out.println(pr.get());
}
}).start();
new Thread(() -> {
while(true){
//一直监测有没有一个虚引用被回收
Reference<? extends M> poll = QUEUE.poll();
if(poll != null){
System.out.println("---虚引用对象被JVM回收---" + poll);
}
}
}).start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
虚引用:是jvm用来管理堆外内存的。操作系统的内存空间中运行着虚拟机,虚拟机有独有的空间,但始终还是在内存中,所以虚拟机JVM之外的空间称为堆外内存。为什么java想直接管理堆外内存呢?传统意义上,I/O中传输的数据是由操作系统将I/O设备上的数据先读入内存,然后拷贝到JVM,然后jvm再处理。这样一来效率就低了,不太恰当的例子:自己能直接吃饭,偏要让人来喂,是比较花费时间的。所以java在NIO中加入了管理堆外内存的API:DirectByteBuffer类。
有人会问这有什么用?快啊!那么这些虚引用指向的对象或者说数据怎么回收呢?在T4_PhantomReference这个demo中有一个QUEUE队列,这是用来存储消息的。这个消息是当这个虚引用指向的堆外空间没有被使用时就会发送一个消息,存入这个队列,jvm通过检测这个队列查看哪个对象没有被使用了,然后JVM就把它回收。