JVM成神之路:内存分配和垃圾回收

1. 对象的访问方式

Java 通过栈上的引用(reference)来操作堆中的对象。主流的对象访问方式有两种:

  1. 句柄:JAVA堆中划分一块内存作为句柄池,引用中存储句柄地址,句柄包含对象实例和类型数据的地址。优点:在 GC 过程中,对象移动只需修改句柄中的实例数据地址,引用不变。
  2. 直接指针:引用直接存储对象的地址,访问速度更快,节省一次指针定位的时间。HotSpot 虚拟机主要使用直接指针。

2. Java 引用类型

Java 引用按强度分为四种:

  1. 强引用:最常见的引用,GC 不会回收。
  2. 软引用:用于描述非必需对象,在内存不足时回收,适合缓存使用。
  3. 弱引用:也用于描述非必需对象,在垃圾回收时会被回收,适合缓存短生命周期对象。
  4. 虚引用:最弱的引用,无法通过它访问对象。仅用于在对象回收时收到通知,必须与引用队列联合使用。

3. 如何判断对象是否是垃圾?

  1. 引用计数法:给对象添加一个引用计数器,引用增加时计数器加1,引用失效时计数器减1。如果计数器为0,则对象被认为是垃圾。此方法简单高效,但 Java 中很少使用,因为它无法处理对象之间的循环引用问题。
  2. 可达性分析:通过一组被称为 GC Roots 的根对象作为起点,从这些节点开始向下搜索引用关系,形成引用链。如果一个对象没有从 GC Roots 开始的任何引用链与之相连,则被视为垃圾。GC Roots 包括虚拟机栈、本地方法栈、类的静态属性、常量池中的引用等。

4. 可作为 GC Roots 的对象

  1. 虚拟机栈中的引用对象:例如各个线程调用方法堆栈中使用的参数、局部变量、临时变量等。
  2. 方法区中类静态属性引用的对象:例如 Java 类的静态变量。
  3. 方法区中常量引用的对象:如字符串常量池中的字符串。
  4. 本地方法栈中 Native 方法引用的对象
  5. Java 虚拟机内部引用的对象:包括基本数据类型对应的 Class 对象、常驻异常对象(如 NullPointerExceptionOutOfMemoryError)、系统类加载器等。
  6. 被同步锁(synchronized 关键字)持有的对象

5. GC 算法

  1. 标记-清除算法 (Mark-Sweep Algorithm)

    • 过程:分为“标记”和“清除”两个阶段。首先从 GC Roots 出发,标记所有有引用关系的对象;然后清除所有没有标记的对象。
    • 优点:实现简单,不需要移动对象。
    • 缺点执行效率不稳定:如果堆中有大量对象且大部分需要回收,则会进行大量的标记和清除操作,效率会降低。内存碎片化:清除后会产生大量不连续的内存碎片,可能导致大对象分配失败并触发 Full GC。
  2. 标记-复制算法 (Copying Algorithm)

    • 过程:将可用内存按容量分为两块,每次只使用其中一块。当使用的内存块空间耗尽时,将存活的对象复制到另一块内存中,然后清理掉已使用的内存块。此算法主要用于新生代。
    • 优点高效:每次只复制存活对象,未使用的内存直接清理,减少了标记和清理的时间。解决碎片化问题:因为每次都将存活对象复制到另一块内存中,保持内存连续性。
    • 缺点:只能使用一半的内存,浪费了空间。
    • 应用:HotSpot 虚拟机的新生代采用这种算法,将内存分为较大的 Eden 区和两块较小的 Survivor 区。内存分配时使用 Eden 和一个 Survivor 区,回收时将存活对象复制到另一个 Survivor 区,清理已使用的 Eden 和 Survivor。默认的 Eden 和 Survivor 大小比例为 8:1,意味着每次新生代可用空间为整个新生代的 90%。
  3. 标记-整理算法 (Mark-Compact Algorithm)

    • 过程:标记过程与标记-清除算法相同,但在清理时不是直接清理可回收对象,而是将所有存活对象向内存空间的一端移动,然后清理边界以外的内存。该算法多用于老年代。
    • 优点:减少了内存碎片化问题,因为所有存活对象都会被移动到一端。
    • 缺点效率低下:当对象存活率高时,需要进行大量对象的移动操作。需要暂停用户线程:对象移动过程中,程序需要暂停,影响应用程序的响应速度。
  • 总结:标记-清除标记-整理算法的主要区别在于对象是否移动。标记-整理是移动式算法,可以减少内存碎片,但需要更复杂的操作和暂停用户线程。而标记-清除是非移动式的,简单但容易导致内存碎片。标记-复制算法则在对象存活率低的情况下效率更高,适合用于新生代的垃圾回收。

6. CMS (Concurrent Mark-Sweep)

CMS(Concurrent Mark-Sweep)是一种以减少垃圾回收停顿时间为目标的垃圾回收器,基于标记-清除算法,适用于对响应时间要求较高的应用。CMS 专注于老年代的垃圾回收,旨在最小化停顿时间,与 Eden 区的垃圾回收过程分开管理。CMS 回收过程分为四个主要步骤:

  1. 初始标记 (Initial Mark):标记从 GC Roots 能直接关联的对象,速度快,但需要“Stop-The-World”(STW),即暂停所有用户线程。

  2. 并发标记 (Concurrent Mark):从 GC Roots 的直接关联对象开始,遍历整个对象图,对对象进行标记。此过程耗时较长,但与用户线程并发执行,不需要暂停应用程序。

  3. 重新标记 (Remark):修正并发标记期间因为用户程序运行导致的对象引用变动。这一步需要短暂的 STW 停顿。

  4. 并发清除 (Concurrent Sweep):清除标记阶段判断为已死亡的对象,不需要移动存活对象,且可以与用户线程并发执行。

