JVM-垃圾回收

如何识别垃圾

引用计数法

原理: 对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收

产生的问题 : 循环引用

  1. 实例化两个同一类型的对象
  2. 使它们指向的对象互相指望对方
  3. 将两个对象置为null,但是由于之前它们指向的对象互相指向了对方(引用计数都为1),所以无法回收

所以现代虚拟机都不用引用计数法来判断对象是否应该被回收

可达性算法

原理: 以一系列叫做GC Root的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。(这样通过GC Root串成的一条线就叫引用链) ,直到所有的结点都遍历完毕,如果相关对象不在任意一个以GC Root为起点的引用链中,则这些对象会被判断为垃圾,会被GC回收
在这里插入图片描述

a,b对象可回收,就一定会被回收吗?

当对象不可达(可回收)时,发生GC的时候会先判断对象是否执行了finalize方法,如果未执行,则会先执行finalize方法,在此方法里将当前对象与GC Roots关联,执行finalize方法之后,GC会再次判断对象是否可达。如果不可达则会被回收!

注意: finalize方法只会被执行一次,如果第一次执行此对象变成了可达确定不会回收,但如果对象再次被GC,则会忽略finalize方法,对象会被回收

哪些对象可以作为GC Root

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(一般说的Native方法)引用的对象

虚拟机栈中引用的对象
a是栈帧中的本地变量,当a=null时,由于此时a充当了GC Root的作用,a与原来指向的实例 new Test() 断开了连接,所以对象会被回收

public class Test {
    public static  void main(String[] args) {
	Test a = new Test();
	a = null;
    }
}

方法区中类静态属性引用的对象
当栈帧中的本地变量a=null时,由于a原来指向的对象与GC Root(变量a)断开了连接,所以a原来指向的对象回被回收,而由于给s赋值了变量的引用,s在此时是累静态属性引用,充当了GC Roots的作用,它指向的对象依然存活

public class Test {
    public static Test s;
    public static  void main(String[] args) {
	Test a = new Test();
	a.s = new Test();
	a = null;
    }
}

方法区中常量引用的对象
常量 s 指向的对象并不会因为 a 指向的对象被回收而回收

public class Test {
	public static final Test s = new Test();
        public static void main(String[] args) {
	    Test a = new Test();
	    a = null;
        }
}

本地方法栈中 JNI 引用的对象
本地方法: 一个 java 调用非 java 代码的接口,该方法并非 Java 实现的,可能由 C 或 Python等其他语言实现的, Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。通过调用本地的库文件的内部方法,使 JAVA 可以实现和本地机器的紧密联系,调用系统级的各接口方法

当调用 Java 方法时,虚拟机会创建一个栈桢并压入 Java 栈,而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法
在这里插入图片描述

垃圾回收算法

标记清除算法

  • 标记清除算法其过程分为标记清除两个阶段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间
  • 标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用空间的问题

复制算法

  • 复制算法首先将内存划分为两块大小相等的内存区域,即区域1和区域2,新生成的对象都被存放在区域1中,在区域1内的对象存储满后会对区域1进行一次标记,并将标记后仍然存活的对象全部复制到区域2中,这时区域1将不存在任何存活的对象,直接清理整个区域1的内存即可
  • 复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量的内存浪费。同时,在系统中有大量长时间存活的对象时,这些对象将在内存区域1和内存区域2之间来回复制而影响系统的运行效率。因此,该算法只在对象为“朝生夕死”状态时运行效率较高

标记整理法

  • 标记整理算法结合了复制算法和标记清除算法的优点,其标记阶段和标记清除算法相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存
  • 每次垃圾清除都要频繁地移动存活的对象,效率十分低下

分代垃圾算法

  • 大部分的对象都很短命,都在很短的时间内都被回收了,所以分代收集算法根据对象存活周期的不同将堆分成新生代老生代(Java8以前还有个永久代),默认比例为 1 : 2

  • 新生代又分为 Eden 区, from Survivor 区(简称S0),to Survivor 区(简称 S1),三者的比例为 8: 1 : 1,这样就可以根据新老生代的特点选择最合适的垃圾回收算法,我们把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Full GC)
    在这里插入图片描述
    为什么堆要分新生代和老年代

    为了更有效的管理内存

  • 如果不分新老代,内存就一整块,垃圾收集器每次都要把那些长期存在的对象生命周期很短的对象放在一起回收,一般长生命周期的对象可能跟应用生命周期一致,基本回收不掉的。比如Spring 框架里面的Bean管理相关的对象(ApplicationContext),整个应用运行期间都存在,这种一般经过几次回收最后都放在老年代,但是如果不区分新老代,每次都一起回收,性能消耗很大

  • 区分新老代之后,老年代放长期存活的对象,新生代就放生命周期短的对象,老年代对象很稳定,新生代回收不影响老年代,回收效率能大大提高

