- 引用的含义:Object o = new Object();(o就是一个引用),一个引用变量指向一个new出来的对象,就叫做引用
- java中的引用分4种,平常中用的引用为强引用
- 各种不同的引用在垃圾回收中的不同表现(多线程与jvm有交叉)
- 垃圾回收时会调用finalize方法,java语言中new了一个对象的时候是不需要手动回收的,而C/C++是需要程序员手动回收这个对象的
- jvm中的垃圾回收线程gc会在自动回收对象的时候调用finalized方法
- 实际上finalized方法永远都不需要重写(几乎没有重写的场合),而且也不应该去重写他、不应该去调用他
- 面试造火箭—->观察结果用的
M类代码
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;
public class M {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
}
}
强引用
- VarHandler本身就是个强引用
- 强引用是默认的引用并且是最经常使用的引用
- 只要有一个强引用指向这个对象,垃圾回收是一定不会回收这个对象的—->普通类型的引用(强引用)
- 因为有强引用指向他,所以不会回收他;只有没有引用指向他的时候,才会被回收
- 用System.gc();显式地调用一下垃圾回收,看能不能回收m
- 调用System.gc();之后要阻塞住当前线程—->System.in.read();
- 因为gc是跑在别的线程中的后台线程(守护线程),假如没有非守护线程(普通线程、用户线程、主线程、本地线程ThreadLocal?)在执行,那么后台线程也会自动退出——>gc跑在别的线程中,main线程(主线程)直接退出了整个程序就退出了,那么gc也就没有什么意义了,所以需要阻塞住当前线程(调用阻塞方法System.in.read();)
- System.in.read();没有任何含义,只是阻塞当前线程的意思—->让当前线程不会停止
- System.gc();是full gc
强引用的垃圾回收
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;
import java.io.IOException;
public class T01_NormalReference {
public static void main(String[] args) throws IOException {
M m = new M();
m = null;
System.gc(); //DisableExplicitGC
System.in.read();
}
}
软引用
- 如何在内存中体现一个软引用
- 栈内存中有一引用m指向堆内存中的一个软引用对象,软引用里面又有一个对象指向了字节数组(这个引用才是软引用)
- m.get可以拿到这个字节数组,会输出hashcode值;如果被回收了会打印null值
- 软引用的含义(概念):当一个对象被一个软引用所指向的时候,只有系统内存不够用的时候才会回收他;内存够用的时候不会回收他(这里的内存指的应该是堆内存,而不是真正的系统内存)
- 运行下列程序的时候设置一个运行时的参数-Xms20m -Xmx20m—->限制程序运行时的堆内存为20m(最大最小均为20m)
- 运行结果:第一次和第二次能够打印出来,第三次就被回收了
- 原因:堆内存总共20m,第一次分配了10m可以分配下来,虽然调用了gc,但是内存够用,所以不会被回收;第二次分配了15次,这时的堆内存不够用了,会进行gc清理,这时就会将软引用给干掉空出足够的空间,15m内存这时才会被分配进去
- 内存不够了才会把你干掉
- System.gc();是full gc
软引用的垃圾回收
/**
* 软引用
* 软引用是用来描述一些还有用但并非必须的对象。
* 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。
* 如果这次回收还没有足够的内存,才会抛出内存溢出异常。
* -Xmx20M
*/
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;
import java.lang.ref.SoftReference;
public class T02_SoftReference {
public static void main(String[] args) {
SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
//m = null;
System.out.println(m.get());
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
//再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用干掉
byte[] b = new byte[1024*1024*15];
System.out.println(m.get());
}
}
//软引用非常适合缓存使用
运行结果
用途
- 做缓存用,用作缓存
- 从内存中读出一个特别大的图片,读完之后就没什么用了,可以放在内存中缓存在那里,要用的时候直接从内存中拿,但是这个大图片占用的空间比较大(内存效率低下),如果不用了的话,别人也能利用到这块空间,这时就可以把图片干掉了,把这块空间空出来——>这时应该使用软引用
- 从数据库中读一大堆的数据出来,对于一个按一下“back”就能再次访问到这些数据(不用再从数据库中拿)的业务逻辑,也可以使用软引用。这样第一次读到的数据放在内存中,只要有足够大的内存就一直放在那里,不会回收,那么再次去拿的时候就不会去数据库中拿了,效率就会高很多(没有足够的内存同样会被回收,需要再次去数据库中拿)
- 需要新的空间,可以讲软引用指向的对象给干掉,下次从数据库中取就行了;但新空间还够用的时候,下次就不用从数据库中取,而直接从内存中拿,做缓存用
- 一般我们用不上,我们做缓存也是直接调redis;底层细节(抠细节的优化)的优化可能会用到这个
🌟弱引用(面试最有可能被问到、最容易出问题的、同样答好可以给面试官留下深刻印象的)
- ThreadLocal中用到了软引用WeakReference(是什么?)
- jvm中的内容,提前讲也没问题
- 不了解jmm或者jvm原理的时候,理解起多线程也会比较困难
- 弱引用的含义(概念):只要遭遇到gc就会回收,二话不说直接干掉
- 弱引用的作用在于:如果有一个强引用指向他的时候,只要强引用消失掉,他就应该去被回收,就不用管了,一般用在容器中
- 一般用在容器中!!!
- WeakHashMap—->弱引用的HashMap(ThreadLocal的那个ThreadLocalMap就相当于是WeakHashMap)
- ThreadLocal
- 复习AQS的unlock的源码
- 弱引用的应用:ThreadLocal
弱引用垃圾回收代码
/**
* 弱引用遭到gc就会回收
*
*/
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;
import java.lang.ref.WeakReference;
public class T03_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<>();
tl.set(new M());
tl.remove();
}
}
弱引用的应用:ThreadLocal
- 一般画法如下所示:
- 实际执行的操作:
- 详细调用执行过程:
- 内存泄漏和内存溢出是两个概念(内存泄漏不一定导致内存溢出—->当分配的内存足够大的时候有少量的内存泄漏是不会导致内存溢出的,除非不断地出现内存泄漏,最终还是会出现OOM)
- ThreadLocalMap中的key使用弱引用的原因:为了防止内存泄漏!!!详见6(疑问:有了remove,为什么还要使用弱引用???)
- 对上图ThreadLocal结构的详细说明:
- 有一个线程(运行着的),其中有ThreadLocal对象tl和ThreadLocalMap对象threadLocals
- tl指向堆中ThreadLocal对象,往ThreadLocal中扔对象其实是往当前线程中的threadLocals变量指向的Map中存入一个“以ThreadLocal对象为key、以要存入的M对象为value”的Entry===>这里的Entry继承了WeakReference>,是一个ThreadLocal类型的弱引用,其中还有一个Object类型的value对象
- 所以ThreadLocalMap通过一个弱引用指向了ThreadLocal对象
- 为什么要用弱引用?
- 如果是一个强引用,当tl不再指向ThreadLocal对象的时候(tl是一个局部变量,方法一结束就消失了),ThreadLocal依然被一个强引用指向,这个ThreadLocal就不能被回收了
- 又因为有些线程是长期存在的(服务器线程7*24、365天不间断运行),所以threadLocals这个map会长期存在(tl不一定会长期存在,只要tl所在的方法结束或者不再用到tl的时候tl就不再存在了===>tl一般只是作为局部变量使用,但当他作为线程的成员变量的时候依然会长期存在),那么在key是强引用的前提下,key所指向的ThreadLocal对象是不会被回收的,是永远不会被回收的,因此就发生了内存泄漏
- 但是当key变为弱引用就不会存在这样的问题了,当tl这个强引用消失的时候,key又是弱引用,因此在下一次内存回收的时候会自动讲ThreadLocal对象回收(弱引用就会消失)
- 当key指向的ThreadLocal对象被回收了之后,key指向了空值null,并且此时map是不会消失的,是永远存在的;这时就变成了键值是空值null,而value值指向了那个M类的对象;这时这条记录(key为空的记录)就不能访问到了,如果map这样下去越攒越长,还是会有value所指向M类对象的内存泄漏
- 所以,使用ThreadLocal时,里面的对象不用了,务必要调用ThreadLocal的remove方法将其remove掉,不然还会有内存泄漏(ThreadLocal抠在这里,不用了一定要用remove方法remove掉)
- map中的Entry中的value还会有引用,不会被自动回收,因为这个map还是存在的;map中有Entry,Entry中有value,value还是指向那个M类的对象的
- 养成一个好习惯:ThreadLocal用完了,二话不说就调用他的remove方法将他remove掉
- 用完之后一定要手动remove掉
虚引用
- 一般用在对堆外内存的管理上,就干这一件事,管理堆外内存
- 创建一个虚引用必须有两个参数,第二个参数必须是一个队列
- 普通的程序员基本不用,是给写jvm或者jdk源码的程序员使用的
- List用来占据内存,让内存溢出-Xms20M -Xmx20M
- 生产环境中最大最小一般设置成一样的
- utf-8编码问题
- 内存结构:
- 通过一个虚引用指向new出来的M
- 有一个关联的的队列queue
- 一旦虚引用被回收,这个虚引用会被装到这个队列中去,让程序员接受到一个通知;什么时候检测到队列中有一个引用存在就说明这个虚引用被回收了,给你个通知,这个虚引用被回收了放到队列中去
- 虚引用指向的是特别虚的引用,垃圾回收肯定一启动看到就会将其干掉(有强引用的时候会不会被干掉)
- 通知的方式是向队列中扔一个值
- 还有一点,在虚引用中,无论怎么get这个值、这个对象,他输出一定都是空值null;永远get不到,即便其中有值,而弱引用是可以get到的
- 拿不到那虚引用就没用了吗?并不是这样的,虚引用只是为了再对象被垃圾回收器回收掉的时候给你个通知而已
- 被回收了就放到队列中,是写jvm或者一些底层源码的人使用的
- 有值被回收的时候会做出相应的处理:
- NIO中的新的buffer—->DirectByteBuffer直接内存
- 直接内存是不被虚拟机直接管理的内存,是被操作系统管理的,也叫堆外内存
- 不在堆中,垃圾回收器无法正确回收这块内存中的对象
- 自己写netty等一些要用到堆外内存的时候,在OS中直接分配内存,可以用虚引用进行回收,当检测到虚引用被垃圾回收器回收的时候,可以做出相应的处理去回收堆外内存(用在这儿)
- 不让用户自己去回收堆外内存
- 一般程序员get这个虚引用指向的对象的时候为null,但是写jvm的程序员还是有办法拿到这个对象的,用其他方法
- 回去写开源软件去
- 堆外内存如何回收?
- C/C++中用free、delete
- Java中堆外内存的直接回收—->Unsafe(jdk底层用的多)
- 其中有很多CAS的方法
- 可以直接访问内存并复制该内存
- 也可以分配和回收内存
- Unsafe在jdk1.8中可以通过反射来用,但jdk1.9之后他被加入了包里,现在普通人是用不了的,但是在juc的底层的一些compareAndSet方法中都用到了Unsafe类
- 虚引用引用到了什么不重要,在jvm中主要指向的是堆外内存,实际上是可以指向任何对象(但是指向的东西get不到,所以实际中除了特殊需求很少用到虚引用,get不到什么用都没有!)
虚引用代码举例
/**
*
*
* 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,
* 也无法通过虚引用来获取一个对象的实例。
* 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
* 虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象,
* 那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null,
* 弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。
*
* jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,
* 而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),
* 所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,
* 会在堆内存分配一个对象保存这个堆外内存的引用,
* 这个对象被垃圾收集器管理,一旦这个对象被回收,
* 相应的用户线程会收到通知并对直接内存进行清理工作。
*
* 事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,
* DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。
*
*/
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;
public class T04_PhantomReference {
// List用来占据内存,让内存溢出-Xms20M -Xmx20M
// 生产环境中最大最小一般设置成一样的
private static final List<Object> LIST = new LinkedList<>();
private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();
public static void main(String[] args) {
PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
new Thread(() -> {
while (true) {
// 扔1M
LIST.add(new byte[1024 * 1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
System.out.println(phantomReference.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();
}
}
}