欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JVM学习(4):垃圾回收

程序员文章站 2022-05-04 14:27:39
进行垃圾回收的区域:堆,方法区 运行时数据区的【堆】和【方法区】在所有线程间是共享的,进行回收 【栈】是线程私有的,所有不进行回收 什么情况下进行回收: 开发中经常有这样的写法 List list = new ArrayList<>(); list.add(); list.add() ......

进行垃圾回收的区域:堆,方法区

运行时数据区的【堆】和【方法区】在所有线程间是共享的,进行回收

【栈】是线程私有的,所有不进行回收

 

什么情况下进行回收:

开发中经常有这样的写法

list<string> list = new arraylist<>();
list.add();
list.add();
list.add();
//业务逻辑代码
return ;

这样是不合理的,list是一个局部变量,使用完毕之后应该赋值为null

 

这段代码,然后使用参数-xx:+printgcdetails -xx:+useserialgc

public class referencecountinggc {
    private static final int mb = 1024 * 1024;
    object instance = null;
    private byte[] size = new byte[2 * mb];

    public static void main(string[] args) {
        referencecountinggc o1 = new referencecountinggc();
        referencecountinggc o2 = new referencecountinggc();
        o1.instance = o2;
        o2.instance = o1;
        o1 = null;
        o2 = null;
        system.gc();
    }
}

打印中有这样一段:[名称:gc前内存占用->gc后内存占用(该区内存总大小)]

[tenured: 0k->643k(349568k), 0.0016527 secs]

 

引用计数器:每当一个对象进行一次引用的时候,计数器加一;每当引用失效的时候,计数器减一;当一个引用计数器等于0的时候,表示这个对象不会再被引用了

o1和o2new完成后是强引用,然后进行互相引用,最后虽然赋值为null了,但是引用计数器不为0,按理来说是不回收的,实际上进行了回收

 

四种引用:

(1)强引用:代码中有明显的new object()这类引用,只要引用还存在,那么就不会进行回收,如果内存不够,抛出oom异常

(2)软引用:当内存够用时,不进行回收;当内存不够用时,进行回收,弱引用的使用如下(java.lang.ref.softreference)

        object object = new object();
        softreference test = new softreference(object);

(3)弱引用:生存到下一次垃圾回收之前,无论内存是否够用,都会回收弱引用关联的对象(java.lang.ref.weakreference)

(4)虚引用:没有实际用途,唯一用途是在对象被垃圾回收前收到一个系统通知(java.lang.ref.phantomreference)

 

可达性分析:

在java中,可作为gc roots的对象包括下面四种:

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象

(2)方法区中类静态属性引用的对象

(3)方法区中常量引用的对象

(4)本地方法栈中jni(native方法)引用的对象,例如下面这句话调用test(person)后的person

private native void test(person person);

 

根据图算法,可以通过对象层层引用找到gc root,是不可回收的,只要和gc root断开,就是可回收的

 

oopmap:

在正式的gc之前,要进行可达性分析来标记出将来可能要宣告死亡的对象

如果每次gc的时候都要遍历所有的引用,这样的工作量是非常大的

因为在可达性分析的时候要保证期间不发生引用关系的变化,所有执行线程要停顿等待,程序中的线程需要停止来配合可达性分析

所以,每次直接遍历整个引用链肯定是不现实的。 为了应对这种尴尬的问题,最早有保守式gc和后来的准确式gc

这里准确式gc就会提到一个oopmap,用来保存类型的映射表

通俗来讲:oopmap就是告诉我们gc的时候可以回收哪些数据

 

safe point:

有了oopmap,hotspot可以快速准确完成gc roots枚举

但是另一个问题来了,我们要在什么地方创建oopmap

程序运行期间,引用的变化在不断发生,如果每一条指令都声称oopmap,那占用空间就太大了,所以有了安全点(safe point)

只在安全点进行gc停顿,只要保证引用变化的记录完成于gc停顿之前就可以

安全点选定太少,gc等待时间就太长,选的太多,gc就过于频繁。

选定原则是”具有让程序长时间执行的特征“,也就是在这个时刻现有的指令是可以复用的。

一般选在方法调用、循环跳转、抛出异常的位置。

通俗来讲:safepoint就是告诉我们在哪一个点进行gc 

 

stw:stop the world的缩写,指正常执行的用户线程全部停止 

 

现在的问题是在safe point让线程们以怎样的机制中断?方案有两种:抢先式中断、主动式中断。

抢先式中断:

gc发生时,中断所有线程,如果发现有线程不再安全点上,就恢复线程让它运行到安全点上。现在几乎不用这种方案。

缺点:thread.sleep();wait()情况无法跑到这个安全点的

主动式中断:

设置一个标志,和安全点重合,再加上创建对象分配内存的地方。各个线程主动轮询这个标志,发现中断标志为真就挂起自己

hotspot使用主动式中断 

 

标记-清除算法

JVM学习(4):垃圾回收

该算法标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

