静态网站怎么做百度推广上饶网站建设多少钱

张小明 2026/1/2 9:49:47
静态网站怎么做百度推广,上饶网站建设多少钱,设计一个电子商务网站,展示型网站JVM性能调优与监控实战完整指南 一、JVM内存模型深度解析 1.1 JVM内存结构概述 Java虚拟机#xff08;JVM#xff09;作为Java程序的运行环境#xff0c;承担着内存管理、垃圾回收、字节码执行等核心职责。在JVM的众多职责中#xff0c;内存管理无疑是最重要的一环。合理的…JVM性能调优与监控实战完整指南一、JVM内存模型深度解析1.1 JVM内存结构概述Java虚拟机JVM作为Java程序的运行环境承担着内存管理、垃圾回收、字节码执行等核心职责。在JVM的众多职责中内存管理无疑是最重要的一环。合理的内存划分和管理直接影响到应用程序的性能表现。当我们谈论JVM性能调优时首先需要深入理解JVM是如何组织和管理内存的。JVM在执行Java程序时会将系统分配给它的内存划分为多个不同的数据区域每个区域有着明确的用途、创建时间和销毁时间。这种设计不是随意为之而是经过精心考虑的结果目的是为了更高效地管理内存、更快速地分配对象、更安全地执行垃圾回收。理解这些内存区域的划分方式、各自的职责、以及它们之间的协作关系是进行JVM性能调优的基础。只有深入了解了内存的组织结构我们才能在遇到内存问题时快速定位原因才能在进行参数调优时做到有的放矢。从宏观角度来看JVM内存可以分为两大类线程共享区域和线程私有区域。线程共享区域包括堆内存和方法区这些区域在JVM启动时创建所有线程都可以访问这些区域的数据。线程私有区域包括程序计数器、虚拟机栈和本地方法栈这些区域的生命周期与线程相同随线程的创建而创建随线程的结束而销毁。根据Java虚拟机规范JVM内存主要分为以下几个区域JVM内存结构 ├── 堆内存Heap- 线程共享 │ ├── 年轻代Young Generation │ │ ├── Eden区 │ │ ├── Survivor0区From │ │ └── Survivor1区To │ └── 老年代Old Generation ├── 方法区Method Area- 线程共享 │ ├── 运行时常量池 │ ├── 类型信息 │ └── 字段和方法信息 ├── 虚拟机栈VM Stack- 线程私有 ├── 本地方法栈Native Method Stack- 线程私有 ├── 程序计数器Program Counter Register- 线程私有 └── 直接内存Direct Memory- 不属于JVM规范1.2 堆内存详解堆内存是JVM内存管理中最核心、也是最复杂的一块区域。从大小上来说堆通常是JVM管理的最大一块内存空间在现代应用中堆内存的大小通常从几百兆到几十GB不等。从重要性上来说堆是垃圾回收器工作的主战场几乎所有的对象实例以及数组都在堆上分配内存。可以说堆内存的设计和管理水平直接决定了JVM的性能表现。为什么堆内存如此重要这要从Java的内存分配机制说起。在Java中我们通过new关键字创建对象时这些对象的内存主要分配在堆上。与栈内存不同堆内存不会随着方法的结束而自动回收这些对象会一直存在于内存中直到垃圾回收器判断它们不再被使用时才会被回收。这种特性使得堆内存的管理变得复杂需要专门的垃圾回收机制来处理。堆内存的一个核心设计理念是分代管理。这个设计源于一个被大量实际应用验证的经验规律绝大多数对象的生命周期都很短它们被创建后很快就会变得不可达可以被回收只有很少一部分对象会长期存活。这个规律被称为弱分代假说Weak Generational Hypothesis。基于这个假说JVM将堆内存划分为不同的代对不同年龄的对象采用不同的回收策略从而大大提高了垃圾回收的效率。1.2.1 年轻代Young Generation年轻代是所有新创建对象的出生地。当我们在代码中创建一个对象时这个对象通常会首先被分配到年轻代的Eden区。年轻代的设计体现了朝生夕死的对象特点——大部分对象在这里被创建也在这里被回收。从容量规划角度来看年轻代的大小通常占整个堆内存的1/3左右。这个比例不是固定的而是可以根据应用的特点进行调整。如果你的应用创建了大量生命周期很短的对象比如Web应用中的Request、Response对象那么可以适当增大年轻代的比例反之如果应用中对象的生命周期普遍较长则可以减小年轻代的比例。年轻代内部又进一步细分为三个区域Eden区和两个Survivor区。这种设计看似复杂实际上是为了实现高效的垃圾回收算法。让我们详细了解这三个区域Eden区伊甸园区Eden区是年轻代中最大的一块区域默认占据年轻代的80%空间。之所以叫Eden伊甸园寓意是所有对象的出生地。几乎所有新创建的对象都会首先被分配到这里这是对象生命周期的起点。Eden区采用的是一种非常高效的内存分配策略叫做指针碰撞Bump the Pointer。简单来说JVM维护一个指针指向Eden区已使用内存和未使用内存的分界点。当需要分配新对象时只需要检查剩余空间是否足够如果足够就将指针向前移动相应的大小即可。这种分配方式非常快速几乎与在栈上分配内存的速度相当。当Eden区的空间被用完时就会触发一次Minor GC也叫Young GC。这时候JVM会暂停应用程序的运行Stop The World检查Eden区中的所有对象找出那些仍然被引用的存活对象将它们复制到Survivor区然后清空整个Eden区。整个过程通常非常快因为Eden区中的大部分对象都已经死亡需要复制的对象很少。Survivor区幸存者区Survivor区的设计是年轻代回收机制中最巧妙的部分。它由两个大小完全相等的区域组成通常称为S0和S1或者From区和To区。每个Survivor区默认占年轻代的10%空间。为什么需要两个Survivor区这个设计源于一个重要的考虑如何避免内存碎片。如果只有一个Survivor区那么在多次GC后这个区域会充满各种大小不一的对象它们之间会产生很多不连续的空闲空间。这些碎片化的空间很难被有效利用可能导致明明有足够的总空闲空间却无法分配一个稍大的对象。两个Survivor区的工作机制是这样的在任何时刻两个Survivor区中只有一个是活跃的From区另一个保持完全空闲To区。当发生Minor GC时Eden区和From区中的存活对象会被一起复制到To区。复制完成后Eden区和From区被完全清空然后From区和To区的角色互换——原来的To区变成新的From区原来的From区变成新的To区。这种乒乓式的切换机制确保了使用中的Survivor区始终是紧凑的、没有碎片的。这种复制算法有个额外的好处它天然地实现了内存整理。每次GC后所有存活对象都被整齐地排列在To区的前端没有任何碎片。这使得后续的对象分配仍然可以使用快速的指针碰撞方式。对象在年轻代的生命周期一个对象的成长之路理解对象在年轻代的完整生命周期对于理解JVM的内存管理至关重要。让我们跟随一个对象从创建到晋升的整个过程首先当应用程序创建一个新对象时这个对象会被分配到Eden区。此时这个对象的年龄Age被标记为0。对象的年龄是JVM用来跟踪对象经历了多少次GC的一个计数器。随着程序的运行越来越多的对象被创建Eden区逐渐被填满。当Eden区无法再分配新对象时JVM触发第一次Minor GC。垃圾回收器会扫描Eden区的所有对象识别哪些对象仍然被程序引用存活对象哪些对象已经没有被引用垃圾对象。存活对象会被复制到Survivor0区同时它们的年龄增加到1。垃圾对象则被清除Eden区恢复为空。程序继续运行新的对象继续在Eden区分配。当Eden区再次填满时触发第二次Minor GC。这次不仅要扫描Eden区还要扫描Survivor0区。Eden区和Survivor0区中的存活对象会一起被复制到Survivor1区它们的年龄再次加1对于Eden区的新对象年龄变为1对于Survivor0区的对象年龄变为2。然后Eden区和Survivor0区被清空。这个过程会反复进行。每次Minor GC时Eden区和使用中的Survivor区From区的存活对象都会被复制到空闲的Survivor区To区对象年龄加1然后两个Survivor区角色互换。那么对象什么时候会离开年轻代进入老年代呢默认情况下当对象的年龄达到15时就会被晋升Promotion到老年代。为什么是15因为对象头中用于存储年龄的位段只有4位最大只能表示15。当然这个阈值是可以通过参数调整的。值得注意的是对象并不一定要等到年龄达到15才能晋升。如果Survivor区空间不足装不下所有存活对象那么一些对象会提前晋升到老年代即使它们的年龄还很小。这被称为过早晋升Premature Promotion是一种不理想的情况因为这些对象可能很快就会死亡但却进入了老年代增加了老年代GC的负担。1.2.2 老年代Old Generation老年代是堆内存中用于存放长者的区域这里的对象都是经过多次GC考验、依然存活的老对象。从空间分配来看老年代通常占据堆内存的2/3左右这个比例反映了一个事实虽然大部分对象都很短命但那些长寿对象所占用的总内存量却不小。老年代的管理策略与年轻代有着本质的不同。在年轻代对象密度较低大部分是垃圾适合用复制算法快速清理。但在老年代对象密度很高大部分都存活如果还用复制算法就需要复制大量对象效率很低。因此老年代通常采用标记-清除或标记-整理算法这些算法不需要大量复制对象但执行时间较长。老年代的GC事件通常称为Major GC或Full GC严格来说两者有细微差别但在实际中常被混用其特点是频率低、耗时长、影响大。一次Full GC可能需要几秒甚至更长时间在此期间应用程序会完全停顿。因此性能调优的一个重要目标就是减少Full GC的频率。对象进入老年代的条件不只是年龄很多人以为对象进入老年代只有一个条件年龄达到阈值。实际上JVM设计了多种机制来决定对象何时晋升这些机制共同作用确保内存的高效利用。让我们逐一分析1. 年龄达到晋升阈值Age Threshold这是最常见的晋升方式。对象每经历一次Minor GC年龄就加1。当年龄达到设定的阈值默认15可通过-XX:MaxTenuringThreshold调整对象就会晋升到老年代。这个机制的逻辑很简单一个对象如果经历了这么多次GC还没死那它很可能是个长寿对象应该放到老年代去。2. 大对象直接分配Large Object Direct Allocation某些特别大的对象通常是大数组会直接绕过年轻代在创建时就被分配到老年代。这个设计的考虑是大对象在年轻代会占用大量空间而年轻代的复制算法需要复制存活对象复制大对象的成本很高。与其在年轻代折腾不如直接放到老年代。这个特别大的阈值可以通过-XX:PretenureSizeThreshold参数设置。不过需要注意这个参数只对Serial和ParNew收集器有效对Parallel Scavenge无效。在实际应用中应该尽量避免创建大对象如果必须创建也要考虑对象池等复用机制。3. 动态年龄判定Dynamic Age Determination这是一个很聪明的机制。JVM并不会死板地等待对象年龄达到15才晋升。如果在Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半那么年龄大于或等于该年龄的对象就可以直接晋升到老年代无需等到MaxTenuringThreshold设定的年龄。为什么要这样设计假设有一批对象都是在同一时刻创建的比如处理一批请求它们会一起在Survivor区中停留。如果这批对象的数量很大占据了Survivor区的大半空间那么继续让它们留在Survivor区就没有意义了还不如早点让它们晋升腾出Survivor区的空间给更年轻的对象。4. 空间分配担保Allocation Guarantee这是一种应急晋升机制。当发生Minor GC时如果Survivor区空间不足以容纳所有存活对象那些放不下的对象就会直接晋升到老年代不管它们的年龄是多少。这种情况通常说明Survivor区设置得太小了是一个需要关注的调优点。老年代GC的特点慢而重老年代的垃圾回收与年轻代有着本质的不同主要体现在以下几个方面首先是触发时机。老年代GC通常在老年代空间不足时触发这可能是因为对象晋升导致的也可能是直接在老年代分配大对象导致的。还有一种情况是在Minor GC之前JVM会做一个检查如果预测这次Minor GC后需要晋升的对象大小大于老年代剩余空间就会先触发一次Full GC。其次是回收算法。老年代不能使用年轻代的复制算法因为老年代中大部分对象都是存活的复制的成本太高。老年代通常使用标记-清除或标记-整理算法这些算法需要标记所有存活对象然后清除或整理内存过程比年轻代的GC复杂得多。最后是性能影响。一次Full GC可能需要几百毫秒到几秒的时间在此期间应用程序完全停顿Stop The World。对于在线服务来说几秒的停顿意味着成百上千个请求超时这是无法接受的。因此性能调优的一个核心目标就是减少Full GC的频率或者选用能够并发执行、停顿时间短的垃圾回收器。1.3 方法区元空间类信息的存储库方法区是JVM规范中定义的一个逻辑概念用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然规范称其为方法区但它存储的内容远不止方法更准确地说它是类的元数据的存储区域。方法区的重要性常常被忽视但它在JVM中扮演着至关重要的角色。当我们编写一个Java类时包含了类的结构信息有哪些字段、哪些方法、类的继承关系、实现了哪些接口等等。这些信息在类加载时会被解析并存储到方法区中。可以说方法区存储的是Java程序的骨架而堆中存储的则是这个骨架的血肉对象实例。从永久代到元空间一次重要的演进方法区的实现在JVM的演进过程中经历了一次重大变革理解这次变革有助于我们更好地理解和调优JVM。在JDK 7及以前的版本中HotSpot虚拟机使用永久代Permanent Generation简称PermGen来实现方法区。永久代使用的是JVM堆内存这意味着永久代的大小受到堆内存的限制。这种实现带来了一些问题首先永久代的大小很难估算。不同的应用加载的类的数量差异很大框架多、使用反射多、动态代理多的应用可能需要很大的永久代空间。如果永久代设置得太小容易发生java.lang.OutOfMemoryError: PermGen space错误如果设置得太大又会挤占堆内存空间。其次永久代的垃圾回收效率低。类的卸载条件非常苛刻需要满足类的所有实例都被回收、类加载器被回收、Class对象没有被引用等条件。在实际应用中类的卸载很少发生这意味着永久代的空间基本上是只增不减的。为了解决这些问题从JDK 8开始HotSpot虚拟机完全移除了永久代改用元空间Metaspace来实现方法区。这是一次革命性的变化主要体现在元空间使用的是本地内存Native Memory而不是JVM堆内存。这意味着元空间的大小不再受到-Xmx参数的限制而是受到机器物理内存的限制。默认情况下元空间可以动态扩展理论上可以使用所有可用的系统内存当然这通常不是好事所以还是应该设置上限。这个变化带来了几个好处首先不再需要精确估算方法区的大小减少了OOM的风险。其次堆内存的规划变得更简单不需要在堆内存和永久代之间权衡。最后元空间的垃圾回收更加高效因为它与堆的GC独立进行。但这个变化也带来了新的挑战如果不设置元空间的上限类加载过多或者发生类加载器泄漏时可能会耗尽系统内存影响整个机器的稳定性。因此在生产环境中通常建议显式设置-XX:MaxMetaspaceSize参数。方法区存储内容方法区 ├── 类型信息类名、父类、接口、修饰符等 ├── 方法信息方法名、返回类型、参数、字节码等 ├── 字段信息字段名、类型、修饰符 ├── 运行时常量池 │ ├── 字面量字符串常量、final常量等 │ └── 符号引用类、方法、字段的符号引用 └── 静态变量1.4 虚拟机栈方法执行的舞台虚拟机栈是线程私有的内存区域它的生命周期与线程相同。当创建一个新线程时JVM会为这个线程分配一个虚拟机栈当线程结束时它的虚拟机栈也随之销毁。虚拟机栈描述的是Java方法执行的内存模型每个方法在执行时都会创建一个栈帧Stack Frame用于存储该方法运行时需要的各种信息。理解虚拟机栈对于理解Java程序的执行机制非常重要。当我们调用一个方法时实际上是将一个新的栈帧压入栈顶当方法执行完毕无论是正常返回还是抛出异常对应的栈帧就会从栈顶弹出。这种后进先出LIFO的结构天然地支持了方法调用的嵌套关系。栈帧中存储了什么呢主要包括局部变量表、操作数栈、动态链接和方法返回地址等信息。局部变量表存储了方法的参数和方法内定义的局部变量操作数栈用于执行字节码指令时的操作数临时存储动态链接用于将符号引用转换为直接引用方法返回地址则记录了方法执行完成后要返回到哪里继续执行。虚拟机栈的大小是有限的如果线程请求的栈深度大于虚拟机所允许的深度就会抛出StackOverflowError。这最常见于递归调用没有正确设置终止条件的情况。虚拟机栈也可以动态扩展但如果扩展时无法申请到足够的内存就会抛出OutOfMemoryError。在性能调优时虚拟机栈的大小通常不是关注的重点除非应用程序有以下特点使用了大量的递归、方法调用层次很深、或者创建了大量的线程。这时候就需要通过-Xss参数来调整每个线程的栈大小。栈帧结构栈帧Stack Frame ├── 局部变量表 │ └── 存储方法参数和局部变量 ├── 操作数栈 │ └── 用于存放方法执行过程中产生的中间结果 ├── 动态链接 │ └── 指向运行时常量池中该栈帧所属方法的引用 ├── 方法返回地址 │ └── 方法正常退出或异常退出的定义 └── 附加信息相关异常StackOverflowError线程请求的栈深度大于虚拟机所允许的深度如递归调用过深OutOfMemoryError虚拟机栈动态扩展时无法申请到足够的内存栈内存大小设置-Xss256k# 设置每个线程的栈大小为256KB1.5 直接内存直接内存不是JVM运行时数据区的一部分但在NIO操作中被频繁使用。特点通过DirectByteBuffer对象分配和管理不受JVM堆内存限制但受物理内存限制避免了Java堆和Native堆之间的数据复制提高性能不会被垃圾回收直接管理但通过Reference机制回收参数设置-XX:MaxDirectMemorySize512M# 设置直接内存最大值1.6 对象的内存分配流程理解对象的内存分配流程对于性能调优至关重要对象创建 ↓ 是否为大对象 ├─ 是 → 直接分配到老年代 └─ 否 → 尝试在Eden区分配 ↓ Eden区是否有足够空间 ├─ 是 → 分配成功 └─ 否 → 触发Minor GC ↓ 清理Eden区和Survivor From区 ↓ 存活对象移到Survivor To区 ↓ 对象年龄1 ↓ 年龄是否达到阈值 ├─ 是 → 晋升到老年代 └─ 否 → 留在Survivor区 ↓ Survivor区是否放得下 ├─ 是 → 分配成功 └─ 否 → 直接进入老年代 ↓ 老年代是否有空间 ├─ 是 → 分配成功 └─ 否 → 触发Full GC ↓ GC后是否有空间 ├─ 是 → 分配成功 └─ 否 → OutOfMemoryError二、垃圾回收器原理与选择2.1 垃圾回收算法基础垃圾回收Garbage CollectionGC是JVM自动内存管理的核心机制。在Java中程序员不需要手动释放内存不像C/C需要free或delete这项工作由垃圾回收器自动完成。但自动不意味着随意垃圾回收器遵循着一套精心设计的算法这些算法决定了如何识别垃圾、何时回收垃圾、如何回收垃圾。在深入了解各种垃圾回收器之前我们需要先理解垃圾回收的基础算法。这些算法是所有垃圾回收器的理论基础不同的回收器本质上是这些基础算法的不同组合和优化。掌握了这些基础算法就能理解为什么不同的回收器适用于不同的场景也能在遇到GC问题时更好地分析和解决。2.1.1 标记-清除算法Mark-Sweep最基础的回收算法标记-清除算法是最基础、最早出现的垃圾回收算法由John McCarthy在1960年发明用于Lisp语言。虽然它有明显的缺点但这个算法的思想影响深远后续的很多算法都是在它的基础上改进而来。这个算法的名字已经很好地描述了它的工作过程分为标记和清除两个阶段。标记阶段的深入理解标记阶段的核心任务是找出所有仍然存活的对象。但是JVM如何判断一个对象是否还存活呢采用的是可达性分析算法。这个算法的基本思路是从一系列称为GC Roots的对象开始向下搜索形成一个引用链。如果一个对象到GC Roots没有任何引用链相连用图论的话说就是从GC Roots到这个对象不可达那么这个对象就是垃圾可以被回收。那么哪些对象可以作为GC Roots呢主要包括虚拟机栈中引用的对象方法的局部变量、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象、以及JVM内部的引用等。这些对象被认为是根对象从它们出发可以追踪到所有仍在使用的对象。标记过程需要遍历整个对象图这是一个相对耗时的过程。而且为了保证标记的准确性在标记期间必须暂停所有应用线程Stop The World否则对象的引用关系可能会在标记过程中发生变化导致误判。清除阶段的工作方式标记完成后所有对象就被分为了两类被标记的存活对象和未被标记的垃圾对象。清除阶段的任务就是回收未被标记对象占用的内存。需要注意的是“清除并不是真的将内存清零而是将可回收对象所占用的内存加入到空闲列表”Free List中。当后续需要分配内存时就从空闲列表中寻找合适大小的空闲块。算法的优缺点分析标记-清除算法的优点是概念简单、实现相对容易。它不需要移动对象这在某些场景下是有利的比如对象移动会导致引用关系更新的开销。但它的缺点也很明显主要有两个第一个缺点是效率问题。标记和清除两个过程的效率都不高特别是当堆中对象很多时需要标记和清除的对象数量巨大。而且这两个阶段都需要遍历整个内存空间。第二个缺点也是更致命的是会产生大量的内存碎片。清除后内存空间中会出现大量不连续的小块空闲空间。这些碎片化的空间很难被利用可能会出现明明总的空闲内存足够却无法分配一个稍大的对象的情况。内存碎片会导致不得不提前进行垃圾回收甚至可能触发Full GC严重影响性能。正因为这些缺点标记-清除算法通常不会单独使用而是与其他算法组合使用。比如CMS收集器就是基于标记-清除算法的但它通过并发标记等技术来减少停顿时间并定期进行内存整理来解决碎片问题。示意图标记前 [对象A][对象B][对象C][对象D][对象E] ↓ ↓ ↓ ↓ ↓ 存活 垃圾 存活 垃圾 存活 标记后 [对象A][ ][对象C][ ][对象E] ✓ ✓ ✓ 清除后产生碎片可能无法分配大对象2.1.2 标记-复制算法Mark-Copy为年轻代而生的算法标记-复制算法也常被简称为复制算法是1969年由Fenichel提出的它巧妙地解决了标记-清除算法的内存碎片问题。这个算法的核心思想是将内存分为两块每次只使用其中一块GC时将存活对象复制到另一块区域然后清空当前区域。算法的工作机制让我们详细看看这个算法是如何工作的。假设我们将内存分为A区和B区两块大小相等的区域。开始时所有对象都分配在A区B区保持空闲。当A区满了之后触发垃圾回收首先进行标记找出A区中所有存活的对象。然后不是就地清除垃圾而是将这些存活对象按顺序复制到B区。复制完成后A区中的所有内存包括存活对象和垃圾对象都可以一次性清空。下一次分配时就在B区分配。当B区满了再次GC时就将B区的存活对象复制回A区如此循环往复。这个算法有个非常聪明的地方复制时存活对象是按顺序紧密排列的自然就没有碎片。而且清空内存区域时不需要逐个回收对象而是整块清空效率极高。内存分配的简化复制算法还带来了一个额外的好处简化了内存分配策略。在使用标记-清除算法时内存中到处都是碎片分配内存需要在空闲列表中搜索合适大小的空闲块这个过程比较复杂。而在复制算法中由于所有对象都是紧密排列的内存分配就变得非常简单只需要使用一个指针记录已使用内存的边界分配新对象时只需要将指针向前移动相应的大小即可。这种方式被称为指针碰撞Bump the Pointer速度非常快几乎与在栈上分配内存一样快。算法的适用场景复制算法在理论上很完美但它有一个显而易见的缺点内存利用率只有50%。一半的内存永远处于空闲状态这在内存资源宝贵的情况下是无法接受的。但是如果我们换个角度思考在对象大量死亡、只有少量存活的场景下复制算法就变得非常高效了。需要复制的对象很少而且通过复制就自动解决了碎片问题。而且实际上不需要将内存对半分——如果知道对象的存活率很低就可以让使用区更大、空闲区更小。这正是年轻代的特点研究表明年轻代中的对象有98%在第一次GC时就会死亡。既然绝大多数对象都会死亡那么复制算法就是最合适的选择。现代JVM正是基于这个观察在年轻代采用了改进的复制算法。不是简单地把内存对半分而是分为一个较大的Eden区和两个较小的Survivor区比例为8:1:1。每次使用Eden区和其中一个Survivor区GC时将存活对象复制到另一个Survivor区。这样内存利用率就提高到了90%只有10%的空间是空闲的。而且即使在极端情况下存活对象过多、Survivor区放不下也有老年代作为担保多出来的对象可以直接晋升到老年代。这个机制叫做分配担保Handle Promotion确保复制算法在任何情况下都能正常工作。示意图GC前使用From区 From区: [A][B][C][D][E] To区: [空闲] GC后复制存活对象到To区 From区: [已清空] To区: [A][C][E] 下次GC时From和To互换2.1.3 标记-整理算法Mark-Compact老年代的最佳选择标记-整理算法也称标记-压缩算法可以看作是标记-清除算法的改进版本。它保留了标记阶段但将清除阶段改为整理阶段从而解决了内存碎片问题同时又避免了复制算法的内存浪费问题。算法的工作流程标记-整理算法的执行过程可以分为三个步骤第一步是标记阶段与标记-清除算法完全相同从GC Roots开始遍历对象图标记所有可达的存活对象。第二步是整理阶段这是与标记-清除算法的关键区别。整理阶段会将所有存活对象向内存的一端移动让它们紧密地排列在一起。这个过程类似于整理书架把所有的书都挤到一边让空闲空间集中到另一边。第三步是清理阶段直接清理掉边界外的所有内存。由于所有存活对象都被移到了一端边界另一端的所有内存都是垃圾可以一次性清理掉。为什么需要移动对象你可能会问移动对象不是很麻烦吗需要更新所有指向这些对象的引用这不是很耗时吗确实移动对象是有代价的但这个代价是值得的。首先移动对象后内存变得紧凑没有任何碎片。这意味着后续的内存分配可以使用简单快速的指针碰撞方式不需要维护复杂的空闲列表也不需要搜索合适大小的空闲块。其次虽然移动对象需要更新引用但现代JVM有很多技术来优化这个过程比如使用句柄Handle、转发指针Forwarding Pointer等使得引用更新的开销可以接受。最重要的是避免内存碎片带来的长期收益远大于移动对象的一次性开销。内存碎片会导致频繁GC、降低内存利用率、甚至引发OutOfMemoryError这些问题的代价要比移动对象大得多。老年代为什么选择标记-整理标记-整理算法特别适合老年代原因在于老年代的对象特点首先老年代中大部分对象都是长期存活的对象的存活率很高通常超过90%。在这种情况下如果使用复制算法需要复制大量对象而且需要预留同样大小的空间用于复制内存利用率太低。使用标记-整理算法虽然也需要移动对象但不需要额外的空间内存利用率高。其次老年代的GC频率较低。虽然标记-整理算法移动对象需要一定时间但由于老年代GC不频繁这个开销可以接受。相比之下如果老年代使用标记-清除算法产生的内存碎片会长期存在持续影响性能。最后老年代的对象通常比较大。大对象对内存碎片更敏感因为需要连续的大块空闲空间才能分配。使用标记-整理算法确保了总有连续的大块空闲空间可用。因此大多数针对老年代的垃圾回收器如Serial Old、Parallel Old、G1在Mixed GC阶段等都采用了标记-整理算法或其变种。示意图标记前 [A][B][C][D][E][F][G] ↓ ↓ ↓ ↓ ↓ ↓ ↓ 存活 死 存活 死 存活 死 存活 整理后 [A][C][E][G][ ] ↓ ↓ ↓ ↓ 清空 连续的存活对象 可用空间2.2 垃圾回收器详解2.2.1 Serial收集器特点单线程收集器进行GC时必须暂停所有工作线程Stop The World简单高效适合单CPU环境Client模式下默认的年轻代收集器适用场景单核CPU或CPU核心数少的环境桌面应用程序堆内存较小的应用几十MB到一两百MB启用参数-XX:UseSerialGC# 年轻代和老年代都使用串行收集器GC日志示例[GC (Allocation Failure) [DefNew: 4416K-512K(4928K), 0.0042640 secs] 4416K-1520K(15872K), 0.0043140 secs]工作流程应用线程运行 ↓ 发生GC ↓ Stop The World所有应用线程暂停 ↓ Serial收集器工作单线程 ↓ GC完成 ↓ 恢复应用线程2.2.2 Parallel收集器吞吐量优先特点多线程并行收集关注吞吐量CPU用于运行用户代码的时间与CPU总消耗时间的比值JDK 8默认的收集器也称为吞吐量优先收集器Parallel Scavenge年轻代使用复制算法多线程并行收集可控制吞吐量Parallel Old老年代使用标记-整理算法多线程并行收集适用场景后台计算任务不需要太多交互的任务对吞吐量要求高对停顿时间要求不严格启用参数-XX:UseParallelGC# 年轻代使用Parallel Scavenge-XX:UseParallelOldGC# 老年代使用Parallel Old-XX:ParallelGCThreads4# 设置并行GC线程数-XX:MaxGCPauseMillis100# 设置最大GC停顿时间毫秒-XX:GCTimeRatio99# 设置吞吐量大小默认99即1%的时间用于GC-XX:UseAdaptiveSizePolicy# 自动调节年轻代大小、Eden和Survivor比例等性能对比假设堆内存1GB4核CPU Serial收集器 - GC线程1个 - GC时间100ms - 总停顿100ms Parallel收集器 - GC线程4个 - GC时间30ms理论值实际约40ms - 总停顿40ms - 吞吐量提升约60%2.2.3 CMS收集器Concurrent Mark SweepCMS是一款以获取最短停顿时间为目标的收集器非常适合互联网应用和B/S架构的服务端应用。特点并发收集、低停顿基于标记-清除算法只作用于老年代大部分工作可以与应用线程并发执行工作流程四个阶段初始标记Initial Mark- STW仅标记GC Roots直接关联的对象速度很快停顿时间短并发标记Concurrent Mark从GC Roots直接关联对象开始遍历整个对象图与应用线程并发执行耗时最长但不需要停顿重新标记Remark- STW修正并发标记期间因用户程序继续运行而导致标记变动的对象停顿时间比初始标记稍长但远比并发标记短并发清除Concurrent Sweep清除标记为垃圾的对象与应用线程并发执行时间线示意时间 → |---初始标记(STW)---|并发标记|---重新标记(STW)---|并发清除| 应用停顿 应用继续运行 应用停顿 应用继续运行 (很短) (最耗时) (较短) (耗时)启用参数-XX:UseConcMarkSweepGC# 使用CMS收集器-XX:CMSInitiatingOccupancyFraction70# 老年代使用70%时触发CMS默认68%-XX:UseCMSInitiatingOccupancyOnly# 只使用设定的回收阈值-XX:ConcGCThreads4# 并发GC线程数-XX:CMSParallelRemarkEnabled# 降低重新标记停顿时间-XX:CMSScavengeBeforeRemark# 重新标记前先进行一次年轻代GC-XX:UseCMSCompactAtFullCollection# Full GC后进行碎片整理-XX:CMSFullGCsBeforeCompaction5# 多少次Full GC后进行碎片整理优点并发收集停顿时间短适合对响应时间敏感的应用用户体验好缺点对CPU资源敏感并发阶段会占用部分CPU资源默认启动的回收线程数(CPU核心数 3) / 4在CPU核心少时影响应用性能无法处理浮动垃圾并发标记和并发清除阶段用户线程仍在运行会产生新的垃圾这部分垃圾只能等到下次GC清理需要预留足够内存给用户线程使用产生内存碎片基于标记-清除算法会产生大量碎片可能导致老年代还有很多空间但无法分配大对象不得不提前触发Full GC适用场景互联网网站、B/S架构服务端对响应时间要求高的应用堆内存较大4GB-20GB多核CPU服务器2.2.4 G1收集器Garbage First面向未来的垃圾回收器G1Garbage First收集器是垃圾回收技术的一个里程碑。它从JDK 7开始引入经过多年的优化和改进在JDK 9中成为默认的垃圾回收器。G1的设计目标雄心勃勃既要保证高吞吐量又要实现可预测的低延迟还要能够处理大堆内存几十GB甚至上百GB。这些目标在传统的垃圾回收器中往往是矛盾的但G1通过一系列创新的设计在很大程度上实现了这些目标的平衡。G1的设计理念全新的内存模型G1最大的创新在于彻底改变了堆内存的布局方式。传统的垃圾回收器如CMS将堆内存划分为固定的年轻代和老年代这两个区域在物理上是连续的。而G1引入了全新的Region区域概念将整个堆内存划分为多个大小相等的独立区域。每个Region的大小通常在1MB到32MB之间必须是2的幂次默认情况下G1会将堆划分为约2048个Region。这些Region在逻辑上可以分为Eden区、Survivor区、Old区和Humongous区用于存放大对象但在物理上它们是不连续的可以分散在堆的任何位置。这种设计有什么好处呢最大的好处是灵活性。在传统的分代收集器中年轻代和老年代的大小比例是相对固定的调整起来比较麻烦。而在G1中一个Region可以灵活地在不同角色之间转换。今天它是Eden区经过一次GC后可能变成空闲Region下次可能被用作Old区。这种动态分配使得G1能够根据应用的实际情况自动调整各个代的大小。可预测的停顿时间G1的核心优势G1最吸引人的特性是可以设置期望的GC停顿时间目标。通过-XX:MaxGCPauseMillis参数你可以告诉G1“我希望每次GC的停顿时间不超过200毫秒”。G1会尽力注意不是保证达到这个目标。G1是如何做到的呢关键在于它可以选择性地回收Region。G1会跟踪每个Region中的垃圾比例并估算回收每个Region所需的时间。在GC时G1不会回收所有的Region而是优先选择收益最高的Region进行回收——即那些垃圾比例高、回收时间短的Region。这就是Garbage First名字的由来优先回收垃圾最多的区域。通过这种机制G1可以在有限的时间内停顿时间目标回收尽可能多的垃圾。如果停顿时间目标设置得比较紧G1可能只回收几个Region如果目标比较宽松G1就可以回收更多Region获得更高的回收效率。G1的回收过程年轻代GC与混合GCG1的垃圾回收分为两种类型年轻代GCYoung GC和混合GCMixed GC。年轻代GC与传统收集器类似当所有Eden Region被占满时触发。G1会回收所有的Eden Region和Survivor Region将存活对象复制到新的Survivor Region或晋升到Old Region。年轻代GC是完全Stop The World的但由于年轻代对象死亡率高这个过程通常很快。混合GC是G1独有的特性。当堆内存使用率达到一定阈值时默认45%可通过-XX:InitiatingHeapOccupancyPercent设置G1会启动一个并发标记周期标记整个堆中的存活对象。标记完成后G1就知道了每个Region的垃圾比例。接下来的若干次GC就不只是回收年轻代还会选择一些垃圾比例高的Old Region一起回收这就是混合GC。混合GC的好处是可以渐进式地回收老年代避免传统的Full GC那种一次性回收所有老年代的长时间停顿。通过多次混合GC分批回收老年代每次停顿时间都可控。G1堆内存布局每个格子代表一个Region [E][E][E][S][O][O][O][H] [E][E][S][O][O][O][H][O] [E][E][E][O][O][O][O][O] [E][S][O][O][O][H][O][O] E Eden区 S Survivor区 O Old区老年代 H Humongous区大对象超过Region 50%的对象特点Region化内存布局不再区分年轻代和老年代的物理空间每个Region大小1MB-32MB必须是2的幂次大对象直接分配到Humongous区可预测的停顿时间可以设置期望停顿时间-XX:MaxGCPauseMillisG1会根据历史数据预测每个Region的回收价值优先回收价值最大的Region并发与并行并行多个GC线程同时工作停顿期间并发GC线程与应用线程同时工作工作流程年轻代GCYoung GC当Eden区用完时触发采用复制算法完全STW但速度很快存活对象复制到Survivor或晋升到Old区混合GCMixed GC当堆内存使用达到一定阈值时触发同时回收年轻代和部分老年代RegionFull GC当Mixed GC无法跟上内存分配速度时触发单线程执行停顿时间长应尽量避免Full GCG1 GC详细阶段1. 年轻代GCYoung GC- STW - 清空Eden区 - 复制存活对象到Survivor或Old区 - 暂停时间可控 2. 并发标记周期当老年代使用率达到阈值 a. 初始标记Initial Mark- STW - 标记GC Roots直接关联的对象 - 通常伴随Young GC一起进行 b. 根区域扫描Root Region Scan - 扫描Survivor区对老年代的引用 - 必须在下次Young GC前完成 c. 并发标记Concurrent Mark - 标记整个堆的存活对象 - 与应用线程并发执行 - 可被Young GC中断 d. 重新标记Remark- STW - 完成标记工作 - 使用SATBSnapshot At The Beginning算法 e. 清理Cleanup- 部分STW - 统计每个Region的存活对象 - 回收完全空闲的Region - 重置RSetRemembered Set 3. 混合GCMixed GC - 选择收益最大的若干Region进行回收 - 包括所有年轻代Region和部分老年代Region启用参数# 基础参数-XX:UseG1GC# 使用G1收集器-XX:MaxGCPauseMillis200# 设置期望的最大GC停顿时间毫秒默认200ms-XX:G1HeapRegionSize16m# 设置Region大小范围1MB-32MB# 并发标记相关-XX:InitiatingHeapOccupancyPercent45# 堆使用率达到45%时启动并发标记默认45-XX:ConcGCThreads4# 并发GC线程数# Mixed GC相关-XX:G1MixedGCCountTarget8# 一次并发标记后最多执行8次Mixed GC-XX:G1OldCSetRegionThresholdPercent10# Mixed GC时老年代Region回收的最大比例-XX:G1MixedGCLiveThresholdPercent85# Region中存活对象超过85%不会被选入CSet# 大对象相关-XX:G1HeapWastePercent5# 允许的浪费堆空间百分比默认5%性能调优建议不要设置年轻代大小G1会自动调整年轻代大小以满足停顿时间目标手动设置会影响G1的自适应能力合理设置停顿时间目标不要设置过小的值如50ms可能导致频繁GC推荐值200ms-500ms设置过小可能导致达不到目标反而降低吞吐量观察是否发生Full GC# 如果频繁Full GC可以- 增加堆内存 - 调整InitiatingHeapOccupancyPercent提前触发并发标记 - 增加并发标记线程数适用场景堆内存较大6GB以上推荐8GB-64GB需要可预测的停顿时间服务端应用替代CMS的首选方案G1 vs CMS对比特性CMSG1内存布局连续的年轻代/老年代Region化不连续停顿时间不可预测可预测内存碎片有碎片问题整理内存碎片少大堆支持较差8GB性能下降好可到64GB吞吐量较低较高适用堆大小4GB-8GB6GB-64GB2.2.5 ZGC收集器Z Garbage CollectorZGC是JDK 11引入的一款低延迟垃圾收集器目标是让GC停顿时间不超过10ms。特点停顿时间极短10ms支持TB级别的堆内存吞吐量下降不超过15%使用染色指针Colored Pointer和读屏障Load Barrier技术核心技术染色指针Colored Pointer在64位指针中存储对象的状态信息不需要额外的空间存储标记信息读屏障Load Barrier在对象访问时插入一小段代码实现并发移动对象启用参数-XX:UseZGC# 使用ZGC-XX:ZCollectionInterval120# GC间隔时间秒-XX:ZAllocationSpikeTolerance2# 内存分配尖峰容忍度适用场景大内存服务器16GB以上对延迟极度敏感的应用金融交易系统实时数据处理局限性需要JDK 11目前只支持Linux x64平台JDK 14开始支持Windows和macOS相比G1吞吐量有所下降2.3 如何选择垃圾回收器选择合适的垃圾回收器需要考虑多个因素选择决策树 应用类型 ├─ 单核/桌面应用 │ └─ Serial / Serial Old │ ├─ 多核对吞吐量要求高可接受较长停顿 │ └─ Parallel Scavenge Parallel Old │ ├─ 多核对响应时间敏感堆内存8GB │ └─ CMSParNew CMS │ ├─ 多核需要可预测停顿堆内存6GB-64GB │ └─ G1 │ └─ 大内存对延迟极度敏感堆内存16GB └─ ZGC具体场景推荐应用类型堆内存大小推荐收集器理由桌面应用200MBSerial简单高效停顿时间可接受后台批处理任意Parallel吞吐量优先停顿无所谓普通Web应用4GBParallel平衡吞吐量和停顿电商/支付4GB-8GBCMS响应时间敏感微服务2GB-8GBG1可预测停顿易于调优大数据/缓存8GBG1大堆支持好交易系统16GBZGC极低延迟要求JDK版本与默认收集器JDK 8Parallel Scavenge Parallel OldJDK 9-13G1JDK 14G1推荐使用ZGC for低延迟场景三、JVM参数调优实战3.1 JVM参数分类JVM参数主要分为三类JVM参数分类 ├── 标准参数-开头 │ ├── -version 查看JVM版本 │ ├── -help 查看帮助 │ ├── -cp/-classpath 设置类路径 │ └── 所有JVM都支持稳定不变 │ ├── X参数-X开头非标准参数 │ ├── -Xms 初始堆大小 │ ├── -Xmx 最大堆大小 │ ├── -Xmn 年轻代大小 │ ├── -Xss 线程栈大小 │ └── 所有JVM都支持但可能有差异 │ └── XX参数-XX:开头不稳定参数 ├── Boolean类型-XX:[参数名] 启用 │ -XX:-[参数名] 禁用 │ 示例-XX:UseG1GC │ └── KV类型-XX:[参数名][值] 示例-XX:MaxGCPauseMillis2003.2 堆内存参数详解3.2.1 基础堆内存参数# 堆内存大小设置-Xms4g# 初始堆大小4GB-Xmx4g# 最大堆大小4GB建议与Xms相同避免动态扩容-Xmn1g# 年轻代大小1GB一般为堆的1/3到1/4# 为什么Xms和Xmx要设置相同# 1. 避免运行时堆扩容扩容会导致Full GC# 2. 减少内存碎片# 3. 性能更稳定可预测内存大小单位k 或 K# KB (kilobytes)m 或 M# MB (megabytes)g 或 G# GB (gigabytes)# 示例-Xms512m# 512MB-Xmx4G# 4GB-Xmn1024m# 1GB3.2.2 年轻代参数# 方式1直接指定年轻代大小-Xmn2g# 年轻代大小2GB# 方式2通过比例设置不推荐-XX:NewRatio2# 年轻代与老年代的比例 1:2# 即年轻代占堆的1/3# Eden和Survivor比例-XX:SurvivorRatio8# Eden : Survivor0 : Survivor1 8:1:1# 默认值就是8# 示例堆4GB年轻代1GBSurvivorRatio8# 则内存分布为# Eden: 800MB (1GB * 8/10)# S0: 100MB (1GB * 1/10)# S1: 100MB (1GB * 1/10)# Old: 3GB年轻代大小如何设置# 经验法则# 1. 年轻代一般设置为堆的1/4到1/3# 2. 年轻代太小Minor GC频繁对象过早进入老年代# 3. 年轻代太大Minor GC时间长老年代空间不足# 不同应用类型推荐# 短生命周期对象多Web应用年轻代可适当大一些1/3# 长生命周期对象多缓存应用年轻代可适当小一些1/43.2.3 老年代参数# 对象晋升年龄阈值-XX:MaxTenuringThreshold15# 对象在Survivor区经过15次GC后晋升到老年代# 默认值15CMS为6# 范围0-15# 大对象直接进入老年代的阈值-XX:PretenureSizeThreshold3m# 大于3MB的对象直接分配到老年代# 默认为0即不设限制# 仅对Serial和ParNew有效# 晋升担保参数-XX:HandlePromotionFailure# 允许担保失败JDK 6 Update 24之后默认开启3.3 垃圾回收参数详解3.3.1 通用GC参数# GC日志参数JDK 8-XX:PrintGC# 打印GC简要信息-XX:PrintGCDetails# 打印GC详细信息-XX:PrintGCTimeStamps# 打印GC时间戳相对JVM启动时间-XX:PrintGCDateStamps# 打印GC日期时间戳-XX:PrintHeapAtGC# GC前后打印堆信息-Xloggc:/path/to/gc.log# GC日志输出到文件# GC日志参数JDK 9统一日志-Xlog:gc# 基本GC日志-Xlog:gc*# 详细GC日志-Xlog:gc:file/path/to/gc.log# 输出到文件-Xlog:gc*:file/path/to/gc.log:time,uptime,level,tags# 完整格式# GC日志文件管理-XX:UseGCLogFileRotation# 启用GC日志滚动-XX:NumberOfGCLogFiles10# GC日志文件数量-XX:GCLogFileSize100M# 每个GC日志文件大小# 其他有用参数-XX:PrintGCApplicationStoppedTime# 打印应用停顿时间-XX:PrintGCApplicationConcurrentTime# 打印应用运行时间-XX:PrintTenuringDistribution# 打印对象年龄分布-XX:PrintReferenceGC# 打印引用处理信息3.3.2 Parallel收集器参数# 启用Parallel收集器-XX:UseParallelGC# 年轻代使用Parallel Scavenge-XX:UseParallelOldGC# 老年代使用Parallel Old# JDK 8中设置其中一个另一个自动启用# 并行GC线程数-XX:ParallelGCThreads8# 设置并行GC线程数# 默认值CPU核心数核心数8# 默认值3 (5 * CPU核心数 / 8)核心数8# 性能目标设置-XX:MaxGCPauseMillis200# 最大GC停顿时间目标毫秒# JVM会尝试调整堆大小和其他参数来达到目标# 不是硬性保证-XX:GCTimeRatio99# 设置吞吐量大小# 公式吞吐量 1 - 1/(1GCTimeRatio)# 99表示1%的时间用于GC99%用于应用# 默认值99# 自适应调节策略-XX:UseAdaptiveSizePolicy# 启用自适应策略默认开启# JVM自动调整年轻代大小、Eden/Survivor比例、# 晋升阈值等参数以达到性能目标-XX:-UseAdaptiveSizePolicy# 禁用自适应策略# 示例吞吐量优先配置后台批处理-Xms8g -Xmx8g -XX:UseParallelGC -XX:ParallelGCThreads8-XX:GCTimeRatio99-XX:UseAdaptiveSizePolicy3.3.3 CMS收集器参数# 启用CMS-XX:UseConcMarkSweepGC# 老年代使用CMS-XX:UseParNewGC# 年轻代使用ParNewCMS自动启用# 触发CMS GC的时机-XX:CMSInitiatingOccupancyFraction70# 老年代使用70%时触发CMS# 默认68%JDK 6# 设置过高可能来不及回收导致Concurrent Mode Failure# 设置过低GC过于频繁浪费CPU-XX:UseCMSInitiatingOccupancyOnly# 只使用设定的阈值触发CMS# 不使用JVM的动态计算# 并发线程数-XX:ConcGCThreads4# CMS并发线程数# 默认(ParallelGCThreads 3) / 4-XX:ParallelGCThreads8# 并行GC线程数用于STW阶段# 优化重新标记阶段-XX:CMSParallelRemarkEnabled# 启用并行重新标记默认开启-XX:CMSScavengeBeforeRemark# 重新标记前先进行一次Minor GC# 减少年轻代对象对老年代的引用缩短重新标记时间# 内存碎片处理-XX:UseCMSCompactAtFullCollection# Full GC时进行碎片整理默认开启# 但整理会STW时间较长-XX:CMSFullGCsBeforeCompaction5# 多少次Full GC后进行一次碎片整理# 默认0每次Full GC都整理# 设置为5每5次Full GC后整理一次# 类卸载-XX:CMSClassUnloadingEnabled# 允许CMS回收方法区永久代/元空间# JDK 8默认开启# 增量模式已废弃不推荐-XX:CMSIncrementalMode# CMS增量模式JDK 9已移除# 失败处理# 如果CMS运行期间无法满足内存分配需求会出现Concurrent Mode Failure# 此时会退化为Serial Old进行Full GC停顿时间很长# 解决方案# 1. 降低CMSInitiatingOccupancyFraction提前触发CMS# 2. 增加堆内存# 3. 优化代码减少对象创建# 示例低延迟配置Web应用-Xms6g -Xmx6g -Xmn2g -XX:UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction70-XX:UseCMSInitiatingOccupancyOnly -XX:CMSScavengeBeforeRemark -XX:CMSParallelRemarkEnabled -XX:ParallelGCThreads8-XX:ConcGCThreads23.3.4 G1收集器参数# 启用G1-XX:UseG1GC# 使用G1收集器JDK 9默认# Region大小-XX:G1HeapRegionSize16m# 设置Region大小1MB-32MB必须是2的幂# 默认堆大小 / 2048# 目标是有2048个Region# 停顿时间目标-XX:MaxGCPauseMillis200# 期望的最大GC停顿时间毫秒# 默认200ms# 这是一个软目标不是硬性保证# 不要设置过小否则降低吞吐量# 并发标记相关-XX:InitiatingHeapOccupancyPercent45# 堆使用率达到45%时启动并发标记周期# 默认45# IHOP越小越早触发并发标记越不容易Full GC-XX:ConcGCThreads4# 并发标记的线程数# 默认ParallelGCThreads / 4-XX:ParallelGCThreads8# 并行GC线程数STW阶段# 默认CPU核心数核心8# Mixed GC相关-XX:G1MixedGCCountTarget8# 一次并发标记周期后目标执行的Mixed GC次数# 默认8# 增加此值可以减少每次Mixed GC的停顿时间-XX:G1HeapWastePercent5# 允许的堆空间浪费百分比# 默认5# 当可回收空间小于这个值时不启动Mixed GC-XX:G1MixedGCLiveThresholdPercent85# Region中存活对象超过85%不会被选入CSet# 默认85# 避免回收价值不高的Region-XX:G1OldCSetRegionThresholdPercent10# Mixed GC时老年代Region数量最大占比# 默认10# 大对象相关-XX:G1ReservePercent10# 保留的堆空间百分比防止晋升失败# 默认10# 记忆集Remembered Set相关-XX:G1RSetUpdatingPauseTimePercent10# 允许用于更新RSet的停顿时间百分比# 默认10# StringDeduplication字符串去重-XX:UseStringDeduplication# 启用字符串去重-XX:StringDeduplicationAgeThreshold3# 字符串达到此年龄后进行去重检查# 示例1标准Web应用配置-Xms8g -Xmx8g -XX:UseG1GC -XX:MaxGCPauseMillis200-XX:InitiatingHeapOccupancyPercent45-XX:ParallelGCThreads8-XX:ConcGCThreads2# 示例2大堆内存配置16GB-Xms16g -Xmx16g -XX:UseG1GC -XX:G1HeapRegionSize32m -XX:MaxGCPauseMillis200-XX:InitiatingHeapOccupancyPercent40-XX:G1ReservePercent15# 示例3低延迟配置-Xms8g -Xmx8g -XX:UseG1GC -XX:MaxGCPauseMillis100-XX:InitiatingHeapOccupancyPercent35-XX:G1ReservePercent15-XX:ParallelGCThreads8-XX:ConcGCThreads4G1调优建议不要手动设置年轻代大小-Xmn、-XX:NewRatioG1会自动调整以满足停顿时间目标不要设置过于激进的停顿时间目标设置过小会频繁GC降低吞吐量推荐200ms起步观察GC日志关注Full GCFull GC说明调优不当可以降低IHOP提前触发并发标记大对象优化避免创建超过Region 50%的对象考虑拆分大对象3.3.5 ZGC收集器参数# 启用ZGC-XX:UseZGC# 使用ZGC需要JDK 11# 并发线程数-XX:ConcGCThreads4# 并发GC线程数# 默认CPU核心数 / 8# GC触发时机-XX:ZCollectionInterval0# GC间隔时间秒# 默认0不基于时间触发-XX:ZAllocationSpikeTolerance2# 内存分配尖峰容忍度# 默认2# 示例ZGC配置大内存、低延迟-Xms32g -Xmx32g -XX:UseZGC -XX:ConcGCThreads8-Xlog:gc*:file/path/to/gc.log3.4 元空间参数# 元空间大小JDK 8-XX:MetaspaceSize256m# 初始元空间大小# 默认约21MB平台相关# 达到此值会触发Full GC-XX:MaxMetaspaceSize512m# 最大元空间大小# 默认无限制只受系统内存限制# 建议设置上限防止内存泄漏-XX:MinMetaspaceFreeRatio40# 最小空闲比例-XX:MaxMetaspaceFreeRatio70# 最大空闲比例# 用于控制元空间的扩容和缩容# 永久代大小JDK 7及以前-XX:PermSize256m# 初始永久代大小-XX:MaxPermSize512m# 最大永久代大小元空间调优建议# 问题频繁Full GC日志显示Metadata GC Threshold# 原因元空间不足频繁触发Full GC# 解决增大MetaspaceSize# 典型配置-XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m# 大型应用Spring Boot、微服务-XX:MetaspaceSize512m -XX:MaxMetaspaceSize1024m# 动态类加载多的应用Groovy、反射多-XX:MetaspaceSize1g -XX:MaxMetaspaceSize2g3.5 线程栈参数# 线程栈大小-Xss512k# 每个线程的栈大小为512KB# 默认1MBLinux/Windows# 512KBmacOS# 栈大小影响# 1. 栈太小StackOverflowError递归调用深度受限# 2. 栈太大浪费内存能创建的线程数变少# 线程数计算公式# 最大线程数 ≈ (系统内存 - Xmx - MaxMetaspaceSize) / Xss线程栈大小建议应用类型推荐值说明普通应用512k-1m默认值递归深的应用2m-4m避免StackOverflowError高并发线程数多256k-512k节省内存支持更多线程3.6 性能监控与诊断参数# OOM时自动dump堆-XX:HeapDumpOnOutOfMemoryError# OOM时自动生成堆转储文件-XX:HeapDumpPath/path/to/dumps/# 堆转储文件保存路径# JMX监控-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port9999-Dcom.sun.management.jmxremote.authenticatefalse -Dcom.sun.management.jmxremote.sslfalse# 启用JFRJava Flight Recorder-XX:UnlockCommercialFeatures# JDK 8需要JDK 11不需要-XX:FlightRecorder -XX:StartFlightRecordingduration60s,filename/path/to/recording.jfr# 性能相关-XX:AlwaysPreTouch# 启动时预先分配物理内存# 避免运行时因分配内存导致延迟# 适合对延迟敏感的应用-XX:UseLargePages# 使用大页内存需要系统支持# 减少TLB miss提升性能3.7 实战场景参数配置场景1电商Web应用4核8GB服务器# 特点# - 高并发对响应时间敏感# - 对象生命周期短# - 需要低延迟java -jar application.jar\-Xms4g\-Xmx4g\-Xmn1g\-Xss512k\-XX:MetaspaceSize256m\-XX:MaxMetaspaceSize512m\-XX:UseG1GC\-XX:MaxGCPauseMillis200\-XX:ParallelGCThreads4\-XX:ConcGCThreads1\-XX:InitiatingHeapOccupancyPercent45\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath/logs/heapdump/\-Xlog:gc*:file/logs/gc.log:time,uptime,level,tags场景2大数据处理应用16核32GB服务器# 特点# - 吞吐量优先# - 对停顿时间不敏感# - 批处理任务java -jar batch-processor.jar\-Xms28g\-Xmx28g\-Xmn8g\-Xss256k\-XX:MetaspaceSize512m\-XX:MaxMetaspaceSize1g\-XX:UseParallelGC\-XX:ParallelGCThreads16\-XX:GCTimeRatio99\-XX:UseAdaptiveSizePolicy\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath/logs/heapdump/\-Xlog:gc*:file/logs/gc.log:time,level,tags场景3微服务应用2核4GB容器# 特点# - 资源受限# - 容器化部署# - 需要快速启动java -jar microservice.jar\-Xms2g\-Xmx2g\-Xss256k\-XX:MetaspaceSize128m\-XX:MaxMetaspaceSize256m\-XX:UseG1GC\-XX:MaxGCPauseMillis200\-XX:UseContainerSupport\-XX:InitialRAMPercentage50.0\-XX:MaxRAMPercentage80.0\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath/logs/heapdump.hprof\-Xlog:gc:file/logs/gc.log场景4金融交易系统32核64GB服务器# 特点# - 极低延迟要求10ms# - 大内存# - 高并发java -jar trading-system.jar\-Xms48g\-Xmx48g\-Xss512k\-XX:MetaspaceSize512m\-XX:MaxMetaspaceSize1g\-XX:UseZGC\-XX:ConcGCThreads8\-XX:AlwaysPreTouch\-XX:UseLargePages\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath/logs/heapdump/\-Xlog:gc*:file/logs/gc.log:time,uptime,level,tags四、JVM监控工具详解4.1 命令行工具JVM问题排查的瑞士军刀JDK自带了一套功能强大的命令行工具它们是每个Java开发者和运维人员必须掌握的利器。这些工具虽然看起来不起眼没有华丽的图形界面但在实际的生产环境问题排查中它们往往是最快速、最有效的选择。特别是在很多生产环境中出于安全考虑无法使用图形界面工具这时候这些命令行工具就成了唯一的选择。这些工具的另一个优势是轻量级。它们不需要在目标JVM中安装任何agent不需要修改应用程序只需要连接到目标进程就可以获取信息。这意味着你可以在不影响应用运行的情况下进行监控和诊断这对生产环境来说至关重要。让我们逐一了解这些工具不仅要知道它们的基本用法更要理解在什么场景下使用它们最合适如何解读它们的输出信息以及在实际问题排查中如何组合使用这些工具。4.1.1 jps - 查看Java进程问题排查的第一步jpsJava Virtual Machine Process Status Tool是JVM进程状态工具它的作用类似于Linux的ps命令但专门用于列出Java进程。这个工具看似简单但却是所有JVM问题排查的第一步——你首先需要知道要排查哪个Java进程。在生产环境中可能同时运行着多个Java应用比如多个微服务、多个后台任务等。jps可以帮助你快速定位到目标进程的PID然后才能使用jstat、jmap等工具进行进一步的诊断。# 基本用法jps# 显示Java进程ID和主类名jps -l# 显示完整的类名或jar路径jps -m# 显示传递给main方法的参数jps -v# 显示JVM参数# 输出示例$ jps -l12345com.example.Application12346org.apache.catalina.startup.Bootstrap12347org.elasticsearch.bootstrap.Elasticsearch $ jps -v12345Application -Xms4g -Xmx4g -XX:UseG1GC4.1.2 jstat - 查看JVM统计信息性能监控的核心工具jstatJVM Statistics Monitoring Tool是我个人认为JDK自带工具中最实用、使用频率最高的一个。它可以实时显示JVM的各种运行数据包括类加载信息、垃圾收集统计、编译统计等是性能分析和问题排查的核心工具。jstat的强大之处在于它可以持续监控JVM的状态变化。与jmap等一次性工具不同jstat可以按指定的时间间隔反复采集数据让你看到JVM状态的动态变化。比如你可以观察Eden区是如何逐渐被填满的Minor GC的频率如何老年代的使用率是否在持续上升等等。这些动态信息对于理解应用的运行特征、发现潜在问题至关重要。在实际工作中当接到应用响应变慢、内存使用率高等问题报告时我通常第一时间就是用jstat查看GC情况。很多性能问题的根源都可以通过jstat快速定位是频繁的Minor GC导致的吗还是发生了Full GC老年代使用率是否异常这些问题的答案往往能指引后续的排查方向。# 基本语法jstat -optionpidintervalcount# option选项# -gc 垃圾收集统计# -gcutil 垃圾收集统计百分比# -gccause 垃圾收集统计 最近GC原因# -gcnew 年轻代统计# -gcold 老年代统计# -class 类加载统计# -compiler JIT编译统计# 查看GC情况每1秒输出一次共10次jstat -gc12345100010# 输出示例S0C S1C S0U S1U EC EU OC OU MC MU YGC YGCT FGC FGCT GCT102401024010240819204567820480010240051200480001561.23450.5671.801# 字段说明S0C: Survivor0容量(KB)S1C: Survivor1容量(KB)S0U: Survivor0使用量(KB)S1U: Survivor1使用量(KB)EC: Eden区容量(KB)EU: Eden区使用量(KB)OC: 老年代容量(KB)OU: 老年代使用量(KB)MC: 元空间容量(KB)MU: 元空间使用量(KB)YGC: Young GC次数 YGCT: Young GC总耗时(秒)FGC: Full GC次数 FGCT: Full GC总耗时(秒)GCT: 所有GC总耗时(秒)更详细的统计百分比jstat -gcutil123451000# 输出示例S0 S1 E O M CCS YGC YGCT FGC FGCT GCT10.00.055.850.093.788.21561.23450.5671.801# 字段说明S0: Survivor0使用率(%)S1: Survivor1使用率(%)E: Eden区使用率(%)O: 老年代使用率(%)M: 元空间使用率(%)CCS: 压缩类空间使用率(%)YGC: Young GC次数 YGCT: Young GC总耗时(秒)FGC: Full GC次数 FGCT: Full GC总耗时(秒)GCT: 所有GC总耗时(秒)查看GC原因jstat -gccause123451000# 输出示例S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC0.010.055.850.093.788.21571.24550.5671.812Allocation Failure No GC# LGCC: 最近一次GC的原因Last GC Cause# GCC: 当前GC的原因Current GC Cause实战技巧# 1. 持续监控输出到文件jstat -gcutil123451000gc_monitor.log# 2. 快速判断是否有Full GCjstat -gccause12345100010|grep-ifull# 3. 监控老年代增长速度watch-n1jstat -gc 12345 | tail -1# 4. 计算GC频率和平均时间# Young GC平均时间 YGCT / YGC# Full GC平均时间 FGCT / FGC4.1.3 jmap - 内存映像工具jmap用于生成堆转储快照heap dump和查看内存信息。# 1. 生成堆转储文件jmap -dump:formatb,file/tmp/heap.hprof12345# live选项只dump存活对象jmap -dump:live,formatb,file/tmp/heap_live.hprof12345# 2. 查看堆内存使用情况jmap -heap12345# 输出示例Attaching to process ID12345, please wait... Heap Configuration: MinHeapFreeRatio40MaxHeapFreeRatio70MaxHeapSize4294967296(4096.0MB)NewSize1073741824(1024.0MB)MaxNewSize1073741824(1024.0MB)OldSize3221225472(3072.0MB)NewRatio2SurvivorRatio8MetaspaceSize268435456(256.0MB)MaxMetaspaceSize536870912(512.0MB)G1HeapRegionSize16777216(16.0MB)Heap Usage: G1 Heap: regions256capacity4294967296(4096.0MB)used2147483648(2048.0MB)free2147483648(2048.0MB)50.0% used# 3. 查看对象统计按内存占用排序jmap -histo12345|head-20# 输出示例num#instances #bytes class name----------------------------------------------1:123456987654320[C2:98765456789012java.lang.String3:45678234567890byte[]4:1234598765432java.util.HashMap$Node# 4. 查看存活对象触发Full GCjmap -histo:live12345|head-20# 5. 查看类加载器统计jmap -clstats12345实战场景# 场景1分析内存泄漏# 1. 生成两个heap dump间隔一段时间jmap -dump:live,formatb,file/tmp/heap1.hprof12345# 等待10分钟jmap -dump:live,formatb,file/tmp/heap2.hprof12345# 2. 使用MAT工具对比两个文件找出持续增长的对象# 场景2排查OOM# 当应用即将OOM时手动dumpjmap -dump:formatb,file/tmp/oom_heap.hprof12345# 场景3快速查看内存占用最多的对象jmap -histo:live12345|head-20# 注意事项# 1. jmap -dump会触发Full GC生产环境慎用# 2. dump文件很大确保有足够磁盘空间# 3. dump过程中应用会暂停STW4.1.4 jstack - 线程堆栈工具jstack用于生成线程快照thread dump分析线程状态、死锁等问题。# 1. 生成线程dumpjstack12345/tmp/thread_dump.txt# 2. 检测死锁jstack -l12345# 3. 强制dump进程无响应时jstack -F12345# 线程dump输出示例http-nio-8080-exec-10#123 daemon prio5 os_prio0 tid0x00007f8b2c001000 nid0x1a2b waiting on condition [0x00007f8abc123000]java.lang.Thread.State: WAITING(parking)at sun.misc.Unsafe.park(Native Method)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)at com.example.Service.process(Service.java:123)# 线程状态说明RUNNABLE: 运行中 BLOCKED: 阻塞等待锁 WAITING: 等待wait、park TIMED_WAITING: 超时等待sleep、wait with timeout TERMINATED: 已终止实战场景# 场景1排查CPU 100%# 1. 找到进程IDjps -l# 2. 找到占用CPU高的线程top-H -p12345# 假设线程ID为 6827十进制# 3. 转换为十六进制printf%x\n6827# 输出1aab# 4. 在thread dump中查找 nid0x1aab 的线程jstack12345|grep-A20nid0x1aab# 场景2检测死锁jstack -l12345|grep-ideadlock-A20# 场景3分析线程状态分布jstack12345|grepjava.lang.Thread.State|sort|uniq-c# 输出示例# 15 RUNNABLE# 120 WAITING# 10 TIMED_WAITING# 5 BLOCKED# 场景4找出长时间等待的线程jstack12345|grep-A5WAITING4.1.5 jinfo - 配置信息工具# 1. 查看所有JVM参数jinfo12345# 2. 查看系统属性jinfo -sysprops12345# 3. 查看JVM flagsjinfo -flags12345# 输出示例Non-default VM flags: -XX:ConcGCThreads2-XX:G1HeapRegionSize16777216-XX:InitialHeapSize4294967296-XX:MaxHeapSize4294967296-XX:UseG1GC# 4. 查看特定参数值jinfo -flag MaxHeapSize12345# 输出-XX:MaxHeapSize4294967296# 5. 动态修改参数仅支持manageable标记的参数jinfo -flag PrintGC12345# 开启GC日志jinfo -flag -PrintGC12345# 关闭GC日志jinfo -flagPrintGCDetailstrue12345# 查看可动态修改的参数java -XX:PrintFlagsFinal -version|grepmanageable4.2 可视化工具4.2.1 JConsoleJConsole是JDK自带的图形化监控工具可以监控内存、线程、类、CPU等信息。启动方式# 1. 直接启动连接本地进程jconsole# 2. 远程连接需要配置JMXjconsolehostname:9999# JMX配置-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port9999-Dcom.sun.management.jmxremote.authenticatefalse -Dcom.sun.management.jmxremote.sslfalse主要功能概述CPU使用率、堆内存、线程数、类加载数内存堆内存、非堆内存使用情况可以手动执行GC线程线程列表、线程状态、死锁检测类已加载类数量、已卸载类数量VM摘要JVM参数、系统属性MBean查看和修改MBean属性适用场景快速查看JVM运行状态开发测试环境监控简单的性能分析4.2.2 VisualVMVisualVM是功能更强大的可视化监控工具支持插件扩展。启动方式# JDK 8及以前自带jvisualvm# JDK 9需要单独下载# https://visualvm.github.io/主要功能监视CPU使用率堆内存、元空间使用情况类加载数量线程数量线程线程状态时间线死锁检测线程dump抽样器CPU抽样找出占用CPU最多的方法内存抽样找出内存分配最多的类Profiler性能分析CPU profiling方法级性能分析内存profiling对象分配分析需要安装插件堆Dump分析加载heap dump文件对象实例查看OQL查询对象查询语言计算保留大小实战技巧# 1. 分析CPU热点# 监视 → Profiler → CPU → 运行应用 → 查看热点方法# 2. 分析内存分配# 监视 → Profiler → 内存 → 运行应用 → 查看分配最多的类# 3. 对比堆快照# 手动GC → 生成快照1 → 运行一段时间 → 手动GC → 生成快照2 → 对比# 4. OQL查询示例# 在heap dump中执行OQLselect* from java.lang.String s where s.value.length1000selectheap.objects(java.util.HashMap)推荐插件VisualGC可视化GC过程BTrace动态跟踪TDAThread Dump Analyzer线程dump分析4.2.3 JProfiler商业工具JProfiler是功能最强大的Java性能分析工具但需要商业授权。主要功能实时内存监控所有对象的内存占用垃圾回收活动内存泄漏检测CPU分析调用树热点方法方法调用图线程分析线程状态时间线线程历史记录死锁检测数据库分析JDBC调用SQL语句数据库性能瓶颈HTTP分析URL调用统计响应时间分析适用场景复杂的性能问题生产级性能调优需要详细分析报告4.2.4 Arthas阿里开源Arthas是阿里开源的Java诊断工具无需修改代码即可诊断线上问题。安装和启动# 1. 下载arthascurl-O https://arthas.aliyun.com/arthas-boot.jar# 2. 启动arthasjava -jar arthas-boot.jar# 3. 选择要诊断的Java进程# 会列出所有Java进程输入编号即可# 4. 也可以直接指定PIDjava -jar arthas-boot.jar12345常用命令# 1. dashboard - 实时数据面板dashboard# 显示# - 线程信息# - 内存信息# - GC信息# - 运行环境信息# 2. thread - 查看线程信息thread# 查看所有线程thread1# 查看1号线程thread -n3# 查看CPU使用率最高的3个线程thread -b# 查找阻塞的线程死锁thread --state WAITING# 查看WAITING状态的线程# 3. jvm - 查看JVM信息jvm# 4. memory - 查看内存信息memory# 5. heapdump - 生成heap dumpheapdump /tmp/heap.hprof# 6. watch - 监控方法执行watchcom.example.Service process{params, returnObj, throwExp}-x2# 7. trace - 追踪方法调用链trace com.example.Service process# 输出示例---ts2024-01-0110:00:00;thread_namehttp-nio-8080-exec-1;id1a;is_daemontrue;priority5;---[12.345ms]com.example.Service:process()---[2.123ms]com.example.Repository:query()---[8.456ms]com.example.Service:processData()---[1.234ms]com.example.Service:saveResult()# 8. stack - 查看方法调用堆栈stack com.example.Service process# 9. tt - 时间隧道记录方法调用tt -t com.example.Service process# 开始记录tt -l# 查看记录列表tt -i1000-p# 重放索引为1000的调用# 10. monitor - 方法执行监控monitor -c5com.example.Service process# 每5秒统计一次调用次数、成功次数、失败次数、平均耗时# 11. jad - 反编译jad com.example.Service# 12. sc - 查找类sc -d *Service*# 13. sm - 查找方法sm com.example.Service process*# 14. ognl - 执行OGNL表达式ognlcom.example.ConfigDEBUG_MODE# 15. profiler - 性能采样需要async-profiler支持profiler start# 开始采样profiler status# 查看状态profiler stop# 停止并生成火焰图实战场景# 场景1快速定位慢接口trace com.example.Controller handleRequest -n1--skipJDKMethodfalse# 场景2查看方法入参和返回值watchcom.example.Service process{params[0], returnObj}-x3-n5# 场景3查找CPU占用高的线程thread -n5# 场景4动态修改日志级别通过OGNLognlcom.example.LoggersetLevel(DEBUG)# 场景5查看Spring Beansc *Controller* vmtool --action getInstances --className org.springframework.context.ApplicationContext4.3 生产环境监控方案4.3.1 Prometheus Grafana JMX Exporter架构Java应用JMX Exporter → Prometheus采集和存储 → Grafana可视化1. 配置JMX Exporter# 下载jmx_prometheus_javaagentwgethttps://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.19.0/jmx_prometheus_javaagent-0.19.0.jar# 创建配置文件 jmx_exporter_config.ymlcatjmx_exporter_config.ymlEOF lowercaseOutputName: true lowercaseOutputLabelNames: true rules: - pattern: .* EOF# 启动Java应用时添加参数java -javaagent:./jmx_prometheus_javaagent-0.19.0.jar8088:jmx_exporter_config.yml\-jar application.jar2. 配置Prometheus# prometheus.ymlglobal:scrape_interval:15sscrape_configs:-job_name:java-appstatic_configs:-targets:[localhost:8088]labels:application:my-appenvironment:production3. Grafana Dashboard导入Grafana模板JVM (Micrometer)Dashboard ID 4701JVM (JMX)Dashboard ID 8563监控指标# 堆内存使用率(jvm_memory_used_bytes{areaheap}/ jvm_memory_max_bytes{areaheap}) * 100# GC耗时rate(jvm_gc_pause_seconds_sum[5m])# GC频率rate(jvm_gc_pause_seconds_count[5m])# 线程数jvm_threads_live_threads# CPU使用率process_cpu_usage * 1004.3.2 Micrometer Spring Boot Actuator1. 添加依赖dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-actuator/artifactId/dependencydependencygroupIdio.micrometer/groupIdartifactIdmicrometer-registry-prometheus/artifactId/dependency2. 配置application.ymlmanagement:endpoints:web:exposure:include:health,info,metrics,prometheusmetrics:export:prometheus:enabled:truetags:application:${spring.application.name}environment:${spring.profiles.active}3. 访问监控端点# Prometheus格式的metricscurlhttp://localhost:8080/actuator/prometheus# JSON格式的metricscurlhttp://localhost:8080/actuator/metrics/jvm.memory.used自定义监控指标RestControllerpublicclassMetricsController{privatefinalCounterrequestCounter;privatefinalTimerrequestTimer;publicMetricsController(MeterRegistryregistry){this.requestCounterCounter.builder(api.requests.total).description(Total API requests).tag(endpoint,/api/data).register(registry);this.requestTimerTimer.builder(api.requests.duration).description(API request duration).tag(endpoint,/api/data).register(registry);}GetMapping(/api/data)publicStringgetData(){requestCounter.increment();returnrequestTimer.record(()-{// 业务逻辑returndata;});}}五、性能问题诊断与解决5.1 内存溢出OOM问题最常见也最棘手的问题OutOfMemoryErrorOOM是Java应用中最常见、也最让人头疼的问题之一。当JVM无法再分配对象所需的内存且垃圾回收器也无法回收出更多内存时就会抛出OOM错误。OOM的出现通常意味着应用存在严重的内存问题如果不及时解决可能导致应用崩溃、服务不可用。OOM问题的排查往往比较复杂因为它可能发生在不同的内存区域每种类型的OOM都有不同的原因和解决方案。作为一个有过多次OOM排查经验的人我深知快速定位OOM原因的重要性。通常生产环境的OOM问题都很紧急需要在最短时间内找到根因并解决。因此了解不同类型的OOM、掌握排查方法和工具是每个Java开发者必备的技能。让我们从最常见的堆内存溢出开始逐一分析各种类型的OOM问题。5.1.1 堆内存溢出java.lang.OutOfMemoryError: Java heap space这是最常见的OOM类型通常占所有OOM问题的80%以上。当看到java.lang.OutOfMemoryError: Java heap space这个错误时意味着JVM堆内存空间不足无法再分配新的对象。深入理解堆内存溢出的本质堆内存溢出的本质是对象创建的速度超过了垃圾回收的速度或者说需要的内存空间超过了可用的内存空间。但这个描述还是太笼统了我们需要更细致地分析可能的原因。第一种情况是内存真的不够用。这种情况下应用确实需要更多的内存可能是业务量增长了需要处理的数据量变大了或者堆内存的初始设置就偏小。这种情况的解决方案比较直接增加堆内存。第二种情况是内存泄漏。这是更常见、也更难排查的情况。所谓内存泄漏是指程序中某些对象已经不再使用但由于仍然被引用导致垃圾回收器无法回收它们。随着时间推移这些僵尸对象越积越多最终耗尽了内存。内存泄漏是一种典型的慢性病应用可能运行几小时甚至几天才会出现OOM这使得问题的重现和排查都比较困难。第三种情况是短时间内创建了大量对象。比如一次性从数据库查询了百万条记录或者在循环中不断创建大对象。这种情况虽然不是严格意义上的泄漏但会导致瞬时的内存压力触发频繁的GC甚至OOM。第四种情况是缓存使用不当。很多应用会使用缓存来提升性能但如果缓存没有设置大小上限或过期策略就会不断增长最终导致OOM。我见过不少案例开发者使用了HashMap或ArrayList作为缓存忘记清理随着运行时间增长缓存中积累了大量数据最终耗尽内存。诊断步骤# 1. 配置OOM时自动dump-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/logs/heapdump/# 2. 使用MAT工具分析heap dump# 下载Eclipse MAT: https://www.eclipse.org/mat/# 3. 打开heap dump文件查看# - Leak Suspects内存泄漏嫌疑点# - Dominator Tree支配树找出占用内存最多的对象# - Histogram对象实例统计# 4. 使用OQL查询# 示例查找所有ArrayList实例select* from java.util.ArrayList# 示例查找size大于1000的ArrayListselect* from java.util.ArrayList a where a.size1000解决方案// 方案1增加堆内存-Xms8g-Xmx8g// 方案2修复内存泄漏// 典型内存泄漏场景// ❌ 错误示例1静态集合持有大量对象publicclassCacheManager{privatestaticListUseruserCachenewArrayList();// 永不释放publicvoidaddUser(Useruser){userCache.add(user);// 对象永不被回收}}// ✅ 正确做法使用弱引用或设置缓存上限publicclassCacheManager{privatestaticMapString,WeakReferenceUseruserCachenewWeakHashMap();// 或使用Guava Cache with过期策略privatestaticLoadingCacheString,UsercacheCacheBuilder.newBuilder().maximumSize(10000).expireAfterWrite(10,TimeUnit.MINUTES).build(newCacheLoaderString,User(){publicUserload(Stringkey){returnloadUserFromDb(key);}});}// ❌ 错误示例2资源未关闭publicvoidreadFile(Stringpath){BufferedReaderreadernewBufferedReader(newFileReader(path));// 如果抛出异常reader永不关闭导致内存泄漏Stringlinereader.readLine();}// ✅ 正确做法使用try-with-resourcespublicvoidreadFile(Stringpath)throwsIOException{try(BufferedReaderreadernewBufferedReader(newFileReader(path))){Stringlinereader.readLine();}}// ❌ 错误示例3大集合一次性加载publicListOrdergetAllOrders(){returnorderRepository.findAll();// 可能有百万条数据}// ✅ 正确做法分页查询publicPageOrdergetOrders(intpage,intsize){returnorderRepository.findAll(PageRequest.of(page,size));}// 或使用流式查询Query(SELECT o FROM Order o)StreamOrderstreamAll();try(StreamOrderstreamorderRepository.streamAll()){stream.forEach(order-processOrder(order));}5.1.2 元空间溢出java.lang.OutOfMemoryError: Metaspace问题表现java.lang.OutOfMemoryError: Metaspace原因分析加载的类过多动态代理、Groovy脚本等类加载器泄漏元空间设置过小CGLIB等字节码增强工具生成大量类诊断步骤# 1. 查看元空间使用情况jstat -gc123451000# 2. 查看类加载情况jstat -class123451000# 输出Loaded Bytes Unloaded Bytes Time50000100M10002M15.5# 3. dump内存并分析类加载器jmap -clstats12345# 4. 查看加载了哪些类jcmd12345GC.class_stats解决方案# 方案1增大元空间-XX:MetaspaceSize512m -XX:MaxMetaspaceSize1g# 方案2启用类卸载-XX:CMSClassUnloadingEnabled# CMS# G1默认启用类卸载# 方案3减少动态类生成// 典型场景CGLIB代理导致类过多// ❌ 错误示例每次都创建新的代理类publicObjectgetProxy(){EnhancerenhancernewEnhancer();enhancer.setSuperclass(UserService.class);enhancer.setCallback(newMethodInterceptor(){publicObjectintercept(Objectobj,Methodmethod,Object[]args,MethodProxyproxy)throwsThrowable{returnproxy.invokeSuper(obj,args);}});returnenhancer.create();// 每次都生成新的类}// ✅ 正确做法缓存代理类privatestaticfinalMapClass?,ObjectproxyCachenewConcurrentHashMap();publicObjectgetProxy(Class?clazz){returnproxyCache.computeIfAbsent(clazz,k-{EnhancerenhancernewEnhancer();enhancer.setSuperclass(k);enhancer.setCallback(interceptor);returnenhancer.create();});}5.1.3 直接内存溢出java.lang.OutOfMemoryError: Direct buffer memory问题表现java.lang.OutOfMemoryError: Direct buffer memory原因分析NIO使用DirectByteBuffer过多Netty等框架使用直接内存直接内存设置过小解决方案# 增大直接内存-XX:MaxDirectMemorySize2g# 监控直接内存使用jconsole或VisualVM查看java.nio.BufferPool# 注意# 直接内存不受-Xmx限制# 总内存使用 堆内存 元空间 直接内存 线程栈 ...# 需要预留足够的系统内存5.2 CPU 100%问题诊断步骤# 1. 找到Java进程jps -l# 或psaux|grepjava# 2. 查看进程的线程CPU使用情况top-H -p12345# 记录CPU占用高的线程PID例如15623# 3. 将线程PID转换为十六进制printf%x\n15623# 输出3d07# 4. 生成线程dumpjstack12345/tmp/thread_dump.txt# 5. 在thread dump中搜索对应的nidgrep-A20nid0x3d07/tmp/thread_dump.txt# 输出示例business-thread-10#123 daemon prio5 tid0x... nid0x3d07 runnablejava.lang.Thread.State: RUNNABLE at com.example.Service.infiniteLoop(Service.java:100)at com.example.Controller.handle(Controller.java:50)# 6. 分析代码定位问题常见原因// 原因1死循环publicvoidprocess(){while(true){// 忘记添加退出条件或sleepdoSomething();}}// 解决方案publicvoidprocess(){while(!Thread.currentThread().isInterrupted()){try{doSomething();Thread.sleep(100);// 添加sleep释放CPU}catch(InterruptedExceptione){Thread.currentThread().interrupt();break;}}}// 原因2正则表达式回溯Stringtextaaaaaaaaaaaaaaaaaaaaaaaaaaab;booleanmatchestext.matches((a)b);// 灾难性回溯CPU飙升// 解决方案// 1. 使用更高效的正则booleanmatchestext.matches(ab);// 2. 或使用find而不是matchesPatternpatternPattern.compile((a)b);Matchermatcherpattern.matcher(text);matcher.find();// 原因3频繁GCCPU被GC线程占用// 查看GC情况jstat-gcutil123451000// 如果GC频繁参考后面的频繁Full GC章节// 原因4大量线程竞争// 使用jstack查看大量BLOCKED线程jstack12345|grepjava.lang.Thread.State|sort|uniq-c// 50 BLOCKED// 100 WAITING// 解决方案减少锁竞争使用并发数据结构使用Arthas快速定位# 启动arthasjava -jar arthas-boot.jar# 查看CPU占用最高的线程thread -n5# 直接查看热点方法profiler start# 运行一段时间profiler stop --format html --file /tmp/profile.html# 打开profile.html查看火焰图5.3 频繁Full GC问题诊断步骤# 1. 观察GC情况jstat -gcutil12345100010# 输出示例频繁Full GCS0 S1 E O M CCS YGC YGCT FGC FGCT GCT0.095.210.598.796.595.3125012.4505025.67838.1280.096.815.399.196.595.3125212.4655226.89739.362# 观察到老年代(O)使用率接近100%Full GC频繁# 2. 查看GC日志tail-f gc.log|grepFull GC# 3. 分析GC原因jstat -gccause12345常见原因和解决方案// 原因1年轻代设置过小对象过早进入老年代// 查看对象晋升情况// -XX:PrintTenuringDistribution// 解决方案-Xmn2g// 增大年轻代-XX:MaxTenuringThreshold15// 增大晋升阈值// 原因2大对象频繁创建// ❌ 错误示例publicStringprocessData(){Stringresult;for(inti0;i10000;i){resultdata[i];// 每次都创建新String产生大量对象}returnresult;}// ✅ 正确做法publicStringprocessData(){StringBuildersbnewStringBuilder();for(inti0;i10000;i){sb.append(data[i]);}returnsb.toString();}// 原因3内存泄漏导致老年代不断增长// 使用jmap查看内存占用jmap-histo:live12345|head-20// 对比两次heap dumpjmap-dump:live,formatb,file/tmp/heap1.hprof12345// 等待10分钟jmap-dump:live,formatb,file/tmp/heap2.hprof12345// 使用MAT对比找出持续增长的对象// 原因4元空间不足触发Full GC// 查看元空间使用jstat-gc12345// MC和MU接近MaxMetaspaceSize// 解决方案-XX:MetaspaceSize512m-XX:MaxMetaspaceSize1g// 原因5显式调用System.gc()// 解决方案-XX:DisableExplicitGC// 禁用显式GC// 或者代码审查去除System.gc()调用// 原因6CMS的Concurrent Mode Failure// GC日志中出现// [Full GC (Concurrent Mode Failure) ...]// 原因CMS GC期间老年代空间不足// 解决方案-XX:CMSInitiatingOccupancyFraction70// 降低阈值提前触发CMS-Xms8g-Xmx8g// 增大堆内存G1 Full GC问题# G1发生Full GC说明调优有问题# 常见原因# 1. Humongous对象分配失败# 解决增大Region或避免大对象-XX:G1HeapRegionSize32m# 2. 并发标记跟不上内存分配速度# 解决提前触发并发标记-XX:InitiatingHeapOccupancyPercent40# 3. 晋升失败to-space exhausted# 解决增加预留空间-XX:G1ReservePercent155.4 内存泄漏检测典型内存泄漏场景// 场景1静态集合publicclassUserManager{privatestaticfinalMapString,UserusersnewHashMap();publicvoidregisterUser(Useruser){users.put(user.getId(),user);// 永不移除}// ✅ 解决方案// 1. 添加移除方法// 2. 使用WeakHashMap// 3. 使用Guava Cache with过期策略}// 场景2监听器未移除publicclassEventPublisher{privateListEventListenerlistenersnewArrayList();publicvoidaddListener(EventListenerlistener){listeners.add(listener);}// ❌ 忘记提供removeListener方法// ✅ 解决方案publicvoidremoveListener(EventListenerlistener){listeners.remove(listener);}}// 场景3ThreadLocal未清理publicclassRequestContext{privatestaticThreadLocalUsercurrentUsernewThreadLocal();publicstaticvoidsetUser(Useruser){currentUser.set(user);// ❌ 使用后未清理线程池复用线程时会导致泄漏}// ✅ 解决方案publicstaticvoidclearUser(){currentUser.remove();}// 在finally块或Filter中清理try{RequestContext.setUser(user);// 处理请求}finally{RequestContext.clearUser();}}// 场景4内部类持有外部类引用publicclassOuterClass{privatebyte[]largeArraynewbyte[10*1024*1024];// 10MBpublicInnerClassgetInnerClass(){returnnewInnerClass();// ❌ 内部类持有OuterClass引用}classInnerClass{// 即使只需要InnerClassOuterClass的10MB也无法回收}// ✅ 解决方案使用静态内部类staticclassInnerClass{// 不持有外部类引用}}// 场景5资源未关闭publicvoidprocessFile(Stringpath)throwsIOException{FileInputStreamfisnewFileInputStream(path);// ❌ 如果抛出异常流未关闭byte[]datanewbyte[fis.available()];fis.read(data);}// ✅ 解决方案publicvoidprocessFile(Stringpath)throwsIOException{try(FileInputStreamfisnewFileInputStream(path)){byte[]datanewbyte[fis.available()];fis.read(data);}}检测工具和方法# 方法1对比两个时间点的heap dumpjmap -dump:live,formatb,fileheap1.hprof12345# 运行一段时间执行相同操作jmap -dump:live,formatb,fileheap2.hprof12345# 使用MAT对比# 方法2使用MAT的Leak Suspects报告# 1. 打开heap dump# 2. 点击Leak Suspects# 3. 查看报告分析可疑对象# 方法3使用MAT的Dominator Tree# 1. 打开heap dump# 2. 点击Dominator Tree# 3. 按Retained Heap排序# 4. 查看占用内存最多的对象# 方法4使用VisualVM的OQL查询# 查找所有未关闭的文件流select* from java.io.FileInputStream# 查找大对象select* from instanceof java.lang.Object o where sizeof(o)1000000# 方法5使用JProfiler的内存泄漏检测# JProfiler → Memory → Recorded Objects# 选择类 → Mark Current Values# 运行一段时间# Remove Garbage# 查看仍然存在的对象总结本文从JVM内存模型、垃圾回收器原理、参数调优到监控工具和问题诊断提供了一个完整的JVM性能调优指南。核心要点回顾理解内存模型理解堆、栈、元空间的作用是调优的基础选择合适的GC根据应用特点选择合适的垃圾回收器合理设置参数堆内存、GC参数、监控参数的合理配置持续监控使用工具持续监控JVM运行状态问题诊断掌握OOM、CPU高、频繁GC等问题的诊断方法调优建议不要过早优化先通过监控发现问题每次只调整一个参数观察效果生产环境变更要谨慎做好回滚准备保留GC日志便于问题排查定期review内存使用情况
版权声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

