垃圾收集器与内存分配策略

为什么学习GC

当需要排查各种内存溢出,内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,要对自动化的GC做检测和调节

关注的是堆的垃圾回收机制

程序计数器,虚拟机栈,本地方法区都随线程生随线程灭,他们的内存回收和分配都具有确定性,但是堆中一个接口的多个实现类所需要的内存不同,只有程序运行期间才知道创建了什么对象

判断对象是否已死

  1. 引用计数法
    对象如果被其他变量所引用,就让他的计数+1,失效-1,等于0的时候就没有人引用他
    缺陷:循环引用
public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
        object1.object = object2;
        object2.object = object1;
        object1 = null;
        object2 = null;
    }
}
class MyObject{
    public Object object = null;
}

这里的object1 和 object2虽然都为null了但是因为互相引用了对方没所以无法被回收
在这里插入图片描述
2. 可达性分析算法(Java采用)
扫描堆中的对象,看是否能沿着GC Root对象为起点的引用链找到该对象,找不到表示可以被回收
GC Root:
① 虚拟机栈(栈桢中的本地变量表)中的引用的对象,也就是活动线程中的当前方法里面的变量引用
也就是我们在程序中正常创建一个对象 MyObject object1 = new MyObject(); ,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的

② 本地方法栈中Native方法所引用的对象

③系统类,核心类引用的对象,如Objetct,HashMap等

④ 被加锁的对象

⑤ 我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。

⑥ 常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。

在这里插入图片描述

四种引用

可达性算法、Java引用 详解
Java四种引用包括强引用,软引用,弱引用,虚引用。
在可达性分析算法里面,判断对象是否可达,与引用有关
在这里插入图片描述

  1. 强引用:再程序代码普遍存在的,类似Object obj = new Object() 这类的引用
    特点:只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象

  2. 软引用:用来描述一些还有用但非必须对象,软引用对象是通过软引用来进一步引用的对象,使用SoftReference来实现软引用

SoftReference<String> sr = new SoftReference<String>(new String("hello"));
System.out.println(sr.get());

这里的rf就是对hello的一个软引用

特点:当内存不足的时候,发生一次垃圾回收,垃圾回收后如果内存还不够,则会回收掉软引用对象(没有强引用引用他)

软引用案例分析:
在这里插入图片描述
这里面的byte[]数组就是强引用,在实际中,这种情况可能对应着我们要往byte[]中存图片,并把图片放到list集合中进行显示,但是这些图片资源不是我们的核心业务资源,如果都用强引用来进行引用,则无法被清理,而占用过多内存导致内存溢出,对于这样不重要的资源我们想在内存紧张的时候把他释放掉,需要的时候再重新读取进来,可以使用软引用
在这里插入图片描述
这里list强引用了软引用对象ref,ref软引用了byte[]
但是上面的软引用引用的对象被清理后,软引用为空,但没被清除,可以使用引用队列清除
在这里插入图片描述

  1. 弱引用:描述非必须对象,强度比软引用还要弱,弱引用对象是通过弱引用来进一步引用的对象,使用WeakReferrence来实现
    特点:垃圾回收时候无论内存够不够,都会被回收(没有强引用引用他)

引用队列:软引用对象(弱引用对象)被回收掉以后,软引用(弱引用)自身会被放到引用队列里面,因为他们自身也是对象,也会占用一定的内存,通过遍历引用队列来释放

  1. 虚引用:虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

垃圾回收算法

  1. 标记清除
    在这里插入图片描述
    这里的清除没有被GC Root引用的对象并不是说把他的每个字节都清零,而是把其内存开始,结束位放在一个列表,再分配的时候直接复写

优点:效率高
缺点:会产生内存碎片,不会对垃圾回收后产生的空闲空间进行合并,对于数组等需要连续内存空间的对象可能出现虽然总的内存空间够,但单个的块内存空间不够而放不进去数组的情况

  1. 标记整理
    与标记清除的区别在于第二步的整理,会把可用的对象向前移动,让内存更紧凑,避免了内存碎片
    在这里插入图片描述
    优点:没有内存碎片

缺点:因为可用对象的移动,对应变量的引用地址需要进行改变,这一步需要耗费时间,所以效率低

  1. 复制算法
    把内存区域划分为From和To的两个大小相等的两块区域,To区域始终空闲,垃圾回收时把From中存活的对象复制到To区域中,在复制的过程就完成了整理,完成后交换From和To的位置
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    优点:不会产生碎片

