深入理解 Java 虚拟机

发布于 2024-10-04 01:13:06 字数 6226 浏览 37 评论 0

JVM 堆内存分布

JVM 堆内存分布主要可以分为几个区域,每个区域有其特定的用途和特点:

1. 堆内存结构

JVM 堆内存通常分为以下几个主要区域:

  • 年轻代(Young Generation) :用于存放新创建的对象,通常又分为三个部分:
    • Eden 区 :新对象的默认分配区域。
    • Survivor 区(S0 和 S1) :用于存放经过一次垃圾回收后仍然存活的对象。Survivor 区之间会轮流使用。
  • 老年代(Old Generation) :用于存放经历多次垃圾回收仍然存活的对象。这个区域的对象通常较大且生命周期较长。
  • 永久代(PermGen)或元空间(Metaspace) :存放类的元数据(如类的结构、方法、常量池等)。在 Java 8 及之后,永久代被元空间取代,元空间使用本地内存,不再受限于堆内存。

2. 内存配置

JVM 堆内存的大小可以通过启动参数进行配置:

  • -Xms :设置初始堆内存大小。
  • -Xmx :设置最大堆内存大小。

例如, -Xms512m -Xmx1024m 表示初始堆内存为 512MB,最大堆内存为 1024MB。

3. 垃圾回收策略

不同的垃圾回收器会采用不同的策略来回收堆内存:

  • Serial GC :适合小型应用,使用单线程进行垃圾回收。
  • Parallel GC :使用多线程进行年轻代的垃圾回收,适合多核处理器。
  • CMS(Concurrent Mark-Sweep) :尝试减少停顿时间,通过并发标记和清除。
  • G1(Garbage-First) :以垃圾回收效率为目标,适合大内存应用,能够在可控的停顿时间内进行回收。

4. 堆内存的使用和监控

可以使用工具如 JVisualVM、JConsole 和各种 Profiler 来监控 JVM 堆内存的使用情况,分析内存泄漏和性能问题。

希望这些信息能帮助你更好地理解 JVM 堆内存的分布!如果有特定的方面想深入了解,随时告诉我!

JVM 堆内存分配策略

分新生代、老年代、永久代(jdk 1.8 之后替换成了 Metaspace)

  • 新生代 【8:1:1】
    • Eden 区+Survivor1 区+Survivor2 区,Eden 和 Survivor 默认 8:1,两块 Survivor 一样大(读完本书可以知道是因为新生代多采取复制清除 GC 算法)
    • 对象优先在 Eden 分配
  • 老年代 【新生代:老年代=1:2】
    • 大对象直接放入老年代
    • 长期存活的对象晋入老年代

      动态对象年龄判断:对象最新进入 Eden 区,进过第一次 MinorGC 后如果该对象任然存活,移入 Survivor 区,并维护一个年龄计数器,设为 1。后续如果再次经过一次 MinorGC,还没死亡的话年龄计数器自增 1,如果年龄超过 15,自动晋入老年代。或者相同年龄的对象超过 Survivor 内存区域一般时,也会自动把这些对象移入老年代

  • 永久代在 jdk 1.8 中被 Metaspace 元空间取代

    永久代也是会发生垃圾回收的,但有三点条件:

    1. 该类的实例都被回收。
    2. 加载该类的 classLoader 已经被回收
    3. 该类不能通过反射访问到其方法,而且该类的 java.lang.class 没有被引用

    满足这三个条件可以回收,但回不回收还得看 jvm,但很少说永久代垃圾回收,垃圾回收侧重新生代、永久代

堆内存各区域比例及相关控制参数

总的来说,堆可分为新生代+老年代,可由参数**–Xms(初始堆大小)、-Xmx(最大堆大小) 来控制大小,新生代大小由参数 -Xmn 控制,新生代三块分区比例可由 –XX:SurvivorRatio 控制,新生代和老年代比例可由参数 –XX:NewRatio**控制

如何判断对象已死亡?

引用计数法

最容易想到的就是这种,每有一个引用指向对象,引用计数器就加一,引用失效就减一。一旦引用计数器为 0,表明对象已死亡,可回收。

问题是如果两个对象相互引用,引用计数器始终不会为 0

可达性分析算法

提出一个 GC Roots 对象的概念,GC Roots 为起点向下搜索,所走过的路径为引用链,如果没有 GC Roots 对象到该对象的引用链,表明该对象可回收

  • GC Roots 对象包含哪些?
    • JVM 虚拟机栈中引用的对象(栈帧是对象调用方法时生成的,具体说是局部变量表中引用的对象)
    • 本地方法栈中的 JNI 引用的对象(Java Native Interface 缩写,Native 方法是 jav 调用其他语言的方法,多用来提高效率、和底层硬件、小做系统交互)
    • 方法区中静态变量、常量引用的对象

img

如图,像对象实例 1、2、4、6 是 GC Roots 可达的,3 和 5 是可被回收的

这里有个问题是,搜索时是采用广度优先还是深度优先搜索?搞不清

什么时候会进行垃圾回收 GC?

