深入理解JVM(三)垃圾收集器与内存分配策略

1.如何判定对象已死

判断对象是否已死有两种方法,一种是引用计数法,另一种是可达性分析算法。

1.1引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减一;任何时刻为0的对象就是不肯再被使用的。

1.2.可达性分析

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路称为引用链,当一个对象到GC Roots没有人格引用链项链,则证明对象是不可用的。在java语言中,可以作为GC Roots的对象包括下面几种:

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

1.2.2.方法区中静态属性引用的对象。

1.2.3.方法区中常量引用的对象。

1.2.4.本地方法栈中JNI(即一般说的Native方法)引用的对象。

2.java中的四种引用

2.1. 强引用:类似“Object o = new Object()”,只要强引用还存在,就不会被回收;

2.2.软引用:用来描述一些有用但非必须的对象。如果一个对象只具有软引用,则内存空间足够, 垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没 有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(如果内存够,软 引用没有被回收,则可以直接使用,如果内存不够,软引用已经被回收,则重新读取数据(如从 数据库中))。(java.lang.ref 包)SoftReferencesoftRef = new SoftReference(str);

2.3.弱引用:也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对 象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回 收只被弱引用关联的对象。如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但 又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。

2.4.虚引用:它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间 构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在 这个对象被收集器回收时收到一个系统通知。jdk1.2以后,提供了PhantomReference类来实现虚引用。

3.回收无效对象的过程

finalize()方法

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于 “缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。标记的前提是对象在进 行可达性分析后发现没有与 GC Roots 相连接的引用链。

3.1.第一次标记并进行一次筛选。

筛选的条件是此对象是否有必要执行 finalize()方法。当对象没有覆盖 finalize 方法,或者 finalize 方法已经被虚拟机调用过(finalize 只会调用一次),虚拟机将这两种情况都视为“没 有必要执行”,对象被回收。

3.2.第二次标记

如果这个对象被判定为有必要执行 finalize()方法,那么这个对象将会被放置在一个 名为:F-Queue 的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的 Finalizer 线 程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。 这样做的原因是,如果一个对象 finalize()方法中执行缓慢,或者发生死循环(更极端的 情况),将很可能会导致 F-Queue 队列中的其他对象永久处于等待状态,甚至导致整个内存 回收系统崩溃。

finalize()方法是对象脱逃死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对 象进行第二次小规模标记,如果对象要在 finalize()中成功拯救自己----只要重新与引用 链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那 在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真 的被回收了。

4.回收方法区

方法区中主要清除两种垃圾:

4.1. 废弃常量

4.2. 无用的类

4.1.1.判断废弃的常量

清除废弃的常量和清除对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。

4.2.1.如何判断废弃的类

清除废弃类的条件较为苛刻:

4.2.1. 该类的所有对象都已被清除。

4.2.2. 该类的java.lang.Class对象没有被任何对象或变量引用。

只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除。

4.2.3. 加载该类的ClassLoader已经被回收。

5.垃圾收集算法

5.1.标记-清除算法(Mark-Sweep)

首先标记处所有需要回收的对象,在标记完成后统一回 收。缺点:标记和清除两个过程都效率低;标记清除后会产生大量不连续的内存碎片,空间 碎片太多可能会导致以后在程序运行中需要分配大对象时,无法找到足够的连续内存而不得 不提取触发 GC。

5.2.复制算法

将可用内存按容量划分成大小相等的两块,每次只使用一块。当这一块使用 完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存一次清理掉。这样 不用考虑内存碎片的问题,只要移动堆顶指针,按顺序分配即可,实现简单、运行高效。缺点:内存缩小为原来的一半。现代商用虚拟机都采用这种算法回收新生代。而新生代中约 98%的对象都是“朝生夕死”,所以不需按 1:1 划分。HotSpot 默认 Eden 和 Survivor 是 8:1,所以每次可用内存为 90%。但 我们没法保证每次回收只有不多于 10%的对象存活,当 Survivor 空间不够时,需要依赖其他 内存(这里指老年代)进行分配担保(直接进入老年代)。

缺点:如果对象存活率太高,要进行较多复制操作,效率低。且需要额外空间担保,老 年代不能选用这种算法。

3、标记-整理算法。

