写点什么

必知必会 JVM 三 - 面试必备,JVM 堆内存详解

  • 2021 年 11 月 12 日
  • 本文字数:2963 字

    阅读完需:约 10 分钟

1.1?堆内存区域介绍





在 jvm 的堆内存中有三个区域:


  1. 年轻代:用于存放新产生的对象。

  2. 老年代:用于存放被长期引用的对象。

  3. 持久带:用于存放 Class,method 元信息(1.8 之后改为元空间)。


年轻代


年轻代中包含两个区:Eden 和 survivor,并且用于存储新产生的对象,其中有两个 survivor 区


老年代


年轻代在垃圾回收多次都没有被 GC 回收的时候就会被放到老年代,以及一些大的对象(比如缓存,这里的缓存是弱引用),这些大对象可以不进入年轻代就直接进入老年代


持久代


持久代用来存储 class,method 元信息,大小配置和项目规模,类和方法的数量有关。


元空间


JDK1.8 之后,取消 perm 永久代,转而用元空间代替


元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。并且可以动态扩容。那么使用元空间会有哪些问题呢?同学们可以思考下。


1.2 为什么分代?




因为不同对象的生命周期是不一样的。80%-98%的对象都是“朝生夕死”,生命周期很短,大部分新对象都在年轻代,可以很高效地进行回收,不用遍历所有对象。而老年代对象生命周期一般很长,每次可能只回收一小部分内存,回收效率很低。


年轻代和老年代的内存回收算法完全不同,因为年轻代存活的对象很少,标记清楚再压缩的效率很低,所以采用复制算法将存活对象移到 survivor 区,更高效。而老年代则相反,存活对象的变动很少,所以采用标记清楚压缩算法更合适。


1.3 内存分配策略



1.3.1、 优先在 Eden 区分配?

在大多数情况下, 对象在新生代 Eden 区中分配, 当 Eden 区没有足够空间分配时, VM 发起一次 Minor GC, 将 Eden 区和其中一块 Survivor 区内尚存活的对象放入另一块 Survivor 区域, 如果在 Minor GC 期间发现新生代存活对象无法放入空闲的 Survivor 区, 则会通过空间分配担保机制使对象提前进入老年代(空间分配担保见下).

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

Serial 和 ParNew 两款收集器提供了-XX:PretenureSizeThreshold 的参数, 令大于该值的大对象直接在老年代分配, 这样做的目的是避免在 Eden 区和 Survivor 区之间产生大量的内存复制(大对象一般指 需要大量连续内存的 Java 对象, 如很长的字符串和数组), 因此大对象容易导致还有不少空闲内存就提前触发 GC 以获取足够的连续空间.

**1.3.**3、长期存活对象进入老年区

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(默认为 15)_时,就会被晋升到老年代中。


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码

**1.3.**4、对象年龄动态判定

如果在 Survivor 空间中相同年龄所有对象大小的综合大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代

**1.3.**5、空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的,如果担保失败则会进行一次 Full GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。


HotSpot 默认是开启空间分配担保的。


二、GC 执行的机制


=========


由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC 有两种类型:Minor GC 和 Full GC。


2.1 Minor GC(young GC)




一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Minor GC,对 Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。


2.2 Full GC




对整个堆进行整理,包括 Young、Tenured 和 Perm。Full GC 因为需要对整个堆进行回收,所以比 Minor GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对于 FullGC 的调节。有如下原因可能导致 Full GC:


1.年老代(Tenured)被写满


2.持久代(Perm)被写满


3.System.gc()被显示调用


4.上一次 GC 之后 Heap 的各域分配策略动态变化


2.3?对象生死判定方法




那我们了解 JVM 的 GC 机制之后,那满足什么条件的对象才会被 GC 掉呢?


1、引用计数:每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减 1,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。


2、可达性分析算法



在主流商用语言(如 Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图: Object5、6、7 虽然互有关联, 但它们到 GC Roots 是不可达的, 因此也会被判定为可回收的对象:


在 Java, 可作为 GC Roots 的对象包括:


  1. 方法区: 类静态属性引用的对象;

  2. 方法区: 常量引用的对象;

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

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


注: 即使在可达性分析算法中不可达的对象, VM 也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与 GC Roots 相连接的引用链, 第二次是 GC 对在 F-Queue 执行队列中的对象进行的小规模标记(对象需要覆盖 finalize()方法且没被调用过).


三、GC 原理-垃圾回收算法


=============


Java 与 C++等语言最大的技术区别:自动化的垃圾回收机制(GC),那么为什么要了解 GC 和内存分配策略呢?


  • 面试需要

  • GC 对应用的性能是有影响的;

  • 写代码有好处


栈:栈中的生命周期是跟随线程,所以一般不需要关注


堆:堆中的对象是垃圾回收的重点


方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点


目前为止,jvm 已经发展处四种比较成熟的垃圾收集算法:


  1. 标记-清除算法;

  2. 复制算法;

  3. 标记-整理算法;

  4. 分代收集算法


3.1 标记-清除算法





这种垃圾回收一次回收分为两个阶段:标记、清除。首先标记所有需要回收的对象,在标记完成后回收所有被标记的对象。这种回收算法会产生大量不连续的内存碎片,当要频繁分配一个大对象时,jvm 在新生代中找不到足够大的连续的内存块,会导致 jvm 频繁进行内存回收(目前有机制,对大对象,直接分配到老年代中)



优点


  • 利用率百分之百


缺点


  • 标记和清除的效率都不高(比对复制算法)

  • 会产生大量的不连续的内存碎片


3.2 复制算法





这种算法会将内存划分为两个相等的块,每次只使用其中一块。当这块内存不够使用时,就将还存活的对象复制到另一块内存中,然后把这块内存一次清理掉。这样做的效率比较高,也避免了内存碎片。但是这样内存的可使用空间减半,是个不小的损失。


优点


  • 简单高效,不会出现内存碎片问题


缺点


  • 内存利用率低,只有一半

  • 存活对象较多时效率明显会降低



3.3 标记-整理算法




评论

发布
暂无评论
必知必会JVM三-面试必备,JVM堆内存详解