理解JVM垃圾回收机制(GC)

Garbage Collection(GC),Java进程在启动之后就会自动创建垃圾回收线程,来对无用的内存空间进行回收。

垃圾回收策略

当进行垃圾回收器某一个对象的时候,我们需要判断这个对象是否已经“死去”(不能被任何途径使用的对象),判断的算法有两种:

  • 1、引用计数算法(Reference Counting)

    给对象中添加一个引用计数器,当一个地方引用它时,计数器便加1;当引用失效时,计数器就减一;任何时刻计数器为0的对象就是不可能再被使用的。这个算法的实现简单,判定的效率也挺高,但是它很难解决对象之间的相互循环的引用。

    这也间接说明了虚拟机并不是通过这个算法判定对象是否存活的

  • 2、可达性分析算法
    基本思路就是通过一系列的称为“GC Roots”的对象作为起点,从这些结点向下搜索,搜索所走过的路程称为引用链,当一个对象到GC Roots没有任何引用链想连,则说明这个对象是不可用的。
    在这里插入图片描述
    Java引用类型
    垃圾收集器是否进行垃圾的回收,我们的都离不开引用。Java将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

  • 1、强引用
    就是我们通常new操作,例如“Object o = new Object”这类的引用,只要强引用还存在,垃圾收集器是永远不会回收被引用的对象

  • 2、软引用
    是用来描述一些有用但是并不是必需的对象。而对于软引用关联的对象,会在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还是没有足够的内存空间,才会抛出内存溢出异常。

  • 3、弱引用
    用来描述非必需对象,相对于软引用来说,更弱一些。被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾收集器工作的时候,不管内存是否足够,都会回收掉这部分对象。

  • 4、虚引用
    也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用(PhantomReference)关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

回收的内存

在Java内存模型中,程序在运行的时候,程序计数器、JVM虚拟机栈、本地方法栈这三块内存空间都是线程私有的,随着线程的结束,这三块内存空间也就自己释放;栈中的栈帧随着方的进入和退出操作,进行着出栈和进栈的操作,每一个栈帧中分配多少的内存空间在类结构确定下来之后都是固定的,这几个区域的内存分配和回收都是具有确定性的,因为方法的结束或者线程的结束,内存就随之被回收。
但是堆和方法区却不是,程序只有在运行的时候,才会进行对象的创建,这些部分内存的分配和回收都是动态的,因此垃圾回收也是针对这部分来说的。

1、方法区(jdk1.7)/元空间(jdk1.8)

  • JDK1.7中的方法区在GC中称为永久代
  • JDK1.8中的元空间存在于本地内存,GC也是要对其进行回收的
  • 永久代和元空间的垃圾回收主要是两部分:废弃常量和无用的类

2、堆
这部分是垃圾收集器管理的主要区域,也叫“GC堆”
从内存回收角度来看,由于现在的收集器都采用分代收集算法,Java堆中还可以细分为:

  • 新生代(Young Generation):又分为Eden区,From Survivor区,To Survivor区
    这部分又称为Young GC,Minor GC,发生在新生代垃圾收集的动作,Java对象大多都具有朝生夕灭的特点,所以Minor GC非常频繁,一般回收速度也比较快

  • 老年代(Old Generation)
    这部分又又称为Major GC,回收速度一般比Minor GC慢10倍以上

  • Full GC:在不同的语义中有着不同的含义,有时候是Major GC,有时候也指全堆的垃圾回收,还可能指有用户线程暂停(Stop-The-World)的垃圾回收

在这里插入图片描述

垃圾收集算法

1、标记-清除算法(Mark-Sweep算法)-------->老年代的收集算法
分为两个阶段标记和清除,首先标记出所有需要回收的对象,在标记(finalize())完成后再统一删除,缺点是效率不高,两个过程效率都不高;空间问题,标记清除之后会产生大量不连续的内存碎片,太多的碎片可能会导致以后在程序进行中需要分配大对象的时候,无法找到连续的内存要再次触发另一次垃圾收集
在这里插入图片描述
2、复制算法(Copying算法)---------->新生代的收集算法
这种算法是将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
在这里插入图片描述
3、标记-整理算法(Mark-Compact算法)---------->老年代收集算法
标记过程仍与"标记-清除算法"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述
4、分代收集算法(Generational Collection)
当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思
想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据不同年代的特点采用最适当的收集算法。
新生代中98%的对象都是"朝生夕死"的,所以并不需要按照复制算法所要求1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的
Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden :Survivor From : Survivor To = 8 : 1 : 1。所以每次新生代可用内存空间为整个新生代容量的90%,只有10%的内存会被”浪费“。

新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

垃圾回收过程

在这里插入图片描述
堆的GC
1、新生代GC:用户线程创建的对象对优先分配在eden区中,当eden区空间不足时,会触发Minor GC:将eden区和使用的s区中存活的对象复制到留空的s区(存活对象年龄加1),最后清理eden区和刚才使用过的s区
1)进入eden区条件:创建对象时优先分配的区域;空间不足,触发Minor GC
2)Survior区:一块保存对象,一块留空,
进入的条件:触发Minor GC时,留白的S区,会进入存活的对象,但是当内存不足时,存活对象通过分配担保机制进入老年代
在这里插入图片描述
2、老年代GC:空间不足时触发Major GC
进入老年代的条件:

  • 大对象(大量连续的Java对象)直接进入;

  • 长期存活的对象进入(对象年纪>阈值);

    1)如果对象在Eden出生并经过一次Minor GC后仍然存活,并且可以在Survivor存放的话,将被移动到Survivor空间中,并且把对象年龄设为1。对象在Survivor空间中每经过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将晋升到老年代中
    2)如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

  • Minor GC时,但是当S区内存不足时,存活对象通过分配担保机制进入老年代