过程与“标记-清除”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存 活的对象都向一端移动,然后直接清理掉端边界以外的内存。老年代因为对象存活率高、没 有额外空间进行分配担保,必须使用“标记-清理”或“标记-整理”算法。

4. 分代收集算法

将内存划分为老年代和新生代。老年代中存放寿命较长的对象,新生代中存放“朝生夕死”的对象。然后在不同的区域使用不同的垃圾收集算法。

6.JVM垃圾收集器


6.1.Serial 是一个单线程收集器,在它进行垃圾收集时,必须暂停其他所有工作线程(StopTheWorld);简单高效,是虚拟机在 Client模式下默认的新生代收集器(复制算法)。停顿 时间在几十到一百多毫秒以内,可以接受。


6.2.ParNew 其实就是 Serial 收集器的多线程版本;ParNew 收集器是许多运行在 Server模式下的虚拟机中首选的新生代收集器。除去性能因素,很重要的原因是除了 Serial 收集 器外,目前只有它能与 CMS收集器(老年代)配合工作。(复制算法)

但是,在单 CPU环境中,ParNew收集器绝对不会有比 Serial 收集器更好的效果,甚至 由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU的环境中都不能百分 之百地保证可以超越 Serial收集器。然而,随着可以使用的 CPU的数量的增加,它对于 GC 时系统资源的有效利用还是很有好处的。


6.3.Parallel Scavenge 收集器是新生代垃圾收集器,使用复制算法,也是并行的多线程 收集器。与 ParNew 收集器相比,很多相似之处,但是 Parallel Scavenge 收集器更关注可控 制的吞吐量(运行用户代码时间/(运行用户代码+垃圾收集时间))。吞吐量越大,垃圾收集 的时间越短,则用户代码则可以充分利用 CPU 资源,尽快完成程序的运算任务。

直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是 GC 停顿时间的缩 短是以牺牲吞吐量和新生代空间作为代价的。比如原来 10 秒收集一次,每次停顿 100 毫秒,现在变成 5 秒收集一次,每次停顿 70 毫秒。停顿时间下降的同时,吞吐量也下降了。

6.4.Serial Old 收集器是 Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与 Serial收集器一样。SerialOld收集器的主要意义也是在于给 Client 模式下的虚拟机使用。如果在 Server模式下,那么它主要还有两大用途:一种用途是在 JDK 1.5 以及之前的版本中与 Parallel Scavenge收集器搭配使用,另一种用途就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用。

6.5.Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其通常与 Parallel Scavenge 收集器配合使用,“吞吐量优先”收集器 是这个组合的特点,在注重吞吐量和 CPU 资源敏感的场合,都可以使用这个组合。

6.6.CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法,从总体上来说,CMS收集器的内存回收过程是与用户线程 一起并发执行的(有的过程也是 StopTheWorld)。

CMS分为四个步骤:初始标记(GCRoots能直接关联到的对象,速度快,可达性分析, Stop The World),并发标记(可达性分析),重新标记(修正并发标记期间因用户程序继续 运作而导致的变动,速度快,Stop The World),并发清除

CMS 的优点很明显:并发收集、低停顿。由于进行垃圾收集的时间主要耗在并发标记 与并发清除这两个过程,虽然初始标记和重新标记仍然需要暂停用户线程,但是从总体上看,

这部分占用的时间相比其他两个步骤很小,所以可以认为是低停顿的。

缺点:

对 CPU 资源太敏感,这点可以这么理解,虽然在并发标记阶段用户线程没有暂停,但 是由于收集器占用了一部分 CPU 资源,导致程序的响应速度变慢

CMS 收集器无法处理浮动垃圾。所谓的“浮动垃圾”,就是在并发标记阶段,由于用户程 序在运行,那么自然就会有新的垃圾产生,这部分垃圾被标记过后,CMS 无法在当次集中 处理它们(为什么?原因在于 CMS 是以获取最短停顿时间为目标的,自然不可能在一次垃 圾处理过程中花费太多时间),只好在下一次 GC 的时候处理。这部分未处理的垃圾就称为“浮 动垃圾”。由于垃圾收集阶段用户线程还需要运行,那就不能等老年代几乎全满了再收集, 一般达到 92%时就开始收集,而 CMS 运行期间预留的内存无法满足程序需要,就会出现 “Concurrent Mode Failure”,此时将启动备用方案 serial old

