深入理解JVM(四)—垃圾回收

一、垃圾回收简介

1.1 什么是垃圾回收

JVM中自动检测并移除不再使用的数据对象的这种机制称为:垃圾回收(Garbage Collection ),简称GC

1.2 为什么要垃圾回收

由于不同JAVA对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行垃圾回收,整个程序会因内存耗尽导致整个程序崩溃。垃圾回收还会整理那些零散的内存碎片,碎片过多最直接的问题就是会导致无法分配大块的内存空间以及降低程序的运行效率

二、垃圾对象的判定

2.1 引用计数法

给所有的对象添加一个引用计数器,每当有一个地方引用了这个对象时,就将计数器的值+1;每当引用失效时,就将计数器的值-1;当一个对象计数器的值为0时,就认为这个对象已经没用了,垃圾收集器可以把它回收

算法实现简单,效率较高,但是Java并没有选用引用计数算法来管理内存,因为遇到循环引用的时候这种方法不适用,

2.2 根可达性分析法

Java使用这种方式

将GC Roots作为起始节点,然后垃圾回收器从这这些起始节点开始搜索,搜索走过的路经称为引用链,当一个对象到GC Roots不可达,则证明这个对象已经死了,垃圾收集器可以将它回收

GC Roots由以下几种对象构成

  1. 存放在JVM栈中的栈帧中的本地变量表中的 引用的 对象可作为GC Roots
  2. 存放在方法区中的静态属性引用的对象,可作为GC Roots
  3. 方法区中的常量引用的对象可作为GC Roots
  4. 本地方法栈中的JNI(即Native方法)的引用的对象,可作为GC Roots
2.3 不可达的对象并非“非死不可”

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

2.4 方法区的垃圾回收

方法区中的垃圾回收主要回收两部分数据:废弃常量、无用的类

  • 废弃常量的回收
    废弃常量的回收和堆内存中对象的回收非常类似,加入字符串常量“abc“已经没有任何一个String类型的引用指向,那么当发生垃圾回收时,这个常量就会被内存回收

  • 无用类的回收
    满足以下三个条件才可以回收:
    a)堆内存中不存在该类的任何实例
    b)加载该类的ClassLoader已被回收
    c)该类的Class对象没有在任何地方被引用,也就是没有任何地方通过反射机制访问该类中的成员

三、垃圾回收算法

3.1 分代收集

对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对象也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。

分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率

垃圾回收两种类型

Minor GC

对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成

Full GC

也叫 Major GC,对整个堆进行回收,包括新生代、老年代和永久代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等

1.新生代

新生代内存按照 8:1:1 的比例分为一个eden区和两个survivor(survivor0,survivor1)区,大部分对象在Eden区中生成。

在进行垃圾回收时,先将eden区存活对象复制到survivor0区,然后清空eden区,当这个survivor0区也满了时,则将eden区和survivor0区存活对象复制到survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后交换survivor0区和survivor1区的角色(即下次垃圾回收时会扫描Eden区和survivor1区),即保持survivor0区为空,如此往复。特别地,当survivor1区也不足以存放eden区和survivor0区的存活对象时,就将存活对象直接存放到老年代。

注意在这个循环往复的过程中,对象每经过一次 Minor GC 后,将其年龄加1,当它的年龄增加到一定程度(默认为15岁),就也会被晋升到老年代中。

2.老年代

如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。如果到最后年老区,幸存者1区,幸存者0区和伊甸园区都没有空间的话,则JVM会报告:“JVM堆空间溢出”,也即是在堆空间没有空间来创建对象

3.永久代

永久代主要用于存放静态文件,如Java类、方法等。JVM的规范中没有规定必须实现永久代的垃圾收集。也就是说,不一定必须实现。而且永久代的垃圾回收“性价比”很低,新生代进行一次gc,一般可以回收70%-95%,但是永久代远低于此。永久代的垃圾收集主要分两个部分:废弃常量和无用的类。如果一个应用装载的class类比较多,永久代分配内存小的话,也会出现“永久存储区溢出”

3.2 标记清除算法

标记-清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收

标记-清除算法的不足:

  • 标记和清除两个过程的效率都不高;
  • 标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
3.3 复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉

新生代大部分对象存活时间短,使用这种算法效率高。Eden和Survivor的大小比例是 8:1,当回收时,将Eden和Survivor中还存活着的对象一次地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间,也就是每次新生代中可用内存空间为整个新生代容量的90% ( 80%+10% ),只有10% 的内存会被“浪费”

3.4 标记整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记整理算法的标记过程类似标记清除算法,对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存

四、垃圾收集器

4.1 Serial收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。

Serial收集器是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说, Serial收集器由于没有线程交互的开销,专心做垃圾收集,自然可以获得最高的单线程收集效率

4.2 ParNew收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样

4.3 Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。 parallel Scavenge收集器的特点是它的关注点与其他收集器不同, CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallelScavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%

4.4 Serial Old(串行GC)收集器

Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用标记-整理算法。主要使用在Client模式下的虚拟机

4.6 Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法

4.7 CMS(并发GC)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现的

垃圾回收过程:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrenr mark)
  • 重新标记(CMS remark)
  • 并发清除(CMSconcurrent sweep)

CMS收集器线程与用户线程一起工作,并发收集、低停顿,它对CPU资源非常敏感,不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降,无法处理浮动垃圾,会产生大量碎片

4.8 G1收集器

G1将新生代,老年代的物理空间划分取消,不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域, G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中, G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了

在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上, G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题, G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC

优点:

  • G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
  • G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值