如何判断对象是否是垃圾
引用计数法
原理
在对象中加入一个计数,表示这个对象被引用的数量,每当一个新的引用指向对象,引用计数+1,一个引用不再指向对象,引用计数-1;当这个引用计数为0的时候,没有引用指向这个对象了,这个对象就是垃圾
缺陷
像这样,如果两个对象互相引用,他们的引用计数都为1,但是这两个对象都不会被使用到,这两个对象实际上都是垃圾,但无法被检测出来
可达性分析算法(JVM采用的垃圾检测算法)
原理
扫描堆中的所有对象,判断每个对象是否能沿着GC Root对象的引用链找到该对象;如果能找到,说明该对象不是垃圾,如果找不到,说明这个对象是垃圾
GC Root对象的组成
- 所有线程的虚拟机栈中本地变量表引用的对象
- 所有线程的本地方法栈中的对象,即Native方法引用的对象
- 方法区中类变量引用的对象
- 方法区常量池中的字面量
不难发现GC Root都是现在正在使用和将来可能会使用的对象,如JVM栈和本地方法栈中引用的对象,正在执行的方法将会使用到这些对象,所以这些对象引用链上的对象都不能释放;方法区中的对象都是和类相关的全局变量,不该被释放
五种引用
强引用
- 引用直接引用对象,最常见的引用就是强引用,强引用是直接引用
- 当某个对象在GC Root的引用链中,被强引用,它在垃圾回收时不会被回收
软引用
- 使用SoftReference间接引用对象
- 当垃圾回收,并且内存不足时,软引用引用的对象会被回收
- 软引用可以和引用队列配合使用,当软引用对象引用的对象,软引用对象会进入引用队列,然后通过遍历队列的方式释放软引用
案例
虚拟机参数:-Xmx10M将堆内存最大值设置为10M
public class Main{
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 创建引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i=0;i<5;i++){
// 将引用队列与软引用进行关联,当软引用引用的对象由于内存不足被回收,软引用会进入引用队列
list.add(new SoftReference<>(new byte[_4MB],queue));
}
System.out.println(list.size());
// 将软引用引用的对象进行回收
Reference<? extends byte[]> poll = queue.poll();
while(poll!=null){
list.remove(poll);
poll = queue.poll();
}
System.out.println(list.size());
}
}
弱引用
- 使用WeakReference间接引用对象
- 当发生垃圾回收,不管是否内存充足,都会将弱引用引用的对象回收
- 弱引用可以和引用队列配合使用,当弱引用对象引用的对象,弱引用对象会进入引用队列,然后通过遍历队列的方式释放软引用
虚引用
- 使用PhantomReference间接引用对象
- 虚引用相当于没有引用,它在任何时候都可能被垃圾回收
- 虚引用必须配合引用队列进行使用,当某个对象被垃圾回收,会提前将它的虚引用加入到关联的引用队列中,然后通过遍历引用队列来对虚引用进行回收
- 虚引用主要用来跟踪垃圾回收的过程,如直接内存的释放,会跟踪ByteBuffer的释放,使用Cleaner(虚引用)在ByteBuffer被释放时,对直接内存调用unsafe.freeMemory方法来进行释放
终结器引用
- 重写Object父类的finalize方法,被当作终结器引用对象;在类加载时JVM会调用Finalizer的register方法将其封装乘一个Finalizer对象,并封装到FinalReference里面,不需要手动的使用FinalReference
- 终结器引用的对象,需要在FinalizeHandler线程调用对象的finalize方法之后才会被释放
- 终结器引用必须配合引用队列使用,在垃圾回收前将终结器引用加入引用队列中,再由FinalizeHandler线程从引用队列中的终结器引用找到目标对象,调用其finalize方法,调用过后这个对象下一次将会被回收
- FinalizeHandler线程优先级很低,所以终结器引用的对象可能迟迟没有调用finalize方法,而迟迟不能释放
垃圾回收算法
标记清除算法
- 利用可达性分析,标记所有需要清除的对象,然后将这些标记过的对象清除(也可以标记可达的对象,然后将未标记的对象清除)
- 清除操作并不是指将内存空间所有字节置为0,只需要将该块内存的起始地址和终止地址存入内存列表中,当新的对象来就到内存列表中查找是否有足够的内存空间,如果有的话,新的对象覆盖原有的数据即可
优点
速度快,但是由于需要整堆扫描对象,随着对象的增多,速度会变慢
缺点
- 容易产生碎片化的空间,出现大量不连续的内存空间
- 下一次对象占用的内存大小只能建立在上一次清除掉的对象基础之上,如果新对象较大,可能堆中剩余容量足够容纳这个对象,但是没有一块完整的内存空间可以存储它
标记整理算法
- 标记阶段和标记清除算法一样,标记出来所有的垃圾
- 整理阶段会将所有存活对象移动到内存空间的一端,然后清理掉边界以外的内存
优点
不会有内存碎片的产生
缺点
存活对象移动过程耗时严重
标记复制算法
将内存空间划分为from和to两个区域,从from标记所有存活的对象,复制到to中,from中内存全部释放;然后交换from和to的位置
优点
速度快,且不会有内存碎片
缺点
需要消耗双倍的内存空间
分代回收理论
- JVM堆空间一般分为两个区域:新生代(Young)、老年代(Old),这两个区域的区别在于:新生代中的绝大多数对象都是朝生夕死的;老年代中的对象大多数都是生命周期较长、难以消亡的
- 新生代分为了两个区域:Eden、Survivor;Survivor又分为了from、to两个部分;在新生代中,采用标记-复制算法
- 老年代的对象都是从新生代经过多次垃圾回收晋升过来、或者由于新生代空间不足直接晋升过来的;在老年代中采用标记-清除和标记-整理算法
垃圾回收过程
-
新创建的对象都会创建到新生代的Eden区
-
当Eden内存空间不足时,就会触发Minor GC,将Eden中存活的对象复制到Survivor_To区域,并将存活对象的寿命+1,然后交换Survivor_From和Survivor_To的位置;接下来再次遇到Eden内存不足除了将Eden区域的存活对象复制到Survivor_To中还会将Survivor_From区的存活对象复制到To中
-
如果某个对象达到了年龄阈值(默认15,也是最大阈值,因为对象头中该数据大小为4bit),或者Survivor_To区域的空间不足时,新生代中的对象将会向老年代中晋升(如果出现大对象也会直接进入老年代,通过-XXPretenureSizeThreshold设置直接进入老年代的大对象大小)
-
当老年代空间不足,会先触发Minor GC,触发之后如果空间仍然不足,会触发Full GC;此时将老年代中的空间进行回收,然后将新生代中的对象晋升
stop the world
垃圾回收过程会触发stop the world,即暂停其他用户线程,因为垃圾回收会改变对象的地址,如果线程仍然在运行会找不到使用的对象,等待垃圾回收结束才恢复;minor GC的耗时短、full GC的耗时长
GC分析案例
VM参数
参数 | 含义 |
---|---|
-XMs | 堆初始大小 |
-Xmx或-XX:MaxHeapSize=size | 堆最大大小 |
-Xmn | 同时配置新生代初始大小和最大大小 |
-XX:NewSize=size | 新生代初始大小 |
-XX:MaxNewSize=size | 新生代最大大小 |
-XX:InitialSurvivorRatio=ratio | 幸存区初始比例 |
-XX:+UseSerialGC | 使用串行垃圾回收器 |
-XX:+UseAdapriveSizePolicy | 打开动态调整幸存区比例开关 |
-XX:SurvivorRatio=ratio | 固定幸存区比例 |
-XX:MaxTenuringThreshold | 晋升阈值 |
-XX:+PrintTenuringDistribution | 打印晋升详情 |
-XX:+PringGCDetails -verbose:gc | 打印GC详情 |
-XX:+ScavengeBeforeFullGC | 在FullGC前进行MinorGC |
使用VM参数:-Xms20M -Xmx20M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc |
案例分析
初始状态
Minor GC
public class Main{
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _4MB = 4 * 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
/**
* -Xms20M -Xmx20M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
* @param args
*/
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
// 添加4M的数据到eden区
list.add(new byte[_4MB]);
}
}
触发Minor GC
Full GC
public class Main{
private static final int _8MB = 8 * 1024 * 1024;
/**
* -Xms20M -Xmx20M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
* @param args
*/
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
// 添加4M的数据到eden区
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}
}
垃圾回收器
- 串行:单线程的垃圾回收器,进行垃圾回收时,必须stop the world,暂停所有用户线程
- 吞吐量优先:在串行的基础上添加了多线程,加快垃圾回收的效率,让单位时间内stop the world占用的时间最短
- 响应时间优先:多线程,尽可能让单次stop the world时间最短
串行垃圾回收器
- 通过-XX:+UseSerialGC=serial + serialOld来指定使用串行垃圾回收器,新生代使用复制算法,老年代使用标记-整理算法
- 因为垃圾回收会导致对象地址的改变,所以为了防止用户线程在垃圾回收时突然访问不到对象,所有用户线程会在一个安全点停下来,然后线程阻塞,等待垃圾回收线程进行垃圾回收,回收结束后,重新开始运行
吞吐量优先垃圾回收器
- 通过-XX:+UseParallelGC或-XX:+UseParallelOldGC使用吞吐量优先的垃圾回收线程
- 用户线程在安全点停止后,将会根据CPU核数启用相应的垃圾回收线程(也可以使用-XX:ParallelGCThreads=n来指定线程数)来回收垃圾,由于存在多个垃圾回收线程,所以stop the world的时间会比较短
响应时间优先垃圾回收器
- 通过-XX:+UseConcMarkSweepGC来使用响应时间优先的垃圾回收器,即CMS,适合大内存场景
- 初始标记时stop the world,仅标记GC Root,非常快;并发标记时不会stop the world,仅仅进行可达性分析去标记可达的对象;重新标记时stop the world,标记并发标记过程中用户线程对对象的改动,时间比并发标记要少得多;最后的清理过程也不需要stop the world,因为使用标记清除算法,不会出现对象地址变化,所以在垃圾回收器执行的同时,用户线程也可以并发执行
- 初始标记的线程数由-XX:ParallelGCThreads=n指定,并发标记过程中的线程数用-XX:ConcGCThreads=n指定(通常并发标记线程数是初始标记线程数的四分之一)