缺点

  • 对处理器资源敏感:在并发阶段会占用部分处理器资源,尽管不暂停用户线程,但会降低应用程序的吞吐量。
  • 无法处理浮动垃圾:并发标记期间新产生的垃圾可能无法立即回收,可能导致“并发失败”,最终需要触发 Full GC。
  • 内存碎片问题:由于基于标记-清除算法,CMS 会产生内存碎片,这可能导致后续大对象分配失败。

7. G1 (Garbage-First)

  • G1 是一种面向服务端应用程序的垃圾回收器,设计目标是替代 CMS,通过面向局部收集和基于 Region 的内存布局来提高回收效率。与之前的垃圾收集器不同,G1 并不是一次性回收整个新生代或老年代,而是选择堆中部分内存区域(Regions)进行回收。
  • 特点
  1. 基于 Region 的内存布局:G1 将内存划分为多个大小相同的区域(Region),这些区域可以是 Eden 区、Survivor 区或者老年代的一部分。G1 根据各 Region 中的垃圾数量和回收收益来决定优先回收哪些区域。
  2. 优先级列表:在后台维护一个优先级列表,每次根据用户设定的停顿时间限制,优先回收回收价值最大的 Region(即回收空间最多且耗时最少的区域),以在有限时间内提高回收效率。
  • G1 的运作过程
  1. 初始标记 (Initial Mark):标记从 GC Roots 能直接关联到的对象,并让用户线程继续运行,确保在分配新对象时能正确地使用可用的 Region。需要短暂的 STW 停顿,通常与 Minor GC 同步完成。
  2. 并发标记 (Concurrent Mark):从 GC Roots 开始进行可达性分析,递归扫描整个堆的对象图。此阶段耗时较长,但可以与用户线程并发运行。扫描完成后,处理 SATB(Snapshot-At-The-Beginning)记录的在并发期间发生变动的对象。
  3. 最终标记 (Final Mark):进行短暂的 STW 停顿,处理并发标记阶段结束后仍遗留的少量 SATB 记录。
  4. 筛选回收 (Cleanup/Compaction):根据各 Region 的回收价值排序,制定回收计划。在这一阶段必须暂停用户线程,由多个收集线程并行完成回收。
  • 优点可预测的停顿时间:用户可以指定期望的 GC 停顿时间(通常设置在 100 到 300 毫秒之间),G1 会根据停顿时间动态调整回收策略,提高回收效率。
  • 缺点复杂性:G1 的内部逻辑和算法较复杂,调优难度较高。可能出现内存碎片:尽管 G1 通过并行和并发机制减少了碎片化,但在极端情况下可能仍会出现内存碎片问题。

8. 内存分配与回收策略

Java 虚拟机(JVM)在内存分配和垃圾回收方面使用了多种策略,以优化性能并有效管理堆内存。以下是一些常见的内存分配与回收策略:

  1. 对象优先在 Eden 区分配

    • 在大多数情况下,新创建的对象首先分配在新生代的 Eden 区。当 Eden 区没有足够的空间时,会触发一次 Minor GC 来清理新生代的内存。
    • Minor GC 主要回收新生代内存,对象存活下来的话可能会被转移到 Survivor 区或直接晋升到老年代。
  2. 大对象直接进入老年代

    • 大对象(例如长字符串或大型数组)指的是需要大量连续内存空间的对象。这些对象在 Eden 区或 Survivor 区频繁移动会影响性能。
    • 为了避免频繁的内存复制,大对象可以直接分配到老年代。
  3. 长期存活对象进入老年代

    • JVM 通过对象的年龄来判断对象是否需要晋升到老年代。每个对象都有一个年龄计数器,存储在对象头部。当对象在 Survivor 区经过一次 Minor GC 依然存活,其年龄就会增加。
    • 当对象的年龄达到一定阈值(默认是 15),对象会被晋升到老年代。
  4. 动态对象年龄判定

    • 为了更灵活地适应不同的内存状况,JVM 不要求所有对象的年龄都必须达到设定的阈值才能晋升到老年代。
    • 如果在 Survivor 区中,相同年龄的所有对象大小的总和超过了 Survivor 区的一半,则年龄不小于该年龄的对象可以直接晋升到老年代。这样做是为了避免 Survivor 区被填满,确保内存使用的灵活性和效率。
  5. 空间分配担保

    • 在进行 Minor GC 之前,虚拟机会检查老年代中最大的可用连续空间是否大于新生代所有对象的总空间。如果是,则 Minor GC 是安全的。
    • 如果老年代的可用空间不足,JVM 会检查 -XX:HandlePromotionFailure 参数是否允许空间分配担保失败。如果允许,则会继续检查老年代最大可用空间是否大于历次晋升到老年代对象的平均大小。
    • 如果满足条件,则 JVM 将尝试进行一次 Minor GC,这是一种冒险,因为在 Minor GC 后仍有大量对象存活时,可能需要老年代来接收这些存活对象。如果条件不满足或者冒险失败,则会触发一次 Full GC。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值