当前位置:   article > 正文

《深入理解Java虚拟机》2:垃圾收集器与内存分配策略_jvm from space to space

jvm from space to space

一、常见问题

  • 如何判断对象是否死亡(两种方法)。
  • 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
  • 如何判断一个常量是废弃常量
  • 如何判断一个类是无用的类
  • 垃圾收集有哪些算法,各自的特点?
  • HotSpot 为什么要分为新生代和老年代?
  • 常见的垃圾回收器有那些?
  • 介绍一下 CMS,G1 收集器。
  • Minor Gc 和 Full GC 有什么不同呢?
  • 什么是垃圾回收机制:JVM不定时回收不可达对象(自动)
  • 什么是不可达对象?
  • finalize和finally区别?finalize()方法的作用:垃圾回收机制之前会先执行的方法,用于垃圾收集器删除这个对象之前对这个对象进行调用的。在Object中定义的所有类都继承它,子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。

 

二、JVM内存分配与回收

2.1、堆空间的基本结构 与 内存分配回收大体流程

(1)先了解堆空间的基本结构

于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor(s0)、To Survivor (s1)空间等(新生代s0和s1大小相等,使用复制算法,复制交换)。

 

(2)分配与回收大体流程

对象首先在eden区分配内存,在一次新生代垃圾回收后如果对象还存活,就会进入To Survivor区,并且对象年龄还会加1(Enen区——>Survivor区后对象的初始年龄变为1),当对象的年龄增加到一定程度(默认15岁),就会被晋升到老年代中。Eden区和 From Survivor区被清空时,From Suvivor和To Suvivor角色交换,不管怎么样保证名为To 的Suvivor区域是空的,GC会重复这过程直到To 区被填满,会将所有对象移动到老年代

2.2、堆内存常见分配策略

(1)对象优先在eden区分配

大多数情况下对象在eden区分配,eden区空间不够时虚拟机将发起一次GC。

先了解Minor GC(新生代GC)和Full GC/Major GC(老年代GC)有什么不同?

  • 新生代GC:指发生在新生代的垃圾收集动作,频繁且快速;
  • 老年代GC:指发生在老年代的垃圾收集动作,出现了Full GC/Major GC经常会伴随至少一次Minor GC,老年代GC一般比新生代GC慢10倍以上。

测试:

  1. public class GCTest {
  2. public static void main(String[] args) {
  3. byte[] allocation1, allocation2;
  4. allocation1 = new byte[30900*1024];
  5. //allocation2 = new byte[900*1024];
  6. }
  7. }

通过以下方式运行:

 

添加的参数:-XX:+PrintGCDetails 

运行结果:

       从下图我们可以看出 eden 区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用 2000 多 k 内存),from space和to space都是使用率为0:

 

假如我们再为 allocation2 分配内存会出现什么情况呢? 

allocation2 = new byte[900*1024];

 from space和to space开始使用,并且老年代也开始使用

为什么会出现这种情况?

因为给allocation2 分配内存时eden区内存几乎已经被分配完。Eden区空间不够时虚拟机发起一次Minor GC期间虚拟机又发现allocation1 无法存入Survivor (from space和to space)空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代,老年代空间足够存放allocation1,所以不会出现Full GC/Major GC。

执行完 Minor GC 后,后面的分配对象如果还能够存放在eden区还是会在eden分配内存

2.3、大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如字符串、数组,都在老年代分配内存)。为了避免大对象分配内存时由于分配担保机制带来的复制而降低效率。

2.4、长期存活的对象进入老年代

虚拟机给每个对象一个年龄计数器,如果对象在Eden出生并经过一次Minor GC后仍然能存活,而且能被Survivor容纳的话,将被移动到Survivor空间,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。所以长期存活对象会进入老年代。

动态对象年龄判定:

如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间一半,年龄大于或者等于该年龄的对象可直接进入老年代

三、对象死亡 

垃圾回收第一步就是要判断哪些对象已死亡

3.1、如何判断一个对象已经无效  / 判断对象存活

(1)引用计数器法(不常用,已淘汰)

(因循环依赖(引用)问题,该方法已经被淘汰)

就是给对象添加一个引用计数器,当有一个地方引用它计数器就加1;当引用无效计数器就减1;任何时候计数器为0的对象就是不可再用的对象。原理就是:每个对象有一个年龄,如果小于或者等于15岁,存放在新生代,大于15岁存放在老年代;GC线程不定时回收时,如果对象被引用,年龄会加1,如果没有被引用继续回收年龄会减1;如果年龄为0,垃圾回收器会认为该对象是不可达对象,会被清理掉。

这个方法简单效率也高,但主流虚拟机中并没有选择这个算法来管理内存,因为其很难解决对象之间引用问题,如下代码演示:

  1. /**除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是 GC 无法回收
  2. */
  3. public class ReferenceCountingGc {
  4. Object instance = null;
  5. public static void main(String[] args) {
  6. ReferenceCountingGc objA = new ReferenceCountingGc();
  7. ReferenceCountingGc objB = new ReferenceCountingGc();
  8. objA.instance = objB;
  9. objB.instance = objA;
  10. objA = null;
  11. objB = null;
  12. }
  13. }

