您当前的位置:首页 > 计算机 > 编程开发 > Java

深入理解 Java 虚拟机

时间:12-14来源:作者:点击数:
城东书院 www.cdsy.xyz

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)变差(相关变量散开了)。

广度优先搜索法一般无回溯操作,即入栈和出栈的操作,所以运行速度比深度优先搜索算法法要快些。

深度优先搜索法占内存少但速度较慢,广度优先搜索算法占内存多但速度较快。

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

城东书院 www.cdsy.xyz
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