缺点:需要占用双倍的内存空间
对于对象存活率低的用才好,但是效率高于标记整理(因为要被标记的变少了??)

  1. 分代的垃圾回收机制
    在虚拟机里面的垃圾回收时前三种算法的协调合作,也就是分代的垃圾回收机制,把堆内存划分为新生代和老年代,新生代里面右划分为伊甸园区,From幸存区,和To幸存区;这样划分是要把长时间引用的对象放在老年代,其他放在新生代,这样分别进行不同的垃圾回收算法
    在这里插入图片描述
    ① 第一次垃圾回收:产生新的对象会被放到伊甸园(Eden)当中,当伊甸园区空间不足会使用可达性分析算法进行标记,采用复制算法把存活对象放到幸存区To中(minor GC),并且寿命+1,再把From和To交换位置
    在这里插入图片描述
    ② 第二次垃圾回收:这个时候伊甸园就有空间了可以继续放对象,当伊甸园又满了的时候,触发第二次垃圾回收(minor GC),这个时候在To区域和伊甸园区进行标记,把存活对象放到From区域,并且寿命都+1,再交换From和To的位置(复制算法)
    在这里插入图片描述

③ 当分代年龄到了15时:把该对象晋升到老年代,因为老年代的垃圾回收频率低
在这里插入图片描述
以上都是Minor GC,会触发Stop The World,暂停用户线程,进行垃圾回收,等垃圾回收结束再开始线程(因为Minor GC会牵扯到对象地址的移动,所以要暂停用户的线程)(但是Minor GC的Stop The World时间短)

④ 当老年代也满了就会先触发Minor GC,如果空间还不足则进行Full GC(也会触发Stop The World,并且时间更长,因为老年代的垃圾清除算法和新生代的不同,效率不一样),对全部的对象进行一次垃圾回收(标记清除或标记整理)

⑤ 如果Full GC后内存还不足,则会报错,OutOfMemoryError

大对象直接晋升到老年代

新加入一个放在老年代空间够但新生代的空间不够的对象,直接晋升到老年代而不触发minor GC

垃圾回收器

  1. 串行的垃圾回收器
    使用单线程的垃圾回收,垃圾回收发生的时候,把其他线程都停止,垃圾回收线程开始进行垃圾回收,新生代使用复制算法,老年代使用标记整理,触发垃圾回收的时候要把所有用户线程都在安全点停止,只运行垃圾回收线程
    在这里插入图片描述

适合:
① 堆内存小
② 单核个人电脑,因为只有一个线程工作

  1. 吞吐量优先(多线程的垃圾回收)(jkd1.8默认使用)
    触发垃圾回收的时候,用户线程跑到安全点停止,开启所有的垃圾回收的线程(所有cpu)进行垃圾回收
    其中
    在这里插入图片描述
    开启此垃圾回收器,新生代是复制,老年代是标记整理,开启一个另一个也会被开启
    在这里插入图片描述
    自适应的大小调整新生代的大小开关
    在这里插入图片描述
    (1/1+radio)是垃圾回收时间和总运行时间的占比,会通过调整堆的大小来达到该目标(如增大堆的大小,这样垃圾回收的次数就少了)(radio的默认值是99)
    在这里插入图片描述
    设置暂停的最大毫秒数,和上一个的目标实际上是冲突的,堆的大小变大,那么每次的垃圾回收暂停时间就变长了
    在这里插入图片描述
    控制垃圾回收开启的线程数

在这里插入图片描述
适合:
① 堆内存大
② 多核,因为是多线程的垃圾回收

目标: 尽可能让单位时间的STW时间最短

  1. 响应时间优先(多线程的垃圾回收)(针对老年代)
    用户线程和垃圾回收线程是并发的,在某几个阶段都可以运行,但某些阶段还是要STW
    在这里插入图片描述
    是针对老年代的垃圾回收器,采用的是标记清除,开启此垃圾回收器,如果老年代发生并发失败会退化到老年代并行垃圾回收器
    ① 老年代发生内存不足,线程到达安全点暂停,阻塞用户线程,进行初始标记,把GC Root标记出来(快)
    ② 用户线程继续运行,并且并发标记,找垃圾
    ③ 并发标记结束以后,再停止用户线程进行重新标记
    ④ 并发清理

在这里插入图片描述
并行的垃圾回收线程数,一般就是cpu数(重新标记所有的cpu同时进行)

在这里插入图片描述
并发的垃圾回收线程数,一般是cpu数的1/4,其他的给用户线程

在这里插入图片描述
① 堆内存大
② 多核,因为是多线程的垃圾回收

