Skip to content

JVM层GC调优

JVM的内存结构

参考

JVM的内存结构主要分为几个不同的区域,这些区域按照功能和生命周期来划分,每个区域负责存储不同类型的程序数据。以下是JDK 8及之前版本的JVM内存结构的主要组成部分:

  1. 程序计数器 (Program Counter Register, PC Register)

    • 每个线程都有一个独立的程序计数器,用于记录当前线程执行的字节码行号位置,是线程私有的。
    • 它是唯一一个没有规定任何OutOfMemoryError情况的区域。
  2. 虚拟机栈 (Java Virtual Machine Stacks)

    • 线程私有,与线程生命周期相同。
    • 存储方法调用时的信息,包括局部变量表、操作数栈、动态链接、方法出口等信息。
    • 如果请求的栈深度超过了所允许的最大值,会抛出StackOverflowError;
    • 如果无法申请到足够的内存空间,则抛出OutOfMemoryError。
    • 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
  3. 本地方法栈 (Native Method Stack)

    • 类似于虚拟机栈,但服务于native方法
    • 在执行Java本地接口JNI调用的本地方法时使用。
  4. Java堆 (Heap)

    • 所有线程共享的一块内存区域,也是GC(Garbage Collection,垃圾回收)的主要区域。
    • 存储对象实例以及数组等所有对象的实例部分。
    • 分为新生代(Young Generation)、老年代(Old Generation)和永久代/元空间(PermGen/Metaspace,从JDK 8开始元空间取代了永久代)。
    • 新生代进一步划分为Eden区、 Survivor区(From和To)
    • 当堆无法再扩展且无法满足新的内存分配需求时,将抛出OutOfMemoryError。
  5. 方法区 (Method Area) / 元空间 (Metaspace)

    • 在JDK 8及之后,永久代被移除,取而代之的是元空间(Metaspace),它位于操作系统内存而非堆中。
    • 存储已被加载的类信息、常量池、静态变量、即时编译后的代码缓存等数据。
    • 方法区内存不足或元空间溢出时同样会导致OutOfMemoryError。
  6. 运行时常量池 (Runtime Constant Pool)

    • 方法区的一部分,存放类文件中的符号引用和字面量信息。
  7. 直接内存 (Direct Memory)

    • 不属于JVM内存规范中定义的运行时数据区域,但是Java应用程序可以使用java.nio包下的ByteBuffer等类直接向操作系统申请内存,这部分内存需要在Java堆外分配。
    • 直接内存的容量大小可以通过-XX:MaxDirectMemorySize参数设置,若超出此范围也会抛出OutOfMemoryError。

上述各部分共同构成了JVM的内存模型,它们协同工作以支持Java程序的运行,并通过垃圾收集机制来管理内存资源。随着JDK版本的更新,内存结构的具体实现细节可能有所变化,比如从Java 8开始永久代变成了元空间,并且其内存管理方式与之前的永久代有所不同。

运行时数据区: 程序计数器、虚拟机栈、本地方法栈、堆、方法区

常用参数