2) 可达性分析算法(根搜索法)

基本思想就是一系列称为“GCRoots的对象”作为起点,从这些节点开始写向下搜索,节点所走过的路称为引用链,当一个对象到GC Roots没有任何引用链相连接的话,就证明该对象是不可用的+

注:什么是不可达对象?  答:相当于对象没被引用 / 对象没有继续使用 / 没有存活

可以作为GCRoots的对象包括下面几种:

(1)虚拟机栈(栈中的局部变量区)中引用的对象

(2)方法区中 静态属性 引用的对象

(3)方法区中 常量 引用的对象 

(4)本地方法栈中JNI(Native方法)引用的对象

3.2、对象是否死亡也与 “引用” 有关 

无论通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象引用链是否可达,判断对象的存活都与引用有关。

(1)强引用

如果一个对象具有强引用,类似生活必不可少的用品,垃圾回收器不会回收它。当内存不足虚拟机宁愿抛出OutOfMemoryError (内存溢出)错误,使程序异常终止也不会回收它。

(2)软引用(用的最多)

如果一个对象具有软引用,类似可有可无生活用品。如果内存空间足够垃圾回收期不会回收它,如果内存空间不足就会回收。软引用可以用来实现内存敏感的高速缓存

(3)弱引用

也是类似于可有可无生活用品。和软引用区别在于:只有弱引用的对象具有更短暂的生命周期,垃圾回收器一发现就回收。不过垃圾回收器优先级低不一定很快发现弱引用对象。

(4)虚引用

与其他三种引用都不同,它不会决定对象的生命周期,形同虚设。随时可能被回收。主要用来跟踪对象被垃圾回收的活动。

虚引用和弱引用的区别

虚引用必须和引用队列(ReferenceQueue)一起使用。程序可以通过判断引用队列中是否有虚引用来决定对象是否要被垃圾回收。

3.3、如何判断常量“死亡” / 无用常量

运行时常量池( JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆中开辟了一块区域存放运行时常量池)主要回收的是废弃常量。

以字符串常量为例:假如常量池中存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,说明该常量是废弃常量,如果发生内存回收并且有必要“abc”就会被回收。总的来说,就是常量池中没被引用的就是废弃常量就会死亡

3.4、如何判断类“死亡”/ 无用类

方法区主要回收的就是无用的类。

需要同时满足如下三个条件:

(1)该类的所有实例都已经被回收,也就是说Java堆中不存在该类的实例。

(2)加载该类的ClassLoader已经被回收。

(3)该类的对象没有被任何地方引用

四、垃圾收集算法

收集算法是内存回收方法论,垃圾收集器是内存回收具体实现

4.1、标记-清除算法(最基础)

一般用于老年代。

该算法分为“标记”和“清除”阶段:首先标记所需要回收的对象,在标记完成后统一回收所有被标记的对象。

它使最基础算法后边都是它的改进,它有俩问题:

  • 效率问题;
  • 空间问题(标记清除后会产生大量不连续的碎片)。

优点:可解决循环引用问题、必要时才回收(内存不足时) ;

