0.前言:
1.强引用:当对象被一个或一个以上的引用类型变量引用时,此对象处于可达状态不会被回收。(当没有引用变量引用此对象时,对象就处于不可达状态。 就非常非常可能会被回收)
2.软引用:软引用需要用到SoftReference类,内存不够时,可能会被回收。
(通常用于内存敏感的程序中) 对于不是必须一直存在于内存中的对象可以使用软引用,比如处理一张图片的过程,如果更重要的程序需要在处理过程中途运行,可以将这张图片使用软引用。 (应用于缓存数据<硬盘中还有一份儿, 内存不够时可以清理这个缓存中的数据,因为硬盘中还有一份儿数据>)
(中途的程序在内存中存不下时,就删除这个软引用代表的对象)
3.弱引用:弱引用需要用到WeakReference类,无论内存够不够,只要垃圾回收器启动,弱引用关联的对象肯定被回收。用来描述非必需对象。(和软引用很像,但比软引用级别更低)。 常见的应用是ThreadLocal保存数据时key采用弱引用。
再比如: java集合中有一个WeakHashMap专门用来存储弱引用的(map中存储弱引用,弱引用指向一个对象)
WeakHashMap<String, Object> whm=new WeakHashMap<String, Object>();
4.虚引用:PhantomReference: 虚引用需要用到PhantomReference类, 用来在对象被回收时接收一个系统通知。
如果一个对象具有虚引用,那么它和没有任何引用一样,
被虚引用关联的对象引用通过get方法获取到的永远为null,
也就是说这种对象在任何时候都有可能被垃圾回收器回收,
通过这种方式关联的对象也无法调用对象中的方法。
虚引用主要是用来管理堆外内存的,通过ReferenceQueue这个类实现,当一个对象被回收的时候,会向这个引用队列里面添加相关数据,给一个通知。
设置虚引用的唯一目的,就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理。
1.强引用:
最常见的一种对象引用方式: Test1 myObject = new Test1();
/**
*
* @author zhaoYQ
*
*
*/
public class Test1 {
@Test
public void strongReference() throws InterruptedException {
Test1 myObject = new Test1();
System.out.println("Gc前:" + myObject);
System.gc();
TimeUnit.SECONDS.sleep(1);
//强引用: 未置空无法被GC
System.out.println("未置空Gc后打印:" + myObject);
myObject = null; //置空可被GC
System.gc();
System.runFinalization();//强制调用finalize
TimeUnit.SECONDS.sleep(1);
System.out.println("置空后Gc后打印:" + myObject);//置空可被GC
}
protected void finalize() throws Throwable {
System.out.println("对象被回收");
super.finalize();
}
}
2.软引用:
应用于缓存。
package com;
import java.lang.ref.SoftReference;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
/**
*
* @author zhaoYQ
* 软引用
*
*/
public class Test1 {
@Test
public void strongReference() throws InterruptedException {
byte[] t1 = new byte[1024*1024*10];//10M
SoftReference<byte[]> softReference = new SoftReference<byte[]>(t1);
//注意: 强引用之外的引用经常把上边两行写为一行,如下:
System.out.println("Gc前,打印对象:" + softReference.get());
SoftReference<byte[]> softReference =
new SoftReference<byte[]>(new byte[1024*1024*10]);
t1=null;
System.gc();//将t1的引用置空//只存在软引用(不存在强引用)
TimeUnit.SECONDS.sleep(1);
System.out.println("Gc后,打印对象:" + softReference.get());
//再次创建一个数组,堆中存不下的时候,垃圾回收器工作
//先回收一次,如果第一次回收后内存还是不够
//则再清理第二次,这一次会把软引用对象清除
byte[] t2 = new byte[1024*1024*15];//15M
System.gc();//启动GC(但是GC不一定会回收某个对象<要看当时堆内存环境>)
TimeUnit.SECONDS.sleep(1);
System.out.println("Gc后,打印对象:" + softReference.get());
}
}
运行时设置堆内存大小(设置运行参数): -Xmx100M 当堆最大内存设置为100M时因为内存够用,所以出发GC时,对象不会被回收。
如下,当当堆最大内存设置为25M时因为内存不够用, 所以当需要往堆中再次分配一个更大的t2时,就要回收t1了(所以软引用是当内存不够时才可能回收<内存空间够用时不会回收>)
3.弱引用:
package com;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
/*
System.gc(); 等效于: Runtime.getRuntime().gc()
//告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行收集是不确定的
System.runFinalization(); 等效于: Runtime.getRuntime().runFinalization()
//强制调用已经失去引用的对象的finalize方法
*/
public class Test1 {
@Test
public void strongReference() throws InterruptedException {
// 1.创建一个对象
Test1 t1 = new Test1();// 强引用,未置空无法被GC
// 2.创建一个弱引用,让弱引用引用对象t1
WeakReference<Test1> wr = new WeakReference<Test1>(t1);
// 3.切断t1和对象之间的引用
t1 = null;
// 4.取出弱引用所指代的对象
System.out.println("gc前打印对象:"+wr.get());//打印对象
// 5.催促垃圾回收
System.gc();
TimeUnit.SECONDS.sleep(2);
System.runFinalization();//强制调用finalize
// 6.再次取出弱引用所指向的对象
System.out.println("gc后打印对象:"+wr.get());//null
}
protected void finalize() throws Throwable {
System.out.println("对象被回收");
super.finalize();
}
}
执行结果如下:
gc前打印对象:com.Test1@fbb2c1
对象被回收
gc后打印对象:null
弱引用说明案例2:
package com.zyq.csmall.business.config;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
public class Test {
// 运行结果:
// weakRef = com.zyq.csmall.business.config.Test$1M@19f7e29
// M对象被回收
// weakRef = null
public static void main(String[] args) throws InterruptedException {
class M{
protected void finalize() throws Throwable {
System.out.println("M对象被回收");
}
}
WeakReference weakRef=new WeakReference(new M());
System.out.println("weakRef = " + weakRef.get());
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("weakRef = " + weakRef.get());
}
}
4.虚引用:
虚引用需要用到PhantomReference类, 用来在对象被回收时接收一个系统通知。
如果一个对象具有虚引用,那么它和没有任何引用一样,
被虚引用关联的对象引用通过get方法获取到的永远为null,
也就是说这种对象在任何时候都有可能被垃圾回收器回收,
通过这种方式关联的对象也无法调用对象中的方法。
虚引用主要是用来管理堆外内存的,通过ReferenceQueue这个类实现,当一个对象被回收的时候,会向这个引用队列里面添加相关数据,给一个通知。
设置虚引用的唯一目的,就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理。
下边的情况下: 当堆中的对象被回收时,用来通知垃圾回收器将堆外的这个对象同时回收掉。
虚引用这这种情景下: 就可以在堆中的对象被回收时通知 垃圾回收器去回收堆外的这个字节缓冲数组 (因为堆中使用字节缓冲数组的对象已经要被回收了, 堆外的这个字节缓冲数组就没有存在的必要了)
//ByteBuffer.allocate(1024)可以在堆外创建一个字节数组用来:
//在nio通信中直接接收网卡的数据(如果不在堆外创建此数组,则需要
//在堆中创建一个类似的数组来拷贝操作系统接收网卡后在堆外构建的
//一个字节数组的数据) (用ByteBuffer.allocate(1024)可以在堆外
//创建一个字节数组用来直接接收网卡的数据<尤其在游戏编程中经常使用>)
ByteBuffer b=ByteBuffer.allocate(1024);
//-verbose:gc -XX:+PrintGCDetails -XX:+DisableExplicitGC -XX:MaxDirectMemorySize=40M
// 禁止手动调用System.GC() 堆外最大内存是40M
案例:
某个对象被虚引用所指代(虚引用构造中必须还得包含一个队列用于存储虚引用指代的对象被销毁时同时存入此虚引用的信息)
虚引用指向一个对象时,通过这个引用不能获取这个对象的信息,也不能调用此对象的方法(虚引用有点儿像一个不存在的引用)
/*
运行结果:
null
null
null
虚引用对象被JVM回收java.lang.ref.PhantomReference@146d875
null
分析:
//因为: 虚拟机的信息会占用堆的一部分空间,所以第4次向LIST集合中
//放入数组时堆空间不够用了。 所以就回收了虚引用代表的对象
//(相当于List占用了大概3M空间,虚拟机占用了17M空间)
//(虚拟机堆最大内存设置为20M)
//(栈中就会被放入虚引用的数据,就可以从队首取出元素)
*/
//vm arguments/vm options 设置为: -Xms20m(最小堆大小) -Xmx20m(最大堆大小20M)
public class MyObject {
//这个给List中不停存入数组的情况体现不出虚引用,这个案例作者的用意没体会出来
private static final List<Object> LIST=new LinkedList<>();
private static final ReferenceQueue<MyObject> QUEUE=new ReferenceQueue<>();
public static void main(String[] args) {
//虚引用使用的时候里边必须给引用的对象对应一个QUEUE的队列
//用来在对象被回收之前(注意是回收之前),将把此对象对应的虚引用
//添加到它关联的引用队列QUEUE中(用来告知虚拟机此对象关联的堆外对象也应该被回收掉)
PhantomReference<MyObject> phantomRef=new PhantomReference<>(new MyObject(),QUEUE);
Thread t1=new Thread(()->{
while(true){
try {
//往LIST集合每次存入一个大小为1M的数组(LIST集合会越来越大)
//当设置的虚拟机堆内存空间已经不够用时就会回收虚引用代表的对象
//(此时会给引用队列中放入回收对象的虚引用信息)
//因为: 虚拟机的信息会占用堆的一部分空间,所以第4次向LIST集合中
//放入数组时堆空间不够用了。 所以就回收了虚引用代表的对象
//(相当于List占用了大概3M空间,虚拟机占用了17M空间)
//(虚拟机堆最大内存设置为20M)
LIST.add(new byte[1*1024*1024]);//1M
TimeUnit.SECONDS.sleep(1);
//注意: 虚引用和没有引用一样(使用虚引用无法获取所引用的对象)
System.out.println(phantomRef.get());//null
} catch (Exception e) {
e.printStackTrace();
}
}
});
t1.start();
Thread t2=new Thread(()->{
while(true){
//System.out.println("t2Thread-run");
//往LIST集合每次存入一个大小为1M的数组(LIST集合会越来越大)
Reference<? extends MyObject> pollRef =QUEUE.poll();//从队首删除元素
//因为: 虚拟机的信息会占用堆的一部分空间,所以第4次向LIST集合中
//放入数组时堆空间不够用了。 所以就回收了虚引用代表的对象
//(相当于List占用了大概3M空间,虚拟机占用了17M空间)
//(虚拟机堆最大内存设置为20M)
//(栈中就会被放入虚引用的数据,就可以从队首取出元素)
if(pollRef != null){
System.out.println("虚引用对象被JVM回收" + pollRef);
}
}
});
t2.start();
}
}
案例2: 逻辑上上边案例基本相同
/**
null
t2Thread-run
t2Thread-run
null
t2Thread-run
t2Thread-run
t2Thread-run
null
Exception in thread "t1" java.lang.OutOfMemoryError: Java heap space
at com.Test.lambda$0(Test.java:44)
at com.Test$$Lambda$1/21405346.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
t2Thread-run
*/
public class MyObject{
public static void main(String[] args) {
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue();
//引用队列
PhantomReference<MyObject> phantomReference =
new PhantomReference<>(new MyObject(),referenceQueue);
System.out.println(phantomReference.get());
//获取不到对象(因为虚引用相当于没有引用)
List<byte[]> list = new ArrayList<>();//存储byte数组的ArrayList集合
new Thread(() -> {//线程1给ArrayList集合不断放入大小为1M的byte数组
while (true)
{
list.add(new byte[3 * 1024 * 1024]);//3M
try { TimeUnit.MILLISECONDS.sleep(600); }
catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(phantomReference.get());
}
},"t1").start();
new Thread(() -> {
while (true)
{ System.out.println("t2Thread-run");
Reference<? extends MyObject> reference =
referenceQueue.poll();//从队首删除元素
try { TimeUnit.MILLISECONDS.sleep(300); }
catch (InterruptedException e) { e.printStackTrace(); }
if (reference != null) {
System.out.println(
"****有虚引用对象被回收了,队列中有被回收的信息了");
}
}
},"t2").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(5); }
catch (InterruptedException e) { e.printStackTrace(); }
}
}
5.面试题扩展:
你使用过ThreadLocal吗, ThreadLocal是否有可能会导致内存泄漏? (弱引用案例)
答案: A.ThreadLocal是某个线程给ThreadLocal中存入数据时,这个数据只能给这个线程用
(其他线程无法获取此数据) //放入的数据时引用类型的(对象)
B.ThreadLocal的数据使用完一定要调用ThreadLocal对象的remove方法将放入的数据
置为null, 这样垃圾回收器才会回收这个数据对象
案例扩展: ThreadLocal的使用方式如下:
package com.zyq.csmall.business.config;
import java.util.concurrent.TimeUnit;
public class TestTL {
/**
* ThreadLocal中有一个Map集合,可以给里边放入元素
* (特点是: 哪个线程给ThreadLocal放元素,哪个线程才能获取这个数值
* <其他线程获取不到>)
* 观察源码:ThreadLocal的set(T value)方法可以发现它从当前线程中会获取一个
* Map,然后给里边放入元素:
* Thread t = Thread.currentThread();
* ThreadLocalMap map = getMap(t);
*
* getMap(Thread t) { return t.threadLocals; }
* if (map != null) map.set(this, value);
* //当前线程中有一个ThreadLocalMap(就是一个Map),把当前ThreadLocal
* 的引用(ThreadLocal本身)作为key,数值作为value放入到ThreadLocalMap
* //所以如果Map中需要放入多个value,就得有多个key(多个ThreadLocal)
*
* 注意:
* 观察ThreadLocal的set(T value)方法中的map.set(this, value)源码,
* 发现:
* ThreadLocalMap是ThreadLocal的静态内部类
* 这个ThreadLocalMap的set方法放元素逻辑是:
* // map.set(this, value);
* ThreadLocalMap中封装了一个Entry数组,Entry键值对类继承了弱引用
* WeakReference类
* (键值对类在创建对象时,会先创建父类WeakReference对象:
* super(k)父类对象会用key指向)
* super(k)(所以ThreadLocalMap的每个元素的
* key<ThreadLocal对象还会被一个弱引用指向ThreadLocalMap对象)
* (ThreadLocal对象会被tL强引用指向, 还会被一个弱引用
* WeakReference指向)
*
*/
public static void main(String[] args) {
ThreadLocal<Integer> tL=new ThreadLocal<>();
//ThreadLocal<Integer> tl2=new ThreadLocal<>();
Thread th1=new Thread(){
public void run() {
tL.set(1);
//tl2.set(2);
System.out.println("Integer:"+tL.get());
//System.out.println("Integer:"+tl2.get());
//不用ThreadLocal之后一定要调用tL.remove()将Entry的value置为null
//(弱引用解决key, tL.remove()解决value)
//(弱引用指向的ThreadLocal对象在垃圾回收时会被回收掉, tL.remove会将
// Entry的value置为null<将value指向的对象设置为垃圾对象, GC会回收此对象>)
//否则会导致th1指向线程对象,线程对象指向ThreadLocalMap threadLocals
// threadLocals指向ThreadLoalMap对象,ThreadLoalMap对象指向Entry数组,
tL.remove();
}
};
th1.start();
Thread th2=new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("Integer:"+tL.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
th2.start();
}
}
内存泄漏图示说明:
6.线程池中的ThreadLocal:
扩展线程池的TheadLocal的remove()问题:
同样的 ,在线程池中使用ThreadLocal存储数据时,每次将线程使用完也要将此线程的ThreadLocalMap清空,否则下一条线程可能会获取上一条线程的数据,也会导致内存泄漏。
zhaoYQ 2022-08-22