垃圾回收不能像 C++似的手动进行,java 中是由 JVM 自动判断进行 GC 的,但 System.gc() 可以提示进行 GC

  • 新生代内存空间不足时,会进行 Minor GC
  • 长期存活的对象由新生代晋升入老年代时,老年代内存空间不足的话也会进行 full GC

看下对象的引用

引用分为四种:强引用、软引用、弱引用、虚引用

  • 强引用:强引用存在的话,即使内存不够,也不会对强引用对象回收。 P p = new P() 就是强引用
  • 软引用:内存空间足够时,不会回收;不足时才会对其回收。 可用来实现内存敏感的高速缓存
  • 弱引用:垃圾回收器扫描到弱引用对象时就会对其回收,无论内存空间足不足
  • 虚引用:“虚”字可知,表明该引用可有可无。该虚引用对象被垃圾回收器回收时候,系统能够接收到一个通知

GC 算法有哪些?

  • 标志-清除法
    • 适合老年代
    • 问题:标记和清除效率不高、内存空间碎片化的问题
  • 标志-整理法
    • 适合老年代
    • 也是为了解决碎片问题,标志同上,但会将所有存活的对象向一端移动,再回收
  • 复制清除法
    • 适合新生代
    • 两块一样大小的内存,每次用一块,这也是它的缺点(内存只使用一半)
    • 每次只对一块内存空间回收,效率高,不会出现空间碎片
  • 分代收集法 【用的最多的 GC 算法】
    • 根据新生代和老年代对象的性质,采取新生代使用复制清除法,老年代使用标志-清除/整理法
    • 新生代会有大量对象死亡,所以 Minor GC 发生频率高,而老年代中对象存活率高,GC 没那么频繁

垃圾回收器

垃圾回收器(Garbage Collector,GC)是 JVM 的重要组成部分,负责自动管理内存,回收不再使用的对象,以防止内存泄漏和优化性能。以下是几种主要的垃圾回收器及其特点:

1. Serial GC

  • 描述 :单线程垃圾回收器,适用于小型应用。
  • 特点 :简单,易于实现。停顿时间较长,因为它在回收过程中会暂停所有应用线程。

2. Parallel GC

  • 描述 :多线程垃圾回收器,适用于多核处理器。
  • 特点 :在年轻代的回收中使用多个线程,提高回收效率。通过使用多个线程缩短停顿时间。

3. CMS (Concurrent Mark-Sweep) GC

  • 描述 :旨在减少停顿时间的垃圾回收器。
  • 特点
    • 使用并发标记和清除的方式,允许应用线程在大部分回收过程中继续运行。
    • 在标记阶段,标记所有存活的对象;在清除阶段,清除未标记的对象。
    • 停顿时间较短,但可能导致碎片问题。

4. G1 (Garbage-First) GC

  • 描述 :适用于大内存应用的低延迟垃圾回收器。
  • 特点
    • 将堆分成多个小块(Region),并优先回收垃圾最多的区域。
    • 通过控制停顿时间,优化应用性能。
    • 支持并行回收和并发标记。

5. ZGC (Z Garbage Collector)

  • 描述 :低延迟垃圾回收器,适合大内存应用。
  • 特点
    • 支持大规模堆内存(可达数 TB)。
    • 在垃圾回收过程中,几乎不产生停顿时间,停顿时间通常在毫秒级。

6. Shenandoah GC

  • 描述 :类似于 ZGC 的低延迟垃圾回收器。
  • 特点
    • 采用并发的方式进行标记和清除,降低停顿时间。
    • 适合大内存应用,能够实现较短的停顿时间。

GC 策略选择

选择合适的垃圾回收器取决于具体应用的需求,通常需要在停顿时间、吞吐量和内存占用之间进行权衡。

监控和调优

可以使用 JVM 参数和工具来监控和调优垃圾回收器的行为,例如使用 -XX:+PrintGC 打印 GC 日志,或者使用工具如 VisualVM 和 JConsole 分析内存使用情况。

如果你对某个特定的垃圾回收器或调优策略感兴趣,可以进一步深入讨论!

补充

看到一篇博客提到了死亡对象如何搜索 BFS、DFS,有点兴趣, 见此

为什么新生代用广度搜索,老生代用深度搜索?

深度优先 DFS 一般采用递归方式实现,处理 tracing 的时候,可能会导致栈空间溢出,所以一般采用广度优先来实现 tracing(递归情况下容易爆栈)。
广度优先的拷贝顺序使得 GC 后对象的空间局部性(memory locality)变差(相关变量散开了)。
广度优先搜索法一般无回溯操作,即入栈和出栈的操作,所以运行速度比深度优先搜索算法法要快些。
深度优先搜索法占内存少但速度较慢,广度优先搜索算法占内存多但速度较快。

结合深搜和广搜的实现,以及新生代移动数量小,老生代数量大的情况,我们可以得到了解答。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

眼趣

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

漫雪独思

文章 0 评论 0

垂暮老矣

文章 0 评论 0

鹊巢

文章 0 评论 0

萌酱

文章 0 评论 0

雨说

文章 0 评论 0

冰葑

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文