缺点:回收时其他线程要挂起、对象多时效率不高、会造成内存碎片化(内存明明有空间,但是不连续导致稍微申请大对象无法做到

4.2、复制算法

一般使用在新生代。

解决了标记清除算法的效率问题。它可以将内存分为大小相同两块from(s0)域和to(s1)域,每次只使用from(s0)域,to(s1)域空闲。当from域内存不够了,开始执行GC操作,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。每次的内存回收都是对内存区间的一半进行回收,效率高。

注意: 万一存活对象数量比较多,那么to域的内存可能不够存放,这个时候会存放在老年代的空间。

优点:性能高、能解决碎片化问题、能解决引用更新问题;

缺点:会造成一部分内存浪费(不过可以调整内存块比例大小) 

案例

  1. user1和user2对象在eden区分配内存,当进行GC回收时,发现这两个对象经常被使用到,所以把这俩对象放在s0区;
  2. 同理user3来了,GC回收时又发现user3经常被用到,所以被放入s0区;
  3. 又当GC回收时,发现user1没有被使用也就是不可达时,会把user2和user3 copy到s1去,然后把整个s0区清除;
  4. user4来了,GC又发现他经常被使用,但是这时user4对象是直接到s1区(s0区此时是没有任何东西的哦);
  5. 当user2和user3没有被使用也就是不可达时,会把user4对象copy到s0区,然后直接清空s1区;
  6. 来回15次。

注意:s0区和s1区一定有一个空的,目的是为了存放下一次复制

4.3、标记-整理算法 

一般使用在老年代。

和标记-清除类似,但是它解决了碎片化问题。后续步骤不是直接回收对象,而是让所有存活对象向一端移动,然后直接清除掉端边界以外的内存。

优点:解决了碎片化问题;

缺点:由于移动了可用对象,需要去更新引用。

4.3、分代收集算法 (当前虚拟机都采用)

根据对象存活周期不同将内存分为几块,一般将Java堆分为 新生代和老年代,可以根据各年代特点再选择合适的垃圾收集算法(整合其他几种算法)。

在新生代中:对象死亡率高,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。

在老年代中:对象存活率高 ,而且没有额外的空间对它进行分配担保,所以必须选择“标记-清除”或者“标记-整理”算法。

注意:垃圾回收不会发生在永久代,但是当永久代满的时候会发生FULL GC完全垃圾回收(JDK1.8已经移除永久代,增加了元空间).

 

五、垃圾收集器

没有最好的收集器,应在不同场景选择不同收集器

5.1、Serial收集器(最基本、最悠久)

新生代采用复制算法,老年代采用标记-整理算法

  • 配置:通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
  • 使用场景:小型应用

单线程收集器,也称串行收集器。“单线程”的意义不仅是它只会使用一条垃圾收集线程去完成垃圾收集工作,而且收集时会暂停所有其他工作线程,直到它收集结束。但是其单线程效率高适合在Client模式下的虚拟机。

Serial Old收集器:

 Serial收集器的老年代版本,同样是单线程收集器。主要俩用途:在JDK1.5及一般版本与Parallel Svavenge收集器搭配使用;另一种是作为CMS收集器的后备方案

5.2、ParNew收集器

新生代采用复制算法,老年代采用标记-整理算法

  • 参数控制:-XX:+UseParNewGC  ParNew收集器
  •                   -XX:ParallelGCThreads 限制线程数量
  • 使用场景:小型应用

其实就是Serial收集器的多线程版本。适合运行在Server模式下的虚拟机,除了Serial收集器外,只有它能与CMS收集器(真正的 并发收集器)配合工作。

5.3、Parallel Scavenge收集器

新生代采用复制算法,老年代采用标记-整理算法

  • 参数控制: XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
  • 使用场景:大型应用、科学计算、数据采集

几乎和ParNew一样。它的关注点是吞吐量,CMS等垃圾收集器关注点更多是用户线程的停顿时间(提高用户体验)。提供了很多参数供用户找到合适的停顿时间与最大吞吐量。

Parallel Old收集器:

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以考虑Parallel Scavenge收集器和Parallel Old收集器。

5.4、CMS收集器(并发)

采用标记-清除算法

  • 参数配置: -XX:+UseConcMarkSweepGC设置
  • 使用场景:高并发、大型服务器等

HotSpot虚拟机真正意义第一款并发收集器,它第一次实现了让垃圾收集器与用户线程同时工作,是一种 以获取最短回收停顿时间为目标 的收集器,注重用户体验

CMS运作过程有如下四步:

(1)初始标记:暂停所有其他线程,并记录下直接与root相连的对象,速度很快。

(2)并发标记:同时开启GC与用户线程(可以一起工作),用一个闭包结构去记录可达对象。但在这个阶段结束这个结构并不能保证包含当前所有的可达对象(因为用户线程可能会不断更新引用域,所以GC线程无法保证可达性分析的实时性)。

(3)重新标记:为了修复并发标记期间因为用户程序继续运行而 导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段时间长,但远远比并发标记阶段时间短。

(4)并发清除:开启用户线程,同时GC线程开始对为标记的区域做清扫。

CMS优点:并发收集、低停顿;

CMS缺点:对CPU资源敏感、无法处理浮动垃圾、使用“标记-清除”算法会导致收集结束会有大量空间碎片产生。

 

5.5、G1收集器

整体采用标记-整理算法,局部采用复制算法

  • 参数设置: -XX:+UseG1GC

是一款面向服务器的收集器。主要针对配备多个处理器以及大容量内存机器,即满足GC停顿时间要求 又满足高吞吐量特性

G1被视为JDK1.7中HotSpot虚拟机进化重要特征,具备以下特点

并行与并发:能充分利用CPU多核环境下硬件优势,使用多个CPU来缩短停顿时间。部分其他的收集器(除了CMS)原本需要停顿Java线程来执行GC动作,G1仍可以通过并发方式让Java程序继续运行。

分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个堆,但还是保留了分代收集的概念。

空间整合:与CMS的“标记-整理”算法不同,G1从整体采用“标记-整理”算法,局部上来看是基于“复制算法”实现。

可预测的停顿这是G1相对于CMS另一个大优势。降低停顿时间是G1和CMS共同关注点,但G1除了追求低停顿外还能建立可预测停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片内。

G1运作有如下四步:

  • (1)初始标记
  • (2)并发标记
  • (3)最终标记
  • (4)筛选回收

G1优点:吸收了CMS优点、支持很大的堆,高吞吐量、堆被划分成 许多个连续的区域(region)

并行与并发区别

并行:多个处理器同时处理多个不同任务(三个人同时吃三个馒头)

并发:一个处理器同时处理多个任务(一个人同时吃三个馒头)

 

上一篇:Java内存区域与内存溢出异常

下一篇:虚拟机性能监控与故常处理

  参考资料:深入理解Java虚拟机(第2版) : JVM高级特性与最佳实

 

### 若对你有帮助的话,欢迎点赞!评论!转发!谢谢!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/352883
推荐阅读
相关标签
  

闽ICP备14008679号