目标:尽可能让单次STW的时间最短

缺点:
① 并发标记,并发清理等垃圾回收线程占用了CPU,所有CPU的吞吐量有影响

② 有浮动垃圾,是在并发清理的时候,因为还有用户线程在运行导致的,所以要预留一些空间,而不是在空间不足才进行垃圾回收,如果浮动垃圾导致内存空间不足,会退化的单线程的垃圾回收
在这里插入图片描述
设置垃圾回收的触发时机,不能在满了再回收

③ 在重新标记的时候要扫描新生代去看有没有对老年代的引用,但是这个时候新生代的对象很多,所以可以先对新生代做一次垃圾清理再扫描
在这里插入图片描述
④ 因为是并发清除算法所以会有碎片,可能并发失败,会退化为单线程的串行垃圾回收

  1. G1(jdk9默认)
    并发的,同时追求响应时间(低延迟)和吞吐量
    适合超大的堆内存,超大的堆内存,将堆划分为多个大小相等的Region,每个区域都可以做伊甸园,老年区,整体是标记整理算法,两个Region是复制算法
    在这里插入图片描述
    ① Young Collection对新生代进行垃圾收集
    在这里插入图片描述
    新生代内存紧张执行垃圾收集,把幸存对象复制到幸存区
    在这里插入图片描述
    幸存区对象到了一定年龄会进入老年代
    在这里插入图片描述
    这里对于新生代对象的可达性分析,如果他是由老年代的对象进行引用的,而老年代的对象多,则查找效率低,会使用card表的技术,把老年代再细分为一个个card(每个大概是512k),如果这个card里面有对象引用了新生代的对象,则把他标记为脏卡,将来查找只要查脏卡
    在这里插入图片描述

② Young Collection + Concurrent Mark在新生代的垃圾收集进行并发的标记(对老年代)
在Young CC时会对GC Root进行初始标记(STW),老年代占用堆空间比例到达阈值的时候,进行并发标记(顺着GC Root找垃圾,不会STW)
在这里插入图片描述
在这里插入图片描述
③ 混合收集,对新生代和老年代进行垃圾回收
在这里插入图片描述
这里面对于老年代来说,并不是都进行回收,因为有最大暂停时间,如果对所有老年进行垃圾回收(复制),花费的时间多,所有为了达到目标,挑出回收价值最高的区域进行垃圾回收,所以叫做G1,垃圾优先
在这里插入图片描述

Full GC概念辨析

在这里插入图片描述
但是对于G1,当老年代空间不足的时候,会进行并发标记和混合收集,在这个时候,如果回收速度要大于新的垃圾的产生速度,还不会进行Full GC,还是处于并发标记和混合收集,虽然会STW,但只是一部分时间STW,时间短
但是如果回收速度要小于新的垃圾的产生速度,并发垃圾回收就失败了,就会退化为串行的垃圾回收,进行Full GC,STW时间长

重新标记

在并发标记的时候,假设有如下状态(黑色是已经处理完,灰色正在处理,白色没处理)
在这里插入图片描述
处理完B后,B变为已处理,这个时候要去处理C
在这里插入图片描述
但是这个阶段的标记是和用户线程同时进行的,假设用户线程把C和B的引用断了,C就是了垃圾
在这里插入图片描述
但是这个时候,如果用户线程又把A和C引用起来,但是A和C都是检查过的,C已经被当成垃圾了,会被清除
在这里插入图片描述
所以我们最后需要STW重新标记,当并发标记的时候如果对象地址发生改变,JVM就会给这个对象加上一个写屏障,会把C加入一个队列,把C变成灰色,等重现标记的时候,从队列中一个个判断对象状态
在这里插入图片描述
这样就防止了C被当做垃圾处理

字符串去重

在这里插入图片描述

String s1 = new String("hello");//char[]{'h','e','l','l','o'}
String s2 = new String("hello");//char[]{'h','e','l','l','o'}

s1 和 s2 都是new出来的字符串对象底层用char [] 实现放在堆中,他们的值是相同的
在JDK8中有这样的机制,将所有新分配的字符串放入一个队列
当新生代回收的时候,G1并发的检查是不是有字符串的值一样,如果值一样则让他们引用同一个char[]
这和String intern()不同,String intern()关注的是字符串对象,字符串去重关注的是char[]
会节省大量的内存,但是因为垃圾回收会占用cpu时间

G1类卸载

所有对象都经过并发标记后,就能知道哪些类不再使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

发布了56 篇原创文章 · 获赞 0 · 访问量 917
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览