为什么新生代还要分 Eden、From、To区域

大部分对象生命周期是很短的,如果新生代不分多个区域,新生代可能会有二种回收方案

第一种可能 :每次回收都在新生代整块内存上进行,完整的垃圾回收分三步 :

  1. 需要先找到需要清理的对象标记
  2. 清理这些被标记的对象
  3. 移动剩下的对象,对达到老年代晋升年龄的对象移动到老年代

对象被回收掉后会产生很多内存碎片(被回收的对象很多),如果要解决内存碎片,需要移动剩下的对象(标记整理算法),整个回收流程效率很低

第二种可能
如果没有Survivor区(From + To),Minor GC(新生代回收)过程中,存活的对象直接被送到老年代,这样的话老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC),Full GC频繁会影响程序的执行和响应速度

对象在新生代的分配与回收

  • 大部分对象在很短的时间内都会被回收,对象一般分配在 Eden 区, Eden 区将满时,触发 Minor GC
  • 经过Minor GC 后只有少部分对象会存活,它们会被移到 S0 区(空间大小 Eden: S0: S1 = 8:1:1,Eden 区远大于 S0,S1 的原因,因为在 Eden 区触发的 Minor GC 把大部对象(接近98%)都回收了,只留下少量存活的对象,此时把它们移到 S0 或 S1 绰绰有余)同时对象年龄加一( Minor GC 的次数),最后把 Eden 区对象全部清理以释放出空间
  • 当触发下一次 Minor GC 时,会把 Eden 区和 S0 中的存活对象(S0 或 S1 中的存活对象经过每次 Minor GC 都可能被回收)一起移到 S1(Eden 和 S0 的存活对象年龄+1), 同时清空 Eden 和 S0 的空间
  • 若再触发下一次 Minor GC,则重复上一步,此时变成了 从 Eden,S1 区将存活对象复制到 S0 区,每次垃圾回收, S0, S1 角色互换,都是从 Eden ,S0(或S1) 将存活对象移动到 S1(或S0)。在 Eden 区的垃圾回收采用的是复制算法,因为在 Eden 区分配的对象大部分在 Minor GC 后都消亡了,只剩下极少部分存活对象(这也是为啥 Eden:S0:S1 默认为 8:1:1 的原因),S0,S1 区域也比较小,所以最大限度地降低了复制算法造成的对象频繁拷贝带来的开销

对象晋升老年代

  • 当对象的年龄达到了设定的阈值,则会从S0(或S1)晋升到老年代
  • 当某个大对象分配需要大量的连续内存时,此时对象的创建不会分配在 Eden 区,会直接分配在老年代,如果把大对象分配在 Eden 区, Minor GC 后再移动到 S0,S1 会有很大的开销(对象比较大,复制会比较慢,也占空间),也很快会占满 S0,S1 区,所以就直接移到老年代
  • 在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代

空间分配担保

  • 在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
  • 如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败
  • 如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC

Stop The World

  • 如果老年代满了,会触发 Full GC同时回收新生代和老年代(即对整个堆进行GC),它会导致STW,造成挺大的性能开销

  • STW在GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起
    在这里插入图片描述

  • 一般 Full GC 会导致工作线程停顿时间过长(因为Full GC 会清理整个堆中的不可用对象,一般要花较长的时间),如果在此 server 收到了很多请求,则会被拒绝服务!尽量减少 Full GC(Minor GC 也会造成 STW,但只会触发轻微的 STW,因为 Eden 区的对象大部分都被回收了,只有极少数存活对象会通过复制算法转移到 S0 或 S1 区,所以相对还好)

  • 把新生代设置成 Eden, S0,S1区或者给对象设置年龄阈值或者默认把新生代与老年代的空间大小设置成 1:2 都是为了尽可能地避免对象过早地进入老年代,尽可能晚地触发 Full GC。新生代如果只设置 Eden则每经过一次 Minor GC,存活对象会过早地进入老年代,那么老年代很快就会装满,很快会触发 Full GC,而对象其实在经过两三次的 Minor GC 后大部分都会消亡,所以有了 S0,S1的缓冲,只有少数的对象会进入老年代,老年代大小也就不会这么快地增长,也就避免了过早地触发 Full GC