shell
-Xms 
-Xmx
-XX:NewSize
-XX:MaxNewSize
-XX:NewRatio
-XX:SurvivorRatio
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
-XX:+UseCompressedClassPointers
-XX:CompressedClassSpaceSize
-XX:InitalCodeCacheSize
-XX:ReservedCodeCacheSize
参数含义常用场景
-Xms初始堆内存大小,设置Java虚拟机(JVM)启动时的堆内存容量。设置固定的初始堆大小以减少程序运行过程中内存分配的开销,提高性能。
-Xmx最大堆内存大小,指定Java虚拟机在运行期可以占用的最大内存大小。防止应用程序因内存需求增长过大而引发OutOfMemoryError异常。
-XX:NewSize年轻代(Young Generation)的初始大小。调整年轻代的初始分配空间,优化垃圾回收器的效率和应用响应时间。
-XX:MaxNewSize年轻代的最大大小,限制年轻代内存区域可扩展到的最大值。控制年轻代内存上限,防止年轻代占用过多导致老年代空间不足。
-XX:NewRatio新生代与老年代的比例,表示老年代与新生代容量的比例,例如-XX:NewRatio=3意味着老年代是新生代容量的3倍。根据应用程序对象生命周期特性调整GC策略,优化垃圾回收效果。
-XX:SurvivorRatioEden区与每个Survivor区的比例,例如设置为8,则表示Eden区与Survivor区的比值为8:1:1。优化新生代内对象在不同 Survivor 区间的移动,减少GC频率。
-XX:MetaspaceSize方法区(Metaspace)的初始大小,在JDK 8及以后用于替换永久代。控制类元数据占用的空间,避免方法区满引发的OutOfMemoryError
-XX:MaxMetaspaceSize方法区(Metaspace)的最大大小,超过这个值将会触发Full GC,并且可能导致Metaspace扩容失败。对于大量加载类的应用场景,防止方法区过大导致系统资源耗尽。
-XX:+UseCompressedClassPointers是否启用压缩类指针,对于64位JVM有效,能够节省内存空间。提高内存使用效率,尤其是对有限的内存环境或者大型应用非常有用。
-XX:CompressedClassSpaceSize当启用压缩类指针时,预留用于存储已压缩类指针的空间大小。预防由于大量加载和卸载类导致的Compressed Class Space溢出。
-XX:InitialCodeCacheSize初始化代码缓存大小,用于存放从JIT编译后的机器码。调整HotSpot JVM中JIT编译器生成代码的缓存大小,优化编译效率。
-XX:ReservedCodeCacheSize编译代码缓存最大容量,超出该值会引发编译错误或性能下降。针对需要大量动态编译的复杂应用,确保有足够的空间存储编译后的代码。

请注意:部分参数随着JVM版本的更新可能会有所变化或被新的参数替代,请根据实际使用的JVM版本查阅官方文档获取最新信息。

垃圾回收算法

枚举根节点,做可达性分析

根节点一般为:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等等

可参考

标记清除算法

分为标记与清除两个阶段:首先标记处所有需要回收的对象,在标记完成后统一回收所有。

缺点:

1、效率低。标记和清除两个过程的效率都不高

2、产生碎片。碎片太多会导致提前GC

复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

优缺点:实现简单,运行高效,但是空间利用率低。

标记整理算法

主要用于管理老年代内存区域的垃圾回收,主要包含两个阶段:

  1. 标记阶段:与标记清除算法类似,首先遍历堆中的所有对象,标记出所有可达的对象,即从根对象出发能够通过引用链到达的对象。

  2. 整理阶段:不同于标记清除算法直接清理掉未被标记的对象,标记整理算法在标记阶段完成后,会将所有存活的对象“压缩”到内存的一端,使得所有的存活对象紧凑地排列在一起,而剩下的空间则构成了一片连续的、可供分配的内存区域。这样不仅完成了垃圾对象的回收,还解决了标记清除算法带来的内存碎片问题。

优点

  • 减少或避免了内存碎片,有利于大对象的分配和整体的内存利用率。
  • 由于内存是连续的,可以简化内存分配操作,提高效率。

缺点

  • 整理过程比标记清除算法更为复杂,需要移动存活对象,执行成本较高,可能会导致更长的STW(Stop-The-World)停顿时间。
  • 对象移动可能会影响到程序的性能,比如如果存在指向已被移动对象的引用,则这些引用必须更新为新的地址。

标记整理算法是一种兼顾内存利用率和长期稳定性的垃圾收集策略,适用于对长时间运行且对内存碎片较为敏感的应用场景。不过因其潜在的暂停时间和额外开销,通常会在追求低延迟或高吞吐量要求不那么严格时使用。

分代垃圾回收算法

分代垃圾回收算法(Generational Garbage Collection)是一种基于对象生命周期假设的内存管理策略,它将Java堆内存划分为不同的区域,每个区域对应一个或多个世代。这种划分主要依据的是大多数对象在创建后很短的时间内就会变得不可达并可以被回收这一观察结果。

在JVM中,堆内存通常被划分为以下几代:

  1. 新生代(Young Generation): 新创建的对象首先会被分配到新生代空间中,新生代进一步细分为Eden区和两个Survivor区(例如:From Survivor和To Survivor)。大部分新生成的对象会在首次垃圾回收时被回收,这被称为Minor GC或Young GC。

  2. 老年代(Old Generation): 那些在新生代中经历过多次GC仍存活下来的对象,会被晋升至老年代。老年代中的对象生存周期相对较长,对其进行垃圾回收的频率较低,当老年代空间不足时会触发Major GC或Full GC。

  3. 永久代/元空间(PermGen/Metaspace): 在JDK 8及之前版本,用于存储类信息、常量池等静态数据。从JDK 8开始,永久代被移除,其功能被元空间(Metaspace)取代,位于本地内存而不是堆上。

