当我们的程序开启运行之后就,就会在我们的java堆中不断的产生新的对象,而这是需要占用我们的存储空间的,因为创建一个新的对象需要分配对应的内存空间,显然我的内存空间是固定有限的,所以我们需要对没有用的对象进行回收,本文就来记录下JVM中对象的销毁过程。
我们在很多场景中会听到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方法)引用的对象 |
判断对象是否存活我们是通过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类中也是用了弱引用,可以自行了解下。
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态
可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态。
可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。此时,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变成可达状态;否则该对象将进入不可达状态。
不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
所以finalize方法是对象存活的最后一次机会,而且只会执行一次。可以将可恢复状态转变为可达状态。
很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看类的加载和卸载信息。
在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
参考《深入理解Java虚拟机》