由于 CMS 收集器是基于“标记-清除”算法的(可能是为了时间短),前面说过这个算法会导致大量的空间碎片的产生,一旦空间碎片过多,大对象就没办法给其分配内存,那么即 使内存还有剩余空间容纳这个大对象,但是却没有连续的足够大的空间放下这个对象,所以 虚拟机就会触发一次 Full GC。


在使用 CMS收集老年代时,新生代只能选用 ParNew或者 Serial 收集器中的一个(CMS 与其他不配套,其他的没有使用传统的 GC 收集器框架)

6.7.(Garbage-First)收集器,JDK1.7 才开始商用。使用 G1 收集器时,Java 堆内存 布局与其他收集器有很大差别,它将整个 Java 堆分为多个大小相等的独立区域(Region), 虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是 Region(不需要连续)的集合。

特点:并行与并发。分代收集(不需要其他收集器配合)。空间整合(整体来看采用“标 记-整理”,局部(两个 Region 之间)采用复制)。可预测的停顿。

G1 跟踪各个 Region 里面的垃圾堆积价值大小(回收所获得的空间大小以及回收所需的 时间),在后台维护一个优先列表,每次优先收集价值最大的 Region(所以叫 Garbage-First),

从而保证了 G1 在有限时间内可以获取尽可能高的收集效率。

(老年代)过程:初始标记(StopTheWorld)、并发标记、最终标记(StopTheWorld)、筛选回收(Stop The World)

G1 的 YoungGC 就是将 E 区和 S 区复制到灰色的空白区。

G1 中有 Humongous 区(巨大区)用于存放比标准块大 50%的对象



7.JVM垃圾回收机制

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算 法,只需要付出少量存活对象的复制成本就可以完成收集(有 eden和 survivor供复制,有 老年代最分配担保)。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就 必须使用“标记-清理”或者“标记-整理”算法来进行回收。

发生 Minor GC,采用复制算法,发现

7.1.复制对象无法全部放入 Survivor,只好通过分配担保机制提前转移到老年代中

7.2.大对象(长字符串或长数组等需要大量连续空间的对象)直接进入老年代(防止大

对象在 eden和 Survivor中经常复制)通过-XX:PretenureSizeThreshold 参数设置 (如 3MB),大于这个参数的直接进入老年代

7.3.长期存活对象进入老年代(默认 15岁)

Minor GC:新对象先放入 eden区,当 eden满了会触发 Minor GC。

Full GC(等于 Major GC):

7.3.1、每次进行 Minor GC 时,JVM 会计算 Survivor 区移至老年区的对象的平均大小,如 果这个值大于老年区的剩余值大小则进行一次 Full GC

7.3.2、老年代空间不足时触发 Full GC,只有在新生代对象转入或创建为大对象、大数组 时才会出现不足的现象(大对象直接进入老年代),分配担保

7.3.3、永久代满(永久代 JDK8被移除)

优化 Full GC 本身不会先进行 Minor GC,我们可以配置,让 Full GC 之前先进行一次 Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次 Minor GC可以提高老年代 GC 的速度。

在 jvm分带垃圾回收机制中,将应用程序可用的堆空间分为年轻代和老年代,又将年轻 代分为 eden区、from区、to 区,新建对象总是在 eden 区中被创建,当 eden区空间已满,

就触发一次 Minor gc,将还被使用的对象复制到 from 区,这样整个 eden 区都是未被使用 的空间,可供继续创建对象,当 eden区再次用完,再触发一次 Minor gc,将 eden 区和from 区还在被使用的对象复制到 to 区,下一次 Minor gc则是将 eden 区和 to区还被使用的对象 复制到 from区。因此,经过多次 Minor gc,某些对象会在 from区和 to 区多次复制,如果 超过某个阈值对象还未被释放,则将对象复制到老年代。如果老年代空间也已用完,那么就 会触发 full gc,即所谓的全量回收。

永久代的垃圾回收主要有两部分:废弃常量和无用的类。如没有任何 String对象引用 “abc”。在大量使用反射、动态代理、CGlib等 ByteCode框架,动态生成 JSP 以及 OSGi这 类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能(回收永久代),以保证永 久代不会溢出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值