房地产培训网站建设网站里的活动专题栏怎么做

一、设计背景与核心需求 传统养殖环境监测依赖人工巡检,存在数据滞后、覆盖不全面、预警不及时等问题,易导致养殖动物应激反应(如家禽呼吸道疾病、水产缺氧死亡)。基于单片机的智慧养殖环境检测仪,通过多参数协同监测与…

张小明 2026/1/1 23:13:48 网站建设

关于音乐的个人网站网站音乐播放器插件

GKD订阅管理完全指南:2025年高效配置与使用全攻略 【免费下载链接】GKD_THS_List GKD第三方订阅收录名单 项目地址: https://gitcode.com/gh_mirrors/gk/GKD_THS_List GKD第三方订阅收录名单项目是一个专门为GKD用户精心打造的订阅资源聚合平台,汇…

张小明 2025/12/29 8:02:29 网站建设

免费推广网站都有哪些网站设计 下拉式菜单怎么做

2025年AI分镜技术三大突破:电影级运镜算法如何重塑影视制作 【免费下载链接】next-scene-qwen-image-lora-2509 项目地址: https://ai.gitcode.com/hf_mirrors/lovis93/next-scene-qwen-image-lora-2509 随着人工智能技术在影视制作领域的深入应用&#xff…

张小明 2025/12/29 8:02:29 网站建设

深圳那个网站建设江苏建设教育网官网

智慧树刷课插件终极使用指南:3步实现自动化学习 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 还在为智慧树网课的手动操作而烦恼吗?这款智慧树…

张小明 2025/12/29 8:02:31 网站建设

徽石网站建设凯里市企业建站公司

Windows防休眠终极方案:NoSleep工具让电脑永不自动锁屏 【免费下载链接】NoSleep Lightweight Windows utility to prevent screen locking 项目地址: https://gitcode.com/gh_mirrors/nos/NoSleep 在日常工作中,你是否经常遇到这样的困扰&#x…

张小明 2025/12/29 8:02:31 网站建设

嘉兴网站建设与管理专业万网域名注册价格

Buzz离线语音识别技术深度解析:本地AI模型的架构实现与隐私保护 【免费下载链接】buzz Buzz transcribes and translates audio offline on your personal computer. Powered by OpenAIs Whisper. 项目地址: https://gitcode.com/gh_mirrors/buz/buzz 在当今…

张小明 2025/12/29 8:02:34 网站建设