文章目录
- 垃圾回收类型
- 手动垃圾回收:C/C++的内存管理
- 自动垃圾回收:Java的内存管理
- 自动垃圾回收应用场景
- 不同垃圾回收对比
- 线程不共享部分的回收
- 方法区的回收
- 手动触发回收
- 堆回收
- 两种判断方法
- 引用计数法
- 查看垃圾回收日志
- 可达性分析法
- GC Root对象类型
- 可达性算法案例分析
- 查看GC Root
- 五种对象引用
- 强引用
- 软引用
- 执行过程
- SoftReference对象的回收
- 软引用编程案例
- 软引用使用场景-缓存
- 弱引用
- 虚引用(不常用)
- 终结器引用(不常用)
垃圾回收类型
手动垃圾回收:C/C++的内存管理
- 在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收。
- 内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。
自动垃圾回收:Java的内存管理
- Java中为简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。很多语言比如C#、Python、Go都拥有垃圾回收器。
自动垃圾回收应用场景
- 解决系统僵死的问题:大厂的系统出现的许多系统僵死问题都与频繁的垃圾回收有关
- 性能优化:对垃圾回收器进行合理的设置可以有效提升程序的执行性能
不同垃圾回收对比
类型 | 方式 | 优点 | 缺点 |
---|---|---|---|
自动垃圾回收 | 自动根据对象是否使用由虚拟机来回收对象 | 降低程序员实现难度、降低对象回收bug的可能性 | 程序员无法控制内存回收的及时性 |
手动垃圾回收 | 由程序员编程实现对象的删除 | 回收及时性高,由程序员把控回收的时机 | 编写不当容易出现悬空指针、重复释放、内存泄漏等问题 |
线程不共享部分的回收
- Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区。
- 线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存。
- 类的生命周期
方法区的回收
- 方法区中能回收的内容主要就是不再使用的类。判定一个类可以被卸载。需要同时满足下面三个条件:
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象
Class<?> clazz = loader.loadClass("com.itheima.my.A"); Object o = clazz.newInstance(); o=null;
- 加载该类的类加载器已经被回收
URLClassLoader loader = new URLClassLoader(new URL[]{new URL("file:D:\\lib\\")}); loader=null;
- 该类对应的 java.lang.Class 对象没有在任何地方被引用
Class<?> clazz = loader.loadClass("com.itheima.my.A"); clazz=null;
手动触发回收
- 如果需要手动触发垃圾回收,可以调用
System.gc()
方法。 - 注意事项:调用
System.gc()
方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断。 - 注意:开发中此类场景一般很少出现,主要在如 OSGi、JSP 的热部署等应用场景中。每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件
堆回收
- Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。
- 图中A的实例对象要回收,有两个引用要去除:
- 栈中a1变量到对象的引用
- B对象到A对象的引用
- 如果在main方法中最后执行 a1 = null ,b1 = null,可以回收对象,方法中已经没有办法使用引用去访问A和B对象。
两种判断方法
- 常见的有两种判断方法:引用计数法和可达性分析法
引用计数法
- 引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
缺点
- 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
- 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
查看垃圾回收日志
- 查看垃圾回收日志,可以使用-verbose:gc参数
可达性分析法
- Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。
- 可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收。
GC Root对象类型
-
线程Thread对象,引用线程栈帧中的方法参数、局部变量等
-
系统类加载器加载的
java.lang.Class
对象。
-
监视器对象,用来保存同步锁
synchronized
关键字持有的对象。
-
本地方法调用时使用的全局对象
可达性算法案例分析
- 下面代码中的A实例对象和B示例对象,通过可达性算法判断对象能被回收的
查看GC Root
- 通过arthas和eclipse Memory Analyzer (MAT) 工具可以查看GC Root,MAT工具是eclipse推出的Java堆内存检测工具
- 注意:需要使用JDK17+,如果使用JDK8请更新JDK
- 具体操作步骤如下:
- 使用arthas的heapdump命令将堆内存快照保存到本地磁盘中。
- 使用MAT工具打开堆内存快照文件。
- 选择GC Roots功能查看所有的GC Root。
五种对象引用
- 可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收
- 完整的对象引用方式为:强引用、软引用、弱引用、虚引用、终结器引用
强引用
- 强引用(Strong Reference):是最常见的引用类型,也是默认的引用类型。当一个对象具有强引用时,即使系统内存紧张,垃圾回收器也不会回收该对象。
- 只有当该对象没有任何强引用指向时,才会被回收。
Object obj = new Object(); // 强引用
软引用
- 软引用是一种相对弱化的引用类型,用于描述还有用但非必需的对象。如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。可以使用SoftReference类来创建软引用
SoftReference<Object> softRef = new SoftReference<>(new Object()); // 软引用
- 在JDK 1.2版之后提供了
SoftReference
类来实现软引用,软引用常用于缓存中
执行过程
- 软引用的执行过程如下:
- 将对象使用软引用包装起来,
new SoftReference<对象类型>(对象)
。 - 内存不足时,虚拟机尝试进行垃圾回收。
- 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
- 如果依然内存不足,抛出
OutOfMemory
异常
SoftReference对象的回收
- 软引用中的对象如果在内存不足时回收,
SoftReference
对象本身也需要被回收。如何知道哪些SoftReference
对象需要回收呢? - SoftReference提供了一套队列机制:
- 软引用创建时,通过构造器传入引用队列(软引用对象已被回收而SoftReference对象还未回收的SoftReference对象队列,用于跟踪被垃圾回收器回收的软引用对象)
- 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
- 通过代码遍历引用队列,将
SoftReference
的强引用删除
软引用编程案例
- 创建软引用:使用Caffeine中的软引用
public static void main(String[] args) {Cache<Object, Object> build = Caffeine.newBuilder().softValues().build();}
- 基本使用:再运行配置中设置VM选项
-Xmx200m
(设置堆的大小为200M)public static void main(String[] args) throws IOException {byte[] bytes = new byte[1024 * 1024 * 100];SoftReference<byte[]> softReference = new SoftReference<>(bytes);bytes = null;System.out.println(softReference.get());byte[] bytes2 = new byte[1024 * 1024 * 100];System.out.println(softReference.get());}// 运行结果:由于vm启动还要占用一部分的堆空间,如果byte释放,才可以放下byte2,// byte释放后,softReference对象也会释放,所以返回为null// [B@58ceff1// null
- 引用队列使用:设置堆大小VM参数
-Xmx900m
是,count=8
,否则堆空间足够的情况下,不触发软引用对象的回收import java.io.IOException; import java.lang.ref.SoftReference; import java.lang.ref.ReferenceQueue; import java.util.ArrayList;public class SoftReferenceExample {public static void main(String[] args) throws IOException {// 创建一个列表来存储软引用ArrayList<SoftReference> softReferences = new ArrayList<>();// 创建一个引用队列,用于跟踪被垃圾回收器回收的软引用对象ReferenceQueue<byte[]> queues = new ReferenceQueue<>();// 循环创建10个大对象,并为每个对象创建一个软引用for (int i = 0; i < 10; i++) {// 创建一个100MB的字节数组byte[] bytes = new byte[1024 * 1024 * 100];// 创建一个软引用,关联字节数组和引用队列SoftReference<byte[]> studentRef = new SoftReference<>(bytes, queues);// 将软引用添加到列表中softReferences.add(studentRef);}// 创建一个软引用变量,用于从引用队列中取出被回收的软引用SoftReference<byte[]> ref = null;// 用于计数被垃圾回收器回收的软引用数量int count = 0;// 循环,从引用队列中取出软引用直到没有更多的软引用while ((ref = (SoftReference<byte[]>) queues.poll()) != null) {// 对回收的软引用计数count++;}// 输出被回收的软引用数量System.out.println(count);} }
- 使用ArrayList来存储软引用(SoftReference),软引用允许其引用的对象在内存不足时被垃圾回收器回收。
- 使用
ReferenceQueue
来跟踪垃圾回收器回收的对象。当软引用所引用的对象被回收时,软引用对象本身会被添加到ReferenceQueue
中。 - 在for循环中创建了10个大的字节数组(每个大约100MB)并为每个数组创建了一个SoftReference,同时将该软引用关联到了之前创建的引用队列queues。
- 在while循环中,使用poll()方法从ReferenceQueue中非阻塞地移除并返回被垃圾回收器回收的软引用对象。如果队列为空,则返回null。
- 通过计数器count记录被回收的软引用数量,并在循环结束后打印出来。
- 注意:这段代码仅仅创建了引用,并没有显式地触发垃圾回收,所以程序运行结束时输出的count可能是0。软引用的回收通常发生在系统内存不足时,垃圾回收器决定回收一些软可达对象来释放内存。
软引用使用场景-缓存
- 使用软引用实现学生数据的缓存
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;/*** 软引用案例 - 学生信息的缓存*/
public class StudentCache {private static final StudentCache cache = new StudentCache();private final Map<Integer, StudentRef> StudentRefs;// 用于Cache内容的存储private final ReferenceQueue<Student> q;// 垃圾Reference的队列// key:当前学生id// 构建一个缓存器实例(类的构造方法)private StudentCache() {StudentRefs = new HashMap<>();q = new ReferenceQueue<>();}// 取得缓存器实例public static StudentCache getInstance() {return cache;}public static void main(String[] args) {for (int i = 0; ; i++) {StudentCache.getInstance().cacheStudent(new Student(i, String.valueOf(i)));}}// 以软引用的方式对一个Student对象的实例进行引用并保存该引用private void cacheStudent(Student em) {cleanCache();// 清除垃圾引用StudentRef ref = new StudentRef(em, q);StudentRefs.put(em.getId(), ref);System.out.println(StudentRefs.size());}// 清除那些软引用Student对象已经被回收的StudentRef对象private void cleanCache() {StudentRef ref;// q中存放被回收数据的软引用对象while ((ref = (StudentRef) q.poll()) != null) {StudentRefs.remove(ref._key);}}// 继承SoftReference,使得每一个实例都具有可识别的标识。// 并且该标识与其在HashMap内的key相同private class StudentRef extends SoftReference<Student> {private Integer _key;public StudentRef(Student em, ReferenceQueue<Student> q) {super(em, q);_key = em.getId();}}
}class Student {int id;String name;public Student(int id, String name) {this.id = id;this.name = name;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}
弱引用
- 弱引用(Weak Reference):弱引用是一种更弱化的引用类型,用于描述非必需的对象。当垃圾回收器运行时,不管内存是否充足,都会回收被弱引用指向的对象。可以使用WeakReference类来创建弱引用
- 在JDK 1.2版之后提供
WeakReference
类来实现弱引用,弱引用主要在ThreadLocal
中使用。 - 弱引用对象本身也可以使用引用队列进行回收
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
- 案例
public static void main(String[] args) throws IOException {byte[] bytes = new byte[1024 * 1024 * 100];WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes);bytes = null;System.out.println(weakReference.get()); //[B@58ceff1System.gc(); //通知Gc回收对象System.out.println(weakReference.get()); //null
}
虚引用(不常用)
- 虚引用(Phantom Reference):虚引用是最弱化的引用类型,主要用于监控对象被垃圾回收的状态。
- 虚引用无法通过引用直接获取对象,需要通过引用队列(ReferenceQueue)来监听对象的回收状态。可以使用
PhantomReference
类来创建虚引用。例如:ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue); // 虚引用
终结器引用(不常用)
-
终结器引用是一种特殊的引用类型,用于与对象的终结器(Finalizer)相关联。在Java中,每个对象都可以有一个终结器方法,该方法在对象被垃圾回收之前被调用。终结器引用可以用来保留具有终结器的对象,以便在垃圾回收器运行时调用其终结器方法。
-
终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做
/*** 终结器引用案例 仅用于学习*/ public class FinalizeReferenceDemo {public static FinalizeReferenceDemo reference = null;public void alive() {System.out.println("当前对象还存活");}@Overrideprotected void finalize() throws Throwable {try{System.out.println("finalize()执行了...");//设置强引用自救reference = this;}finally {super.finalize();}}public static void main(String[] args) throws Throwable {reference = new FinalizeReferenceDemo();test();//Java虚拟机对任何给定对象调用finalize方法的次数永远不会超过一次test();}private static void test() throws InterruptedException {reference = null;//回收对象System.gc();//执行finalize方法的优先级比较低,休眠500ms等待一下Thread.sleep(500);if (reference != null) {reference.alive();} else {System.out.println("对象已被回收");}} }
-
运行结果:只能救活一次,原因:Java虚拟机对任何给定对象调用finalize方法的次数永远不会超过一次
当前对象还存活 对象已被回收
-
注意:终结器引用已被认为是不安全和不推荐使用的,因为终结器的执行是不可预测的,并且可能导致性能问题和资源泄漏。
-
在Java 9之后,终结器引用已被废弃,推荐使用其他方式来进行资源释放和清理操作,如使用try-with-resources语句块或实现AutoCloseable接口。
-
在实际开发中,建议尽量避免使用终结器引用,并采用更可靠和可控的方式来处理对象的资源释放和清理操作。