缺陷:

(1)效率不高

(2)标记清除后会产生大量不连续的内存碎片

 

复制算法:

JVM学习(4):垃圾回收

将内存分为大小相等的两块,每次只使用其中一块,内存用完后,就将还活着的对象复制到另一块上,然后把已使用过的内存空间一次清理掉

缺点:内存缩小为原来的一半,利用率过低

 

标记整理算法:

JVM学习(4):垃圾回收

标记过程和标记清除算法一样,但是不直接对可回收对象进行清理,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

 

分代算法:

JVM学习(4):垃圾回收

 

一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法,新生代中每次收集都会发生大批对象死亡,那么选用复制算法

只需要付出少量存活对象的复制成本就可以完成回收;老年代中对象存活率高,没有额外空间对它进行分配担保,于是必须使用标记清理算法

java内存示意图

JVM学习(4):垃圾回收

更先进的算法,由于在实际项目中,对象大都是一出生就死亡,每次垃圾回收对象死亡一大批,经过多次回收最终存活的放入更高级的分代中

生动形象的比喻:类似士兵打仗,每次幸存军衔都会升级,最终存活下来的都是将军

 

分配策略:

(1)大对象直接进入老年代,典型的大对象是长字符串和大数组

使用参数-xx:pretenuresizethreshold可以令大于某个值的对象直接保存在老年代分配,避免在伊甸区和两个幸存区之间发生大量的内存复制

(2)长期存活的对象进入老年代,对象在幸存代熬过一个垃圾回收(minorgc)年龄就会增加一岁,默认十五岁晋升到老年代

(3)如果幸存代中相同年龄所有对象大小总和大于幸存代空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无需十五岁

(4)检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试一次minorgc,如果小于,不允许冒险,这时需要进行一次fullgc

 

垃圾回收器:

(1)serial收集器(-xx:+useserialgc)

单线程的收集器,不仅是使用一个线程完成垃圾收集,而且在它进行垃圾回收时,必须暂停其他所有的工作线程(stw),直到它收集结束

新生代采用复制算法,老年代采用标记整理算法

适用场景:桌面应用(eclipse,burpsuite)

 

(2)serialold收集器

serial的老年代版本

 

(3)parnew收集器(-xx:+userparnewgc)

serial收集器的多线程版本,除了使用多线程进行垃圾收集外,与serial无区别,新生代采用多线程,老年代还是单线程

新生代采用复制算法,老年代采用标记整理算法

适用场景:服务器端首选的新生代收集器

 

(4)parallel scavenge收集器(-xx:+useparallelgc)

新生代收集器,可控的吞吐量

吞吐量:运行用户代码实际/(运行用户代码实际+垃圾收集时间)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,高吞吐量可以高效率地利用cpu时间,尽快完成程序的运算任务

JVM学习(4):垃圾回收

与parnew收集器的区别是:用户可以控制gc时用户线程的停顿时间

适用场景:后台计算,不需要太多交互

 

(5)parallel old收集器(-xx:+useparalleloldgc)

是paralle scavenge收集器的老年代版本,适用多线程和标记整理算法

新生代和老年代都是多线程

 

(6)cms(concurrent mark sweep)收集器(-xx:+useconcmarksweepgc)

cms收集器非常复杂,是一种以获取最短回收停顿时间为目标的收集器,cms是基于标记清除算法实现的,标记包括以下部分:

初始标记:需要stw,对根对象做一个标记

并发标记:从根对象开始逐步标记

重新标记:需要stw,前两步没有找到的对象重新标记,比如(string s=null;s="helloworld!")

常用参数:

-xx:cmsinitiatingoccupancyfraction 用来设置cms空间参数

-xx:+usecmscompactatfullcollection gc执行完后做一次整理操作

 -xx:cmsinitiatingoccupancyfraction=70 -xx:+usecmsinitiatingoccupancyonly这两个操作一般在一起配合:降低cms gc频率或者增加频率、减少gc时长

cms回收的线程默认数:(cpu数量+3)/4

注意:cms收集器只能在老年代用

适用场景:web项目

 

(7)g1收集器

g1收集器与之前的完全不同,算法方面来说,既属于标记整理,也属于复制算法

JVM学习(4):垃圾回收

young gc:

(1)扫描根gc roots

(2)rememberset记录回收对象的数据结构

(3)检测rememberset中那些数据需要从年轻代到老年代

(4)拷贝对象,拷贝到幸存代或者老年代

(5)清理工作

mixed gc:

JVM学习(4):垃圾回收

与cms类似,这里不再细写了

 

g1适用场景:服务器端程序

 

不同收集器配合使用:

横线以上的是新生代,横线以下的是老年代,其中g1不能与其他收集器配合

JVM学习(4):垃圾回收

 JVM学习(4):垃圾回收

 

最后

jdk1.8默认垃圾回收器:parallel scavenge(新生代)+parallel old(老年代)

jdk1.9默认垃圾回收器:g1