Java:JVM基础——堆(Java8)

1、堆的介绍

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

JDk7将字符串常量池移到堆中,JDK8的方法区改为元空间,并放入直接内存中,但是此时的字符串常量池还在堆中。
也就是堆中主要存储:①Java实例对象、②字符串常量池、③静态变量(final关键字不影响对象的内存位置)

堆在结构上主要分成几部分:

  • 新生代:主要存储年龄比较小的对象
    • Eden区:刚出生的对象放在伊甸区
    • Survivor区:幸存者区,分成等量的两部分s0 和 s1,在复制交换算法中,对象源区域叫From区,对象目标区域叫To区。
      • From区
      • To区
  • 老年代:超过一定年龄或者触发一定条件放入的对象

一般情况下,对象首先在 Eden 区分配,经历一次新生代垃圾回收之后,存活的对象被放入 Survivor区的From区(注意:From区和To区是来回变换的,并不是确定的),此时对象的年龄加1,年龄变为1(Eden是0),当对象经过GC后,年龄到一定程度(Serial Old默认15岁,CMS默认6岁)或触发一定条件,就会被放置在老年代。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

上述触发的一定条件是:

  1. Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。
  2. 大对象直接分配在老年代,避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

2、Java对象的创建过程

一个Java对象的创建过程:

类加载检查 -> 开辟内存空间 -> 初始化零值 -> 设置对象头 -> 执行init方法

  1. 类加载检查:JVM 遇到 new 之后去检查这个类是否已经被加载、链接和初始化过。如果没有,要先去执行类加载过程。
  2. 开辟内存空间:接下来JVM会为这个对象开辟一块内存空间,对象需要的内存空间大小在类加载完成后就能确定。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
  3. 初始化零值:之后将对象所属的内存空间中的数据初始化零值,保证字段不赋值也能直接使用。
  4. 设置对象头:之后要设置该对象的对象头,对象头中的类型指针记录该类的元数据信息,MarkWord记录该实例对象的GC分代年龄、是否启用偏向锁等信息。(打算之后单独介绍对象头)
  5. 执行init方法:最后执行<init>方法,将数据初始化为程序设定的初始值。此时一个Java对象才算真正创建出来。

3、判断对象是够存活

3.1 引用计数法

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

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

首先判断GC Root对象,然后从GC Root出发,判断所有可以到达的对象,走过的路径叫做引用链,当一个对象没有任何GC Root 通过引用链连接时,则说明该对象不可达,也就代表可以回收。

红色的对象为不可达对象,GC可以回收。其他的是仍然存活的对象。

3.2.1 GC Root对象有哪些?

也是面试被问到的问题。

  1. 虚拟机栈中的变量
  2. 静态变量
  3. 常量池
  4. 本地方法栈中引用的对象

引用类型

两个算法都涉及了对象的引用,之前有面试被问到,Java的引用有几种?

3.X.1 强引用

我们使用的基本都是强引用,强引用的对象 GC 不会回收,哪怕触发 OutOfMemoryError 也不会回收。

3.x.2 软引用

如果内存空间满足,就不会回收它,如果内存空间不足,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.x.3 弱引用

弱引用比软引用还要脆弱,只要垃圾回收器扫描到它,就会被回收。由于GC线程是一个优先级很低的线程,所以不一定很快发现弱引用对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

3.x.4 虚引用

虚引用和没有引用差不多,因为随时会被回收。

虚引用的作用主要是跟踪对象被垃圾回收的活动。

虚引用必须和引用队列(ReferenceQueue)联合使用。垃圾收集器在准备回收该对象时会先将这个虚引用放入对应的引用队列中,可以通过判断队列中是否加入了虚引用,判断这个对象是否将要被回收,可以在回收前做出一定措施。

4、垃圾收集的算法

4.1 复制交换算法

主要应用于年轻代。它将内存分成两块相等大小的区域,数据只存在于其中一块。当这块内存不足之后,把还存活的对象移动整理到另一块区域,然后清空之前存数据的这块内存区域。

内存整理前:

内存整理后:

4.2 标记-清除算法

算法有两个阶段,标记、清除。

首先标记所有的存活对象,标记完成后清除没有被标记的对象。

缺点:

  1. 这种算法很明显会产生大量不连续的碎片。需要维护一个空闲列表
  2. 效率不高
  3. 在进行GC的时候,需要停止整个应用程序,导致用户体验差(这个我觉得不太算,STW都会有,CMS的标记还分了三个阶段呢)

4.3 标记-整理算法

标记-整理算法的标记阶段和标记-清除算法一致,后续的清理阶段是将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

3、垃圾收集器

这里需要着重提一句,如果查过 Java8 默认垃圾收集器,你会发现两种说法,Parallel Scavenge + Serial Old 和 Parallel Scavenge + Parallel Old。

这里不卖关子,先说结论,实际上是 Parallel Scavenge + Parallel Old

在《深入理解 Java 虚拟机》第三版第 128 页中提到 JDK 9 之前,Server 默认使用 Parallel Scavenge + Serial Old(PS MarkSweep)。但是为什么还有另一种说法呢?来看看。