Safe Point

由于 Full GC(或Minor GC) 会影响性能,所以我们要在一个合适的时间点发起 GC,这个时间点被称为 Safe Point,这个时间点的选定既不能太少以让 GC 时间太长导致程序过长时间卡顿,也不能过于频繁以至于过分增大运行时的负荷。一般当线程在这个时间点上状态是可以确定的,如确定 GC Root 的信息等,可以使 JVM 开始安全地 GC

Safe Point 主要指的是以下特定位置:

  • 循环的末尾
  • 方法返回前
  • 调用方法的 call 之后
  • 抛出异常的位置

新生代的特点(大部分对象经过 Minor GC后会消亡), Minor GC 用的是复制算法。在老生代由于对象比较多,占用的空间较大,使用复制算法会有较大开销,所以根据老生代特点,在老年代进行的 GC 一般采用的是标记整理法来进行回收

垃圾收集器种类

Java 虚拟机规范并没有规定垃圾收集器应该如何实现,因此一般来说不同厂商,不同版本的虚拟机提供的垃圾收集器实现可能会有差别,一般会给出参数来让用户根据应用的特点来组合各个年代使用的收集器
在这里插入图片描述

  • 新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge
  • 老年代工作的垃圾回收器:CMSSerial Old, Parallel Old
  • 同时在新老生代工作的垃圾回收器:G1
    图片中的垃圾收集器如果存在连线,则代表它们之间可以配合使用

新生代收集器

Serial收集器

  • Serial 收集器是工作在新生代的,单线程的垃圾收集器,它在进行垃圾收集时,其他用户线程会暂停,直到垃圾收集结束
  • Client 模式下,对于限定单个 CPU 的环境来说,Serial 单线程模式无需与其他线程交互,减少了开销,专心做 GC 能将其单线程的优势发挥到极致。在用户的桌面应用场景,分配给虚拟机的内存一般不会很大,收集几十甚至一两百兆,STW 时间可以控制在一百多毫秒内,所以对于运行在 Client 模式下的虚拟机,Serial 收集器是新生代的默认收集器

ParNew收集器

在这里插入图片描述

  • ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程,其他像收集算法,STW,对象分配规则,回收策略与 Serial 收集器完成一样,在底层上,这两种收集器也共用了相当多的代码
  • ParNew 主要工作在 Server 模式,服务端接收的请求多,多线程可以让垃圾回收得更快,也就是减少了 STW 时间,能提升响应时间,许多运行在 Server 模式下的虚拟机的首选新生代收集器
  • 除了 Serial 收集器,只有它能与 CMS 收集器配合工作

Parallel Scavenge 收集器

  • Parallel Scavenge 收集器也是一个使用复制算法,多线程,工作于新生代的垃圾收集器
  • CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),CMS 等垃圾收集器更适合用到与用户交互的程序,因为停顿时间越短,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,所以更适合做后台运算等不需要太多用户交互的任务

老年代收集器

Serial Old收集器

  • Serial Old 是工作于老年代单线程收集器,此收集器的主要意义在于给 Client 模式下的虚拟机使用
  • 如果在 Server 模式下,则它还有两大用途:一种是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合使用,另一种是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用(后文讲述),它与 Serial 收集器配合使用示意图如下
    在这里插入图片描述

Parallel Old 收集器

  • Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,两者组合示意图如下,这两者的组合由于都是多线程收集器,真正实现了「吞吐量优先」的目标
    在这里插入图片描述

CMS收集器

