当我们的程序开启运行之后就, 就会在我们的 java 堆中不断的产生新的对象, 而这是需要占用我们的存储空间的, 因为创建一个新的对象需要分配对应的内存空间, 显然我的内存空间是固定有限的, 所以我们需要对没有用的对象进行回收, 本文就来记录下 JVM 中对象的销毁过程.
1. 怎么判断对象是没用的了
引用计数算法
我们在很多场景中会听到 java 对象判断存活的方式是计算该对象的引用计数器是否为 0, 如果为 0 就说明没有其他变量引用该对象了, 这个对象就可以被垃圾收集器回收了. 但事实上 JVM 并不是采用该算法来判断对象是否可以回收的, 比如 objectA.a=objectB 及 objectB.b=objectA 除此之外没有其他引用了. 但是按照引用计数算法是不会回收这两个对象的. 但是这两个对象也已经不能被其他对象访问了, 所以这就是问题.
可达性分析算法
java 中判断对象是否可以回收是通过可达性分析算法来实现的. 如下图:
在上图中 object5,object6 及 object7 这三个对象虽然有相互之间的引用, 但是通过 GC Roots 对象并不能引用到这三个对象, 所以这三个对象是满足回收条件的, 而对象 1 到 4 通过 GC Roots 可达, 所以这几个对象任然存活.
GC Roots 并不是一个对象, 而是一组对象, 在 java 中可以作为 GC Roots 对象的有如下几种:
| 序号 | 类型 |
|--|:--|
| 1| 虚拟机栈 (本地变量表) 中引用的对象 |
| 2| 方法区中类静态属性引用的对象 |
| 3| 方法区中常量引用的对象 |
| 4| 本地方法栈中 JNI(一般说的 Native 方法)引用的对象 |
2. 对象的引用分类
判断对象是否存活我们是通过 GC Roots 的引用可达性来判断的, 但是引用关系并不止一种, 而是有四种分别是: 强引用 (Strong Reference), 软引用(Soft Reference), 弱引用(Weak Reference) 和虚引用(Phantom Reference). 引用强度依次减弱.
强引用
强引用是使用最普遍的引用. 如果一个对象具有强引用, 那垃圾收器绝不会回收它. 当内存空间不足, Java 虚拟机宁愿抛出 OutOfMmoryError 错误, 使程序异常终止, 也不会靠随意回收具有强引用 对象来解决内存不足的问题.
软引用
软引用是用来描述一些还有用但并非必须的对象. 对于软引用关联着的对象, 在系统将要发生内存溢出异常之前, 将会把这些对象列进回收范围进行第二次回收. 如果这次回收还没有足够的内存, 才会抛出内存溢出异常.
- /**
- * 软引用: 缓存场景的使用
- * @author dengp
- *
- */
- public class SoftReferenceTest {
- /**
- * 运行参数 -Xmx200m -XX:+PrintGC
- * @param args
- * @throws InterruptedException
- */
- public static void main(String[] args) throws InterruptedException {
- // 存储 100M 的缓存数据
- byte[] cacheData = new byte[100 * 1024 * 1024];
- // 将缓存数据用软引用持有
- SoftReference<byte[]> cacheRef = new SoftReference<>(cacheData);
- // 将缓存数据的强引用去除
- cacheData = null;
- System.out.println("第一次 GC 前" + cacheData);
- System.out.println("第一次 GC 前" + cacheRef.get());
- // 进行一次 GC 后查看对象的回收情况
- System.gc();
- // 等待 GC
- Thread.sleep(500);
- System.out.println("第一次 GC 后" + cacheData);
- System.out.println("第一次 GC 后" + cacheRef.get());
- // 在分配一个 120M 的对象, 看看缓存对象的回收情况
- // 空间不够
- byte[] newData = new byte[120 * 1024 * 1024];
- System.out.println("分配后" + cacheData);
- System.out.println("分配后" + cacheRef.get());
- }
- }
输出结果
第一次 GC 前 null
第一次 GC 前[B@15db9742
- [GC (System.gc()) 104396K->103072K(175104K), 0.0054505 secs]
- [Full GC (System.gc()) 103072K->102931K(175104K), 0.0095426 secs]
第一次 GC 后 null
第一次 GC 后[B@15db9742
- [GC (Allocation Failure) 103597K->102995K(175104K), 0.0099572 secs]
- [GC (Allocation Failure) 102995K->102963K(175104K), 0.0044781 secs]
- [Full GC (Allocation Failure) 102963K->102931K(175104K), 0.0226699 secs]
- [GC (Allocation Failure) 102931K->102931K(199680K), 0.0022288 secs]
- [Full GC (Allocation Failure) 102931K->519K(131072K), 0.0226120 secs]
分配后 null
分配后 null
从上面的示例中就能看出, 软引用关联的对象不会被 GC 回收. JVM 在分配空间时, 若果 Heap 空间不足, 就会进行相应的 GC, 但是这次 GC 并不会收集软引用关联的对象, 但是在 JVM 发现就算进行了一次回收后还是不足(Allocation Failure),JVM 会尝试第二次 GC, 回收软引用关联的对象.
像这种如果内存充足, GC 时就保留, 内存不够, GC 再来收集的功能很适合用在 == 缓存 == 的引用场景中. 在使用缓存时有一个原则, 如果缓存中有就从缓存获取, 如果没有就从数据库中获取, 缓存的存在是为了加快计算速度, 如果因为缓存导致了内存不足进而整个程序崩溃, 那就得不偿失了.
弱引用
弱引用也是用来描述非必须对象的, 他的强度比软引用更弱一些, 被弱引用关联的对象, 在垃圾回收时, 如果这个对象只被弱引用关联(没有任何强引用关联他), 那么这个对象就会被回收
- /**
- * 弱引用
- * @author dengp
- *
- */
- public class WeakReferenceTest {
- /**
- * 运行参数 -Xmx200m -XX:+PrintGC
- * @param args
- * @throws InterruptedException
- */
- public static void main(String[] args) throws InterruptedException {
- // 存储 100M 的缓存数据
- byte[] cacheData = new byte[100 * 1024 * 1024];
- // 将缓存数据用软引用持有
- WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
- // 将缓存数据的强引用去除
- cacheData = null;
- System.out.println("第一次 GC 前" + cacheData);
- System.out.println("第一次 GC 前" + cacheRef.get());
- // 进行一次 GC 后查看对象的回收情况
- System.gc();
- // 等待 GC
- Thread.sleep(500);
- System.out.println("第一次 GC 后" + cacheData);
- System.out.println("第一次 GC 后" + cacheRef.get());
- // 在分配一个 120M 的对象, 看看缓存对象的回收情况
- byte[] newData = new byte[120 * 1024 * 1024];
- System.out.println("分配后" + cacheData);
- System.out.println("分配后" + cacheRef.get());
- }
- }
输出结果
第一次 GC 前 null
第一次 GC 前[B@15db9742
- [GC (System.gc()) 104396K->103072K(175104K), 0.0013337 secs]
- [Full GC (System.gc()) 103072K->531K(175104K), 0.0070222 secs]
第一次 GC 后 null
第一次 GC 后 null
分配后 null
分配后 null
弱引用直接被回收掉了. 那么弱引用的作用是什么? 或者使用场景是什么呢?
- static Map<Object,Object> container = new HashMap<>();
- public static void putToContainer(Object key,Object value){
- container.put(key,value);
- }
- public static void main(String[] args) {
- // 某个类中有这样一段代码
- Object key = new Object();
- Object value = new Object();
- putToContainer(key,value);
- //..........
- /**
- * 若干调用层次后程序员发现这个 key 指向的对象没有用了,
- * 为了节省内存打算把这个对象抛弃, 然而下面这个方式真的能把对象回收掉吗?
- * 由于 container 对象中包含了这个对象的引用, 所以这个对象不能按照程序员的意向进行回收.
- * 并且由于在程序中的任何部分没有再出现这个键, 所以, 这个键 / 值 对无法从映射中删除.
- * 很可能会造成内存泄漏.
- */
- key = null;
- }
在《Java 核心技术卷 1》这本书中对此做了说明
设计 WeakHashMap 类是为了解决一个有趣的问题. 如果有一个值, 对应的键已经不再 使用了, 将会出现什么情况呢? 假定对某个键的最后一次引用已经消亡, 不再有任何途径引 用这个值的对象了. 但是, 由于在程序中的任何部分没有再出现这个键, 所以, 这个键 / 值 对无法从映射中删除. 为什么垃圾回收器不能够删除它呢? 难道删除无用的对象不是垃圾回 收器的工作吗?
遗憾的是, 事情没有这样简单. 垃圾回收器跟踪活动的对象. 只要映射对象是活动的, 其中的所有桶也是活动的, 它们不能被回收. 因此, 需要由程序负责从长期存活的映射表中 删除那些无用的值. 或者使用 WeakHashMap 完成这件事情. 当对键的唯一引用来自散列条目时, 这一数据结构将与垃圾回收器协同工作一起删除键 / 值对.
下面是这种机制的内部运行情况. WeakHashMap 使用弱引用(weak references) 保存键. WeakReference 对象将引用保存到另外一个对象中, 在这里, 就是散列键. 对于这种类型的 对象, 垃圾回收器用一种特有的方式进行处理. 通常, 如果垃圾回收器发现某个特定的对象 已经没有他人引用了, 就将其回收. 然而, 如果某个对象只能由 WeakReference 引用, 垃圾 回收器仍然回收它, 但要将引用这个对象的弱引用放人队列中. WeakHashMap 将周期性地检 查队列, 以便找出新添加的弱引用. 一个弱引用进人队列意味着这个键不再被他人使用, 并 且已经被收集起来. 于是, WeakHashMap 将删除对应的条目.
除了 WeakHashMap 使用了弱引用, ThreadLocal 类中也是用了弱引用, 可以自行了解下.
虚引用
一个对象是否有虚引用的存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来获取一个对象的实例. 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
3.finalize 方法
当一个对象在堆内存中运行时, 根据它被引用变量所引用的状态, 可以把它所处的状态
可达状态: 当一个对象被创建后, 若有一个以上的引用变量引用它, 则这个对象在程序中处于可达状态.
可恢复状态: 如果程序中某个对象不再有任何引用变量引用它, 它就进入了可恢复状态. 此时, 系统的垃圾回收机制准备回收该对象所占用的内存, 在回收该对象之前, 系统会调用所有可恢复状态对象的 ==finalize==()方法进行资源清理. 如果系统在调用 finalize()方法时重新让一个引用变量引用该对象, 则这个对象会再次变成可达状态; 否则该对象将进入不可达状态.
不可达状态: 当对象与所有引用变量的关联都被切断, 且系统已经调用所有对象的 finalize()方法后依然没有使该对象变成可达状态, 那么这个对象将永久性地失去引用, 最后变成不可达状态. 只有当一个对象处于不可达状态时, 系统才会真正回收该对象所占有的资源.
所以 finalize 方法只对象存活的最后一次机会, 而且只会执行一次.
销毁一个对象过程归纳如下:
4. 方法区的回收
很多人认为方法区 (或者 HotSpot 虚拟机中的永久代) 是没有垃圾收集的, Java 虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集, 而且在方法区进行垃圾收集的 "性价比" 一般比较低: 在堆中, 尤其是在新生代中, 常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间, 而永久代的垃圾收集效率远低于此.
永久代的垃圾收集主要回收两部分内容: 废弃常量和无用的类. 回收废弃常量与回收 Java 堆中的对象非常类似. 以常量池中字面量的回收为例, 假如一个字符串 "abc" 已经进入了常量池中, 但是当前系统没有任何一个 String 对象是叫做 "abc" 的, 换句话说是没有任何 String 对象引用常量池中的 "abc" 常量, 也没有其他地方引用了这个字面量, 如果在这时候发生内存回收, 而且必要的话, 这个 "abc" 常量就会被系统 "请" 出常量池. 常量池中的其他类(接口), 方法, 字段的符号引用也与此类似.
判定一个常量是否是 "废弃常量" 比较简单, 而要判定一个类是否是 "无用的类" 的条件则相对苛刻许多. 类需要同时满足下面 3 个条件才能算是 "无用的类":
该类所有的实例都已经被回收, 也就是 Java 堆中不存在该类的任何实例.
加载该类的 ClassLoader 已经被回收.
该类对应的 java.lang.Class 对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法.
虚拟机可以对满足上述 3 个条件的无用类进行回收, 这里说的仅仅是 "可以", 而不是和对象一样, 不使用了就必然会回收. 是否对类进行回收, HotSpot 虚拟机提供了 - Xnoclassgc 参数进行控制, 还可以使用 - verbose:class 及 - XX:+TraceClassLoading, -XX:+TraceClassUnLoading 查看类的加载和卸载信息.
在大量使用反射, 动态代理, CGLib 等 bytecode 框架的场景, 以及动态生成 JSP 和 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载的功能, 以保证永久代不会溢出.
参考《深入理解 Java 虚拟机》
来源: https://www.cnblogs.com/dengpengbo/p/10448412.html