先使用命令看下 JVM 的默认参数
java -XX:+PrintCommandLineFlags -version

在输出中有这么一个参数
-XX:+UseParallelGC

下面是每个参数对应的回收器的类型:

参数 新生代(别名) 老年代
-XX:+UseSerialGC Serial (DefNew) Serial Old (PSOldGen)
-XX:+UseParallelGC Parallel Scavenge (PSYoungGen) Serial Old (PSOldGen)
-XX:+UseParallelOldGC Parallel Scavenge (PSYoungGen) Parallel Old (ParOldGen)
-XX:+UseParNewGC ParNew (ParNew) Serial Old (PSOldGen)
-XX:+UseConcMarkSweepGC ParNew (ParNew) CMS + Serial Old (PSOldGen)
-XX:+UseG1GC G1 G1

按照这个表格来说,应该是 Parallel Scavenge + Serial Old 啊?继续看。

JDK8官网有这么一句话:

Parallel compaction is enabled by default if the option -XX:+UseParallelGC has been specified. The option to turn it off is -XX:-UseParallelOldGC.
大概意思是:
如果已指定选项-XX:+UseParallelGC,则默认情况下启用并行压缩。关闭它的选项是-XX:-UseParallelOldGC。

在 JDK 7U4 之前确实 UserParallelGC 用的就是 Serial Old,在这个版本之后 Parallel Old 已经很成熟了,所以直接替换了旧的收集器,所以 JDK 7u4 以后的 7 和 JDK 8 老年代默认使用的都是 Parallel Old 收集器,只是书中没有更新这个细节。

这里有记载:

Server-class machine ergonomics was introduced in jdk5. If the machine upon which
the jvm is running is powerful enough (currently, at least 2 physical cores plus
at least 2gb of memory), the server jvm is invoked using the parallel scavenger
rather than the serial scavenger. Currently the old gen collector used is
serial mark-sweep-compact. Now that the parallel old gen collector is mature,
we should change to using it instead.
Issue Links

3.1 年轻代

3.1.1 Serial

Serial 是最基本、历史最悠久的垃圾收集器。这是一个单线程收集器,这里的“单线程”不是说用单出一个线程去执行,而是停止所有工作线程(Stop The World),等待它收集完成。

目前这个收集器已经被淘汰,因为它

  • 相比于其他收集器效率太差(这里的效率是总体来说,如果单说内存回收,它就简单高效)
  • 适合单核CPU
  • 小内存(几十MB)场景

3.1.2 ParNew

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。

  • STW时间短,次数多
  • 几个G内存场景
  • 适合与用户交互的场景(因为STW短)
  • 与CMS收集器配合工作

3.1.3 Parallel Scavenge

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。

  • Java8默认年轻代垃圾收集器
  • 吞吐量优先
  • STW时间较长,GC次数少
  • 适合后台计算场景

3.2 老年代

3.2.1 Serial Old

Serial 收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。它主要有两大用途:一种用途是在 JDK以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

  • 单线程整体效率差

3.2.2 Parallel Old

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

3.2.3 CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

使用的“标记-清除”算法,整个过程大致分成四个步骤:

  1. 初始标记: 暂停所有的其他线程,并记录下直接与 GCroot 相连的对象,速度很快;
  2. 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。(此时会产生浮动垃圾标记失败问题)
  3. 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  4. 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

缺点是:

  • 降低用户线程CPU效率(并发过程占用用户线程)
  • 产生浮动垃圾
  • 生成大量碎片(可搭配Serial Old)
  • JDK14被弃用

3.3 不分代收集器

3.1 G1

PS:这个我想详细写一写,这里简单带过,之后单独写一下

G1收集器是JDK9开始默认的垃圾收集器(年轻代+老年代),是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

  • 多线程并发标记,并发回收。
  • 逻辑分代,物理分区(Region,大小为1/2/4/8/16/32MB)
  • 极短GC时间,单次STW默认最多200ms
  • 支持上百G内存GC

年轻代的 Minor GC触发条件:新生代占据整个堆的60%。
新生代+老年代的 Mixed GC触发条件:老年代超过堆的45%。

Mixed GC过程:

  1. 初始标记:只标记GCRoot相连的第一层对象。
  2. 并发标记:建立引用链,分析标记垃圾(产生浮动垃圾,标记失败)
  3. 最终标记:确认所有垃圾,解决浮动垃圾和标记失败问题
  4. 筛选回收:开辟一块最多5%堆空间的内存,用于标记-压缩的数据交换,过程中产生STW。200ms内最多回收10%垃圾最多的区域,回收后检查老年代是否低于45%,未达标就再次进行Mixed GC,最多8次未达标后触发Full GC(Serial Old)

3.2 ZGC 和 Shenandoah

STW时间大幅降低,<10ms 的STW时间。

ZGC是Oracle JDK11出现的实验性GC,用“染色指针”实现对象引用。

Shenandoah是Open JDK12出现的实验性GC,利用“读屏障”和“转发指针”实现对象的动态引用。

posted @ 2022-04-14 21:55  钢板意志  阅读(592)  评论(0编辑  收藏  举报