垃圾收集影响

1、STW(Stop The World):GC导致的暂停用户线程
1)原因:用户线程和GC线程并行、并发的执行,标记可回收的对象,在用户线程并发执行的时候可能重新有引用指向该对象。
2)GC时,什么情况会STW。新生代GC:都会STW——因为Minor GC时间非常快,几乎忽略不计,影响很小;老年代GC:根据阶段来看-——影响非常大,因为老年代空间大,需要回收的对象多,耗时也多;Full GC一般是影响很大,所以一般包含Major GC,可能出现Minor GC
2、指标
吞吐率优先:用户线程总的停顿时间短,即使单次停顿时间长一点也可以接受
用户体验优先:用户线程单次停顿时间短,即使总的停顿时间长一点也可以接受
在固定的时间内来算,单次的STW时间越长,STW总时间可能越少
用户体验和吞吐率就对应表现出反比关系

垃圾收集器

在这里插入图片描述
1、Serial收集器(新生代收集器,串行GC)
特点:单线程、复制算法、STW
优点:简单高效,对于单个CPU。它没有线程的交互,做事效率更好
2、ParNew收集器(新生代收集器,并行GC)-------->和CMS搭配
特点:多线程、复制算法、STW
与CMS搭配,使用在用户体验优先的程序中
3、Parallel Scavenge收集器(新生代收集器,并行GC)
特点:多线程、复制算法、可控制的吞吐量
吞吐量优先
自适应的调节策略:JVM设置这个参数等于true后,JVM可以监控其性能,并且动态的设置内存相关的参数(如:年龄阈值、新生代大小等)
4、Serial Old收集器(老年代收集器,串行GC)
Serial Old是Serial收集器的老年代版本,CMS在发生并发失败时作为备用的垃圾回收方案Concurrent Mode Failure
特点:单线程,“标记-整理”算法
Parallel Old收集器(老年代收集器,并行GC)
Parallel Scavenge收集器的老年代版本
特点:多线程、“标记-整理”算法、STW
吞吐量优先,搭配Parallel Scavenge一起使用
CMS收集器(老年代收集器,并发GC)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
特点:用户体验优先、标记清除算法
分为四个阶段:
1)初始标记:标记GC Roots可以直接关联的对象,速度很快,STW
2)并发标记:GC Roots Trancing
3) 重新标记:STW,解决第二阶段用户线程并发执行,导致已经标记的对象被重新引用
4)并发清除:并发清除对象
四个阶段中,1、3执行速度快,消耗的时间少——暂停用户的时间少;2、4执行时间相对较慢,但是可以和用户线程并发执行

总体来说,CMS垃圾回收的工作是和用户线程同时执行
缺点:
1、CMS垃圾回收线程会抢占CPU资源,导致用户线程总的执行时间更短
2、浮动垃圾的问题
产生的原因:CMS第四个阶段,用户线程并发执行又可能导致有对象进入老年代,而老年代剩余空间不足,触发Major GC——Concurrent Mode Failure
解决方案:Serial Old进行垃圾回收
标记清除算法会导致大量的碎片空间,如果进入的对象没有连续可用的空间,触发Full GC
G1收集器(全区域的垃圾收集器)
特点:整体看是利用了标记整理算法,局部看是复制算法
用户体验优先
实现:堆内存划分为很多的内存区域,每块内存区域都是动态的指定为eden区,S区、T区(老年代)

  • 1、年轻代垃圾收集:使用复制算法,将eden区和S区的对象复制到新的S区
    在这里插入图片描述
  • 2、老年代的垃圾收集:分为四个阶段
    初始标记:可以和新生代Minor GC同时执行,STW
    并发标记:这个阶段做的事情和CMS这个阶段做的事情一样。但是G1害多做了一件事就是Garbage First,清理老年代(T区)存活率很小或没有对象存活的T区
    最终标记:STW,与CMS第三个阶段使用的算法不同(SATB),做的工作一样
    筛选回收:和CMS算法不同,采用clean up/copy和新生代类似的整理工作,可以和Minor GC同时执行,STW

垃圾回收时机

1、显式的调用System.gc(),这个方式仅仅是告诉垃圾收集器打算进行垃圾收集(FGC),而垃圾收集器进不进行收集是不确定的,但是大多数情况下会进行FGC。但是一般不使用这个方法,让JVM自己管理它的内存
2、JVM垃圾回收机制自己决定
Minor GC触发条件:创建的对象在eden区,且eden区空间不足时
Major GC 触发条件:对象需要放入老年代时,老年代内存空间不足时,都会触发:
1)新生代的年老对象进入老年代(年龄达到阈值)
2)大对象(需要大量连续空间的Java对象)直接进入老年代
3)Minor GC 的分配担保机制,在发生Minor GC之前,如果老年代最大可用的连续空间小于新生代所有对象的总空间,触发Major GC:

  • 不允许空间分配担保
  • 允许空间分配担保,但是老年代最大连续可用空间小于历次晋升到老年代的对象的平均大小
  • 允许空间担保,但是Minor GC后空间不足
    4)CMS无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”而导致另一次的Full GC

猜你喜欢

转载自blog.csdn.net/qq_44723296/article/details/106928817