分代垃圾回收的工作流程大致如下:

  • 对于新生代,使用复制算法(Copying),将Eden区和From Survivor区中存活的对象复制到To Survivor区,或者直接晋升到老年代。
  • 对于老年代,一般采用标记整理(Mark-Compact)、标记清除(Mark-Sweep)或CMS并发标记清除等更复杂的垃圾回收算法。

通过这样的分代处理,垃圾收集器可以更加高效地定位和回收垃圾对象,降低内存管理和垃圾回收的整体开销。同时,由于新生代垃圾回收较为频繁且所需时间较短,而老年代回收相对较少但可能耗时较长,分代回收可以平衡系统的性能表现。

对象分配

在Java虚拟机(JVM)中,对象的分配过程主要遵循以下步骤:

  1. 逃逸分析: 在对象分配前,JVM会进行逃逸分析,判断新创建的对象是否有可能被外部方法引用,即该对象是否“逃逸”出当前作用域。如果对象不会逃逸,则可能采用栈上分配(也称为标量替换优化),将对象直接分配到线程栈帧上。这样,当方法调用结束后,栈帧自然销毁,所分配的对象也随之释放,无需垃圾回收器参与。

  2. TLAB分配(Thread Local Allocation Buffer): 对于大部分逃逸至堆的对象,JVM首先尝试在每个线程私有的内存区域——本地线程分配缓冲区(TLAB)中进行分配。TLAB是Eden空间的一部分,每个线程拥有独立的缓冲区,可以快速且无锁地为对象分配内存。这种方式提高了多线程环境下内存分配的并发性与性能。

  3. 堆上分配: 如果TLAB空间不足或者对象较大无法放入TLAB,JVM会在堆内存中为对象分配空间。对于新生代中的对象,通常是在Eden空间分配。若对象大小超过一定阈值(例如大于eden空间剩余部分或达到大对象直接进入老年代的标准),则对象会直接在老年代分配。

  4. 晋升到老年代: 经过多次年轻代垃圾回收后仍然存活的对象,会被复制到老年代中。这种机制基于对象的生存周期假设,认为长期存活的对象在未来更可能继续存活。

  5. 大对象分配: 大型对象(例如数组等占用大量连续内存空间的对象)可能会跳过新生代,直接在老年代分配,以避免在新生代频繁GC时进行大量的内存复制操作。

总的来说,JVM的对象分配是一个动态、复杂的过程,旨在提高内存利用率和系统整体性能。通过各种优化手段,如逃逸分析、TLAB、分代收集等,JVM能够高效地管理程序运行过程中产生的大量对象生命周期。

对象优先在Eden区分配,大对象直接进入老年代: -XX:PretenureSizeThreshold

垃圾收集器

可参考

GC调优步骤

打印GC日志

根据日志得到关键性能指标

分析GC原因,调优JVM参数

Tomcat设置参数

shell
-XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_HOME/logs/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log
shell
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=xxx
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
-XX:+DisableExplicitGC
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers

Parallel GC调优的指导原则

除非确定,否则不要设置最大堆内存

优先设置吞吐量目标

如果吞吐量目标达不到,调大最大内存,不能让OS使用Swap,如果仍然达不到,降低目标

GC调优的指导原则

吞吐量能达到,GC时间太长,设置停顿时间的目标

G1 GC最佳实践

年轻代大小: 避免使用-Xmn、-XX:NewRatio等显式设置Young区大小,会覆盖暂停时间目标

暂停时间目标:暂停时间不要太严苛,其吞吐量目标是90%的应用程序时间和10%的垃圾回收时间,太严苛会直接影响到吞吐量

shell
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent
-XX:G1HeapWastePercent
-XX:G1MixedGCCountTarget
-XX:G1OldCSetRegionThresholdPercent
shell
-XX:+UseG1GC -Xms128M -Xmx128M -XX:MetaspaceSize=64M -XX:MaxGCPauseMillis=100 -XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3