CMS 收集器是以实现最短 STW 时间为目标的收集器,如果应用很重视服务的响应速度,希望给用户最好的体验,则 CMS 收集器是个很不错的选择
老年代主要用标记整理法,而 CMS 虽然工作于老年代,但采用的是标记清除法

  • 初始标记

  • 并发标记

  • 重新标记

  • 并发清除
    在这里插入图片描述

  • 初始标记重新标记两个阶段会发生 STW,造成用户线程挂起,不过初始标记仅标记 GC Roots 能关联的对象,速度很快,并发标记是进行 GC Roots Tracing 的过程,重新标记是为了修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这一阶段停顿时间一般比初始标记阶段稍长,但远比并发标记时间短

  • 整个过程中耗时最长的是并发标记和标记清理,不过这两个阶段用户线程都可工作,所以不影响应用的正常使用,所以总体上看,可以认为 CMS 收集器的内存回收过程是与用户线程一起并发执行的

CMS收集器的三个缺点

  • CMS 收集器对 CPU 资源非常敏感。比如本来我本来可以有 10 个用户线程处理请求,现在却要分出 3 个作为回收线程,吞吐量下降了30%,CMS 默认启动的回收线程数是 (CPU数量+3)/ 4, 如果 CPU 数量只有一两个,那吞吐量就直接下降 50%,显然是不可接受的
  • CMS 无法处理浮动垃圾,可能出现 「Concurrent Mode Failure」而导致另一次 Full GC 的产生,由于在并发清理阶段用户线程还在运行,所以清理的同时新的垃圾也在不断出现,这部分垃圾只能在下一次 GC 时再清理掉(即浮云垃圾),同时在垃圾收集阶段用户线程也要继续运行,就需要预留足够多的空间要确保用户线程正常执行,这就意味着 CMS 收集器不能像其他收集器一样等老年代满了再使用,JDK 1.5 默认当老年代使用了68%空间后就会被激活,当然这个比例可以通过 -XX:CMSInitiatingOccupancyFraction 来设置,但是如果设置地太高很容易导致在 CMS 运行期间预留的内存无法满足程序要求,会导致 Concurrent Mode Failure 失败,这时会启用 Serial Old 收集器来重新进行老年代的收集,而我们知道 Serial Old 收集器是单线程收集器,这样就会导致 STW 更长了
  • CMS 采用的是标记清除法,会产生大量的内存碎片,这样会给大内存分配带来很大的麻烦,如果无法找到足够大的连续空间来分配对象,将会触发 Full GC,这会影响应用的性能。当然我们可以开启 -XX:+UseCMSCompactAtFullCollection(默认是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,内存整理会导致 STW,停顿时间会变长,还可以用另一个参数 -XX:CMSFullGCsBeforeCompation 用来设置执行多少次不压缩的 Full GC 后跟着带来一次带压缩的

G1收集器

G1的几个特点

  • 像 CMS 收集器一样,能与应用程序线程并发执行
  • 整理空闲空间更快
  • 需要 GC 停顿时间更好预测
  • 不会像 CMS 那样牺牲大量的吞吐性能
  • 不需要更大的 Java Heap

与 CMS 相比,它在以下两个方面表现更出色

  • 运作期间不会产生内存碎片,G1 从整体上看采用的是标记-整理法,局部(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片,收集后提供规整的可用内存,这样有利于程序的长时间运行
  • STW 上建立了可预测的停顿时间模型,用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内

在这里插入图片描述
为什么G1能建立可预测的停顿模型?

  • G1对堆空间的分配与传统的垃圾收集器不一样, G1 各代的存储地址不是连续的,每一代都使用了 n 个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址
  • Region还多了一个H,表示这些Region存储的是巨大对象,即大小大于等于region一半的对象,这样超大对象就直接分配到了老年代,防止了反复拷贝移动

G1这样分配的好处
传统的收集器如果发生 Full GC 是对整个堆进行全区域的垃圾收集,而分配成各个 Region 的话,方便 G1 跟踪各个 Region 里垃圾堆积的价值大小,这样根据价值大小维护一个优先列表,根据允许的收集时间,优先收集回收价值最大的 Region,也就避免了整个老年代的回收,也就减少了 STW 造成的停顿时间。同时由于只收集部分 Region,可就做到了 STW 时间的可控。

G1收集器的工作步骤

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

在这里插入图片描述
整体过程与 CMS 收集器非常类似,筛选阶段会根据各个 Region 的回收价值和成本进行排序,根据用户期望的 GC 停顿时间来制定回收计划

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值