jvm内存
内存结构
程序计数器(Program Counter Register):
- 一块较小的内存空间,作为当前线程所执行的字节码的行号指示器。
- 字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
Java虚拟机栈(Java Virtual Machine Stacks):
- 线程私有,生命周期与线程相同。
- 描述Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
本地方法栈(Native Method Stack):
- 与虚拟机栈类似,但为虚拟机使用到的Native方法服务。
- 虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构没有强制规定,因此具体的虚拟机可以自由实现。
Java堆(Java Heap):
- Java虚拟机所管理的内存中最大的一块,被所有线程共享。
- 唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 垃圾收集器管理的主要区域,分为新生代(Young Generation)和老年代(Old Generation),其中新生代又分为Eden Space、Survivor Space1和Survivor Space2三部分。
方法区(Method Area):
- 线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 有一个别名叫做Non-Heap(非堆),与Java堆区分开来。
- 在JDK8及之后的版本中,方法区被元空间(Metaspace)所取代。
运行时常量池(Runtime Constant Pool):
- 方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
- 具备动态性,运行期间也可能将新的常量放入池中。
直接内存(Direct Memory):
- 不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
- 但被频繁使用,并可能导致OutOfMemoryError异常出现。
- 主要通过ByteBuffer类来使用,使用完直接内存后,需要通过调用DirectByteBuffer对象的clean()方法来释放直接内存空间。
metaspace和permgen
什么是metaspace和permgen?
在Java虚拟机(JVM)中,Metaspace 和 PermGen(永久代)都与类的元数据存储有关,但它们属于不同的JVM版本和内存管理策略。
PermGen(永久代)
- 存在版本:在Java 8之前的HotSpot JVM版本中,方法区(Method Area)被实现为永久代(PermGen space)。永久代主要用于存储类的元数据,包括类的结构信息(如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容)以及类型信息(如类的名称、父类、实现的接口等)。
- 限制:永久代的大小是有限的,并且需要通过JVM启动参数(如
-XX:MaxPermSize)来设置。如果永久代被填满,JVM会抛出OutOfMemoryError: PermGen space错误。 - 废弃:由于永久代的大小限制和性能问题,Java 8中永久代被元空间(Metaspace)所取代。
Metaspace(元空间)
- 存在版本:从Java 8开始,HotSpot JVM使用元空间(Metaspace)来取代永久代。元空间位于本地内存中(即堆外内存),而不是JVM堆内存中。
- 优势:元空间的最大好处是它只受物理内存的限制,而不是像永久代那样有一个固定的限制。这意呀着,如果应用程序加载了大量的类,它不会受到像永久代那样的内存限制。
- 配置:元空间的大小可以通过JVM启动参数来配置,例如
-XX:MetaspaceSize(元空间的初始大小)和-XX:MaxMetaspaceSize(元空间的最大大小,如果不设置,则只受物理内存的限制)。 - 错误:如果元空间不足,JVM会抛出
OutOfMemoryError: Metaspace错误。
总结
- PermGen 是Java 8之前HotSpot JVM中用于存储类元数据的内存区域,它的大小有限,并且可能导致
OutOfMemoryError: PermGen space错误。 - Metaspace 是Java 8及以后版本中用于替代永久代的内存区域,它位于本地内存中,只受物理内存的限制,并通过
-XX:MetaspaceSize和-XX:MaxMetaspaceSize等JVM启动参数来配置。如果元空间不足,JVM会抛出OutOfMemoryError: Metaspace错误。
metaspace和permgen作用是什么?
permgen作用PermGen(永久代)在Java 8之前的HotSpot JVM版本中,是用于存储类的元数据的内存区域。它的全称是Permanent Generation space,即永久保存区域。PermGen主要存储以下几类信息:- 类的元数据:
- 类结构信息:包括类的名称、父类、实现的接口等。
- 字段和方法数据:类的字段(包括静态和非静态字段)和方法(包括方法的字节码)的描述信息。
- 构造函数信息:类的构造函数的描述信息。
- 运行时常量池:
- 常量池是一个特殊的存储区域,用于存放编译期生成的各种字面量和符号引用。这些字面量包括字符串常量、数字常量等,而符号引用则包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
- 类型信息:
- 与类相关的类型信息,如数组类型、泛型类型等。
举例说明
假设我们有一个简单的Java类
Example,其内容如下:javapublic class Example { public static final String CONSTANT = "Hello, PermGen!"; private int number; public Example(int number) { this.number = number; } public void printNumber() { System.out.println(number); } }当这个类被JVM加载时,它的以下信息会被存储在PermGen中:
- 类的元数据:
Example类的名称、其父类(Object)、它没有实现的接口(因为默认继承自Object类)等信息。 - 字段信息:
number字段的描述信息,包括其名称、类型(int)和访问修饰符(private)。 - 方法信息:
Example类的构造函数Example(int number)和printNumber()方法的描述信息,包括方法的名称、返回类型、参数列表、方法体(以字节码形式)等。 - 常量信息:
CONSTANT常量字符串"Hello, PermGen!"会被存储在运行时常量池中。
注意事项
- PermGen的大小是有限的,并且需要通过JVM启动参数(如
-XX:MaxPermSize)来设置。如果PermGen被填满,JVM会抛出OutOfMemoryError: PermGen space错误。 - 由于PermGen的这些限制,Java 8及以后的版本引入了元空间(Metaspace)来取代永久代,以提供更灵活和高效的内存管理。
实际应用
在实际应用中,如果应用程序加载了大量的类(比如使用了大量的第三方库或动态生成类),就可能会遇到PermGen空间不足的问题。为了解决这个问题,可以通过增加
-XX:MaxPermSize的值来扩大PermGen空间的大小。但是,在Java 8及以后的版本中,应该使用-XX:MaxMetaspaceSize来配置元空间的大小。- 类的元数据:
metaspace作用在Java中,
Metaspace(元空间)是从JDK 8开始引入的,用于替代之前版本的永久代(PermGen space)。Metaspace主要用于存储类的元数据,这些信息对于Java应用程序的运行至关重要。以下是Metaspace存储内容的详细归纳,并辅以例子:Metaspace存储内容
- 类的结构信息
- 包括类名、父类、接口、字段(包括静态和非静态字段)等信息。这些信息是JVM在运行时识别和操作类的基础。
- 例子:假设有一个名为
Person的类,它有一个名为name的字段和一个名为age的字段。那么,Person类的这些信息(包括类名、字段名、字段类型等)将被存储在Metaspace中。
- 常量池
- 常量池是类文件的一部分,用于存放编译期生成的各种字面量和符号引用。这些常量包括字符串常量、数值常量、类引用、字段引用和方法引用等。
- 例子:在
Person类中,如果定义了一个字符串常量public static final String GREETING = "Hello, Metaspace!";,那么这个字符串常量将被存储在常量池中,进而存储在Metaspace中。
- 字段描述
- 描述类中各个字段的属性,如名称、类型、修饰符(如public、private、static等)等。
- 例子:在
Person类中,name字段的描述信息(包括字段名、字段类型、是否为静态等)将被存储在Metaspace中。
- 方法描述
- 描述类中各个方法的属性,如名称、参数列表、返回类型、方法体(以字节码形式)等。
- 例子:
Person类中的构造方法Person(String name, int age)和任何其他方法的描述信息(包括方法名、参数类型、返回类型等)都将被存储在Metaspace中。
Metaspace的特点
- 动态扩展:Metaspace的大小是动态的,可以根据应用程序的需要进行扩展,避免了像永久代那样因为空间不足而导致的内存溢出问题。
- 内存限制:虽然Metaspace的大小默认是动态扩展的,但也可以通过JVM启动参数(如
-XX:MaxMetaspaceSize)来设置其最大大小,以防止其无限制地扩展。 - 性能优化:Metaspace的引入提高了JVM的性能,因为它避免了永久代在垃圾收集时可能导致的长时间停顿。
总结
Metaspace是Java 8及以后版本中用于存储类元数据的内存区域,它取代了之前的永久代。Metaspace主要存储类的结构信息、常量池、字段描述和方法描述等内容,这些信息对于Java应用程序的运行至关重要。通过合理配置Metaspace的大小,可以确保应用程序的稳定运行,并避免内存溢出等问题的发生。
- 类的结构信息
Native Memory Tracking(NMT)
Native Memory Tracking(NMT)是Java HotSpot虚拟机(JVM)提供的一种功能,用于追踪和分析Java应用程序在本地内存中的内存分配和使用情况。以下是关于Native Memory Tracking的详细介绍:
一、功能概述
NMT可以帮助开发人员和系统管理员理解Java应用程序对本地内存的使用情况,从而更好地诊断性能问题和潜在的内存泄漏。它监控类元数据、线程栈、堆和JVM自身内存,通过命令行工具分析数据,有助于开发者定位内存泄漏和溢出。
二、启用方式
在启动JVM时,可以通过添加特定的参数来启用NMT。这些参数包括:
-XX:NativeMemoryTracking=off:无记录模式,默认模式,不记录任何本地内存信息。-XX:NativeMemoryTracking=summary:概要模式,记录JVM中每个组件的大致本地内存使用量。-XX:NativeMemoryTracking=detailed:详细模式,记录每个本地内存分配的详细信息,包括调用堆栈。
三、使用方法
- 启用NMT:在启动Java应用程序时,通过添加
-XX:NativeMemoryTracking=summary或-XX:NativeMemoryTracking=detailed参数来启用NMT。 - 生成报告:使用
jcmd命令来生成NMT报告。具体命令格式为jcmd <pid> VM.native_memory <path/to/report.txt>,其中<pid>是Java进程的进程ID,<path/to/report.txt>是报告文件的路径。 - 查看报告:生成的NMT报告通常包含总的本地内存使用量、按类别划分的使用情况、每个本地内存分配的详细信息(如分配的大小、时间戳和调用堆栈)等。通过分析这些报告,可以定位和解决内存性能问题。
四、原理与优势
NMT通过跟踪本地方法调用和分析内存分配与回收的详细过程,揭示内存泄漏的线索。它依赖于对Java对象引用的监控,当对象被回收时,如果其引用链上还存在其他对象,就会导致内存泄漏。NMT能够找出这些引用的源头,帮助开发者定位内存泄漏的位置。
此外,NMT还具有以下优势:
- 全面性:能够追踪和报告JVM在本地内存中的所有使用情况,包括直接内存分配、JNI本地堆分配等。
- 灵活性:提供概要模式和详细模式两种记录方式,满足不同场景下的需求。
- 易用性:通过命令行工具即可生成和分析NMT报告,无需额外的复杂配置。
五、注意事项
- 在使用NMT时,需要注意其可能带来的性能开销。特别是在详细模式下,由于需要记录每个本地内存分配的详细信息,因此可能会对应用程序的性能产生一定影响。
- NMT主要用于诊断和解决与本地内存相关的问题,对于JNI调用申请的内存等问题,可能需要结合其他工具进行分析和解决。
综上所述,Native Memory Tracking是Java HotSpot虚拟机中一个非常有用的功能,它可以帮助开发人员更好地理解和优化Java应用程序的内存使用情况。通过合理使用NMT,可以更加高效地诊断和解决与本地内存相关的问题。
Native Memory Tracking输出
Native Memory Tracking:
Total: reserved=1461MB, committed=193MB
- Java Heap (reserved=128MB, committed=128MB)
(mmap: reserved=128MB, committed=128MB)
- Class (reserved=1041MB, committed=14MB)
(classes #627)
(malloc=9MB #668)
(mmap: reserved=1032MB, committed=5MB)
- Thread (reserved=22MB, committed=22MB)
(thread #30)
(stack: reserved=22MB, committed=22MB)
- Code (reserved=244MB, committed=3MB)
(mmap: reserved=244MB, committed=2MB)
- GC (reserved=15MB, committed=15MB)
(malloc=10MB #131)
(mmap: reserved=5MB, committed=5MB)
- Internal (reserved=10MB, committed=10MB)
(malloc=10MB #2860)
- Symbol (reserved=2MB, committed=2MB)
(malloc=1MB #251)
(arena=1MB #1)Native Memory Tracking (NMT) 是 Java 虚拟机(JVM)中用于跟踪和报告 JVM 使用的本地内存(即非堆内存)的工具。以下是对您提供的 NMT 报告的详细解读:
总览
- Total(总计):
- reserved(预留): 1461MB。这是 JVM 为各种内存区域预留的总空间。
- committed(已提交): 193MB。这是 JVM 实际已向操作系统请求并可以立即使用的内存量。
各个内存区域的详细信息
Java Heap(Java 堆)
- reserved(预留): 128MB。为 Java 堆预留的内存。
- committed(已提交): 128MB。已提交给 Java 堆的内存,全部被使用。
- mmap: 使用 mmap 机制分配的内存,同样预留和已提交均为 128MB。
Class(类)
- reserved(预留): 1041MB。为类元数据和内部数据结构预留的内存。
- committed(已提交): 14MB。实际已使用的内存。
- classes #627: 加载的类数量为 627 个。
- malloc: 使用 malloc 分配的内存,9MB,对象数量为 668。
- mmap: 使用 mmap 机制分配的内存,预留 1032MB,已提交 5MB。
Thread(线程)
- reserved(预留): 22MB。为线程栈预留的内存。
- committed(已提交): 22MB。已提交给线程栈的内存,全部被使用。
- thread #30: 当前活动的线程数量为 30 个。
- stack: 线程栈使用的内存,预留和已提交均为 22MB。
Code(代码)
- reserved(预留): 244MB。为编译后的代码预留的内存。
- committed(已提交): 3MB。实际已使用的内存。
- mmap: 使用 mmap 机制分配的内存,预留 244MB,已提交 2MB。
GC(垃圾收集)
- reserved(预留): 15MB。为垃圾收集器预留的内存。
- committed(已提交): 15MB。已提交给垃圾收集器的内存,全部被使用。
- malloc: 使用 malloc 分配的内存,10MB,对象数量为 131。
- mmap: 使用 mmap 机制分配的内存,预留 5MB,已提交 5MB。
Internal(内部)
- reserved(预留): 10MB。为 JVM 内部使用预留的内存。
- committed(已提交): 10MB。已提交给内部使用的内存,全部被使用。
- malloc: 使用 malloc 分配的内存,10MB,对象数量为 2860。
Symbol(符号)
- reserved(预留): 2MB。为字符串和符号表预留的内存。
- committed(已提交): 2MB。已提交给符号表的内存,全部被使用。
- malloc: 使用 malloc 分配的内存,1MB,对象数量为 251。
- arena: 符号表使用的内存区域,1MB,区域数量为 1。
总结
这个报告显示了 JVM 使用的各种本地内存区域的预留和已提交情况。通过分析这些数据,可以了解 JVM 内存使用的主要区域和潜在的内存管理问题。例如,如果某个区域的预留内存远大于已提交内存,可能表示该区域有潜在的内存扩展需求。相反,如果已提交内存接近预留内存,则可能需要考虑优化或增加预留内存以避免内存不足的问题。
查看内存使用情况
通过Native Memory Tracking功能查看jvm内存使用。
通过项目 链接 辅助测试
启动辅助测试项目
bashjava -Xmx128m -Xss512k -XX:NativeMemoryTracking=detail -jar target/demo.jar memalloc-XX:NativeMemoryTracking=detail启用Native Memory Tracking功能使用
jcmd+NMT查看内存使用情况bash./jcmd `/usr/local/jdk1.8.0_271/bin/jps -mlv|grep demo.jar | awk '{print $1}'` VM.native_memory scale=MB/usr/local/jdk1.8.0_271/bin/jps -mlv|grep demo.jar | awk '{print $1}'用于获取进程id
GC相关
什么是垃圾?
在了解垃圾回收机制之前我们首先要定义一下什么是垃圾,我们内存里大部分的对象都是随着方法的执行而创建,方法执行完毕后这些对象就不会被再次使用了,而这些不会被再次使用的对象并不会被清除掉,所以我们内存里面的对象会越来越多占用着我们的内存空间,此时我们就需要一种机制把这种不会被再次使用的对象清除掉,而这些不会被再次使用的对象我们就称之为垃圾。
为何需要STW呢?
GC的主要任务是通过可达性分析算法来标记并回收不再使用的对象。这个过程中,JVM需要从一组称为GC Roots的根对象开始,遍历对象之间的引用链,以判断对象是否存活。如果在分析过程中,应用程序的线程继续运行,可能会改变对象的引用关系,导致GC无法准确判断对象的存活状态。为了避免这种情况,JVM需要暂停所有用户线程,即执行STW操作,以确保在标记过程中引用关系的一致性。
Heap内存结构
在JDK 1.8及之后的版本中,Heap内存被划分为两个主要区域:新生代(Young Generation)和老年代(Old Generation)。新生代又被进一步细分为Eden区、From Survivor(S0)区和To Survivor(S1)区,这三个区域的大小比例可以通过JVM参数进行调整。
- 新生代(Young Generation)
- Eden区:新生代中最大的一块区域,用于存放新生成的对象。当Eden区空间不足时,会触发Minor GC(也称为Young GC),回收不再被其他对象所引用的对象。
- Survivor区:由From Survivor(S0)区和To Survivor(S1)区组成,它们的大小通常相等。在Minor GC过程中,Eden区存活下来的对象会被移动到其中一个Survivor区(通常是To Survivor区),同时清空另一个Survivor区。在下一次Minor GC时,两个Survivor区的角色会互换。
- 老年代(Old Generation)
- 老年代用于存放经过多次Minor GC后仍然存活的对象。当老年代空间不足时,会触发Major GC(也称为Full GC),回收老年代中的非存活对象。
为什么需要GC调优?
垃圾收集过程中,JVM需要暂停应用程序的执行(Stop-The-World),以便垃圾收集器能够遍历和回收堆内存中的无用对象。频繁的GC或长时间的GC停顿会严重影响应用程序的响应时间和吞吐量。通过优化GC,可以减少GC的频率和每次GC的停顿时间,从而提升整体性能。
GC各种算法优化的重点,就是减少STW(暂停),同时这也是JVM调优的重点。
什么是MinorGC/YoungGC、MajorGC/OldGC、FullGC?
todo 深刻点理解MajorGC和FullGC区别
MinorGC/YoungGC其实所谓的 MinorGC,也可以称为 YoungGC,这二者是相同的,都是专门针对于新生代的GC.
「新生代」也可以称为「年轻代」,在新生代的Eden内存区域被占满之后,就会触发「新生代」的GC,或者叫「年轻代」的GC,也就是所谓的 MinorGC 和 YoungGC
FullGC从字面上意思其实就可以理解了,“Full” 就是整个的,完整的意思,所以就是对 JVM 进行一次整体 垃圾回收,把各个内存空间区域,不管是新生代,老年代,永久代的垃圾统统都回收掉。
MajorGC/OldGCMajorGC(老年代垃圾回收)的触发时机主要与老年代(Old Generation)的内存使用情况有关。在Java虚拟机(JVM)中,当老年代空间不足时,就会触发MajorGC。触发MajorGC的主要时机如下:
- 老年代空间不足:
- 当老年代中的空闲空间不足以容纳更多从年轻代(Young Generation)晋升过来的对象时,JVM会触发MajorGC以清理老年代中的不再使用的对象,从而腾出空间。
- 与MinorGC的关系:
- MajorGC的触发有时也与MinorGC(年轻代垃圾回收)有关。在MinorGC过程中,如果幸存的对象(survivor objects)太多,导致老年代无法容纳这些晋升的对象,或者老年代本身的空间就不足,也可能会触发MajorGC。
- 显式调用System.gc():
- 虽然在现代JVM中,显式调用
System.gc()通常只是建议JVM进行垃圾回收,并不保证立即执行,但在某些情况下,如果JVM认为有必要响应这个请求,它可能会触发Full GC(包括老年代的垃圾回收)。需要注意的是,Full GC不一定等同于MajorGC,但Full GC通常会包括MajorGC。
- 虽然在现代JVM中,显式调用
- 垃圾回收器的特定行为:
- 不同的垃圾回收器(如CMS、G1等)可能有不同的触发MajorGC的策略。例如,在CMS垃圾回收器中,如果在MinorGC过程中出现promotion failure(晋升失败),即年轻代中的对象无法晋升到老年代,因为老年代空间不足,那么可能会触发Full GC来清理老年代。
- 老年代空间不足:
GC回收算法
GC回收基础算法
引用计数法
什么是引用计数法呢?
引用计数法是为对象添加一个引用计数器,然后用一块额外的内存区域来存储每个对象被引用的次数,当对象每有一个地方引用它时,那我们对该对象的引用计数就会加1,反之每有一个引用失效时,我们对该对象的引用计数就会减1, 当对象的被引用次数为0时,那么我们可以认为这个对象是不会被再次使用了,通过这种方式我们能快速直观的定位到这些可回收的对象,从而进行清理。
引用计数法存在的问题
- 无法解决循环引用的问题:引用计数法虽然很直观高效,但是通过引用计数法是没办法扫描到一种特殊情况下的“可回收”对象,这种特殊情况就是对象循环引用的时候,比如A对象引用了B,B对象引用了A,除此之外他们两个没有被任何其他对象引用,那么其实这部分对象也属于“可回收”的对象,但是通过引用计数法是没办法定位的。
- 另外一个方面是引用计数法需要额外的空间记录每个对象的被引用的次数,这个引用数也需要去额外的维护。
可达性分析法
什么是可达性分析法呢?
可达性分析法是通过以所有的“GC Roots”对象为出发点,如果无法通过GC Roots的引用追踪到的对象,那我们认为这些对象就不会再次被使用了,现在主流的程序语言都是通过可达性分析法来判断对象是否存活的。
GC回收算法
标记清除法
标记清除法是先找到内存里的存活对象并对其进行标记,然后统一把未标记的对象统一的清理。
特点:简单、收集速度快,但会有空间碎片,空间碎片会导致后面的GC频率增加。
标记清除法优势:
- 标记清除法的特点就是简单直接,速度也非常快,适合存活对象多,需要回收的对象少的场景
标记清除法不足:
- **会造成不连续的内存空间:**就像上图清除后的内存区域一样,清除后内存会有很多不连续的空间,这也就是我们常说的空间碎片,这样的空间碎片太多不仅不利于我们下次分配,而且当有大对象创建的时候,我们明明有可以容纳的总空间,但是空间都不是连续的造成对象无法分配,从而不得不提前触发GC。
- **性能不稳定:**内存中需要回收的对象,当内存中大量对象都是需要回收的时候,通常这些对象可能比较分散,所以清除的过程会比较耗时,这个时候清理的速度就会比较慢了。
适合场景:只有小部分对象需要进行回收的,所以标记清除法比较适用于老年代的垃圾回收,因为老年代一般存活对象会比回收对象要多。
标记复制法
标记清除法最大问题是会造成空间碎片,同时可回收对象如果太多也会影响其性能,而标记复制法则可以解决这两大问题。标记清除法的关注点在可回收的对象身上,而标记复制法的关注点则放在了存活的对象身上,通过把存活的对象转移到一个新的区域,然后对原区域的对象进行统一清理。
首先它把内存划分出三块区域,一块用于存放新创建的对象叫Eden区,另外两块则用于存放存活的对象分别叫 S1区和S2区。回收的时候会有两种情况,一种是把Eden和S1区的存活对象复制到S2区,第二种是把Eden和S2区的存活对象复制到S1区 ,也就是说S1区和S2这两块区域同时只会有一块使用,通过这种方式保证始终会有一块空白的q区域用于下次GC时存放存活的对象,而且原来的区域不需要考虑保留存活的对象,所以可以直接一次性清除所有对象,这要既简单直接同时也保证了清除的内存区域的内存连续性。
特点:收集速度快,可以避免空间碎片,但是有空间浪费,存活对象较多的情况下复制对象的过程等会非常耗时,而且需要担保机制。
标记复制法优势:
- 标记复制法解决了标记清除法的空间碎片问题,并且采用移动存活对象的方式,每次清除针对的都是一整块内存,所以清除可回收对象的效率也比较高,但因为要移动对象所以这里会耗费一部分时间,所以标记复制法效率还是会低于标记清除法。
标记复制法不足:
- **会浪费一部分空间:**通过上面的图我们也不难发现,总是会有一块空闲的内存区域是利用不到的,这也造成了资源的浪费。
- 存活对象多会非常耗时:因为复制移动对象的过程是比较耗时的,这个不仅需要移动对象本身,还需要修改使用了这些对象的引用地址,所以当存活对象多的场景会非常耗时,所以标记复制法比较适合存活对象较少的场景。
- **需要担保机制:**因为复制区总会有一块空间的浪费,而为了减少浪费空间太多,所以我们会把复制区的空间分配控制在很小的区间,但是空间太小又会产生一个问题,就是在存活的对象比较多的时候,这时复制区的空间可能不够容纳这些对象,这时就需要借一些空间来保证容纳这些对象,这种从其他地方借内存的方式我们称它为担保机制(标记复制法的担保机制原理是通过在内存分配和垃圾回收过程中引入一种安全设计,以处理特定情况下内存空间不足的问题。当To区空间不足以容纳From区中的所有存活对象时,担保机制会将部分或全部存活对象分配到老年代(或其他备用内存区域)中,以确保垃圾回收过程的顺利进行和内存的安全性)。
适合场景: 只有少量对象存活的场景,这也正是新生代对象的特点,所以一般新生代的垃圾回收器基本都会选择标记复制法。
标记整理法
标记复制法算是完美的补齐了标记清除法的短板,既解决了空间碎片的问题,又适合使用在大部分对象都是可回收的场景。 不过标记复制法也有不完美的地方,一方面是需要空闲出一块内存空间用来腾挪对象,另外一方面它在存活对象比较多的场景也不是太适合,而存活对象多的场景通常适合使用标记清除法,但是标记清除法会产生空间碎片又是一个无法忍受的问题。
所以就需要有一种算法,专门针对存活对象多,但是又产生空间碎片,还不浪费内存空间,这就是标记整理法的初衷。标记整理法的思路很好理解,好像我们整理房间一样,就是把有用的东西和需要丢弃的垃圾分别挪到房间的两边,然后再把房间垃圾的那一侧整体扫地出门。
标记整理法分为标记和整理两个阶段,标记阶段会先把存活的对象和可回收的对象标记出来;标记完再对内存对象进行整理,这个阶段会把存活的对象往内存的一端移动,移动完对象后再清除存活对象边界之外的对象。
特点:相对于标记复制法不会浪费内存空间,相对标记清除法则可以避免空间碎片,但是速度比其他两个算法慢。
标记整理法优势:
- 标记整理法是解决了标记复制法浪费空间、不适合存活对象多场景的短板,又解决了标记清除法空间碎片的短板, 所以对于标记复制法不适合的场景,同时又不能忍受标记清除法的空间碎片问题,就可以考虑标记整理法。
标记整理法不足:
- 没有任何一种算法是万能的,标记整理法看似解决了很多问题,但它本身存在很严重的性能问题,标记整理法是三种垃圾回收算法中性能最低的一种,因为标记整理法在移动对象的时候不仅需要移动对象,还要额外的维护对象的引用的地址,这个过程可能要对内存经过几次的扫描定位才能完成,同时还有清除对象的动作,既然做的事情这么多那么必然消耗的时间也越多。
适合场景: 内存吃紧,又要避免空间碎片的场景,老年代想要避免空间碎片问题的话通常会使用标记整理法。
垃圾回收器
Serial和Serial Old收集器
Serial 系列的垃圾收集器是JVM的第一款收集器,因为当时的硬件环境配置都不高,这个时候JVM的内存都是几十M,CPU也都是单核的,当时也没有现在这样的高的并发应用场景。所以限于当时的硬件资源和应用场景,所以它的设计思路就是简单高效、消耗资源最少、使用单线程收集。
原理:
- Serial 垃圾收集过程很简单,根据下图一眼就能明白,Serial会开启一个线程进行垃圾收集,在收集的整个过程都会暂停用户线程(Stop the Word),直到垃圾收集完毕,如果把垃圾收集的过程当作打扫房间卫生,那么Serial 的收集过程就是在你收集房间的时候,你首先会让房间里的人都出去,然后你再安心打扫房间,直到你打扫完毕了才能让外面的人进来,这样就不用担心你一边打扫房间一边还有人在房间里扔垃圾了。**注意:**说到“暂停用户线程”,这里也是各种垃圾收集器的一个区分指标,后面的有些垃圾收集器收集的某些阶段是不需要暂停用户线程的。
特点:
- 收集区域: Serial (新生代),Serial Old(老年代)。
- 使用算法: Serial (标记复制法),Serial Old(标记整理法)。
- 搜集方式: 单线程收集。
- 优势: 内存资源占用少、单核CPU环境最佳选项。
- 劣势: 整个搜集过程需要停顿用户线程。多核CPU、内存富足的环境,资源优势无法利用起来。
Parallel Scavenge(PS)和Parallel Old(PO)收集器
随着硬件资源的升级,JVM的内存空间从原来的几十M可以扩展到几百M到甚至几G了,CPU也从单核走向了多核时代,此时使用Serial收集器还无法发挥出多核CPU的优势,因为内存空间变得更大了使用单线程收集的Serial收集的时间也变长了,所以就衍生了Parallel 系列的收集器,Parallel 核心是利用了多核CPU资源的优势,进行垃圾收集时可以多个线程同时进行收集,从而提升整个垃圾收集的性能 。
原理:
- Parallel Scavenge 和Parallel Old的工作机制一样,这里以Parallel Scavenge为例,Parallel Old在收集过程中会开启多个线程一起收集,整个过程都会暂停用户线程,直到整个垃圾收集过程结束。和之前的Serial垃圾收集器一对比,同样进行垃圾收集前都是先叫其他人都离开房间,但是不同的是serial只有一个人打扫房间,而这里却是有多个人一起打扫房间,所以从这一点看Parallel 系列的收集器要比之前的效率高上很多。
特点:
- 收集区域: Parallel Scavenge (新生代),Parallel Old(老年代)。
- 使用算法: Parallel Scavenge (标记复制法),Parallel Old(标记整理法)。
- 搜集方式: 多线程。
- 优势: 多线程收集,CPU多核环境下效率要比serial高。
- 劣势: 整个搜集过程需要停顿用户线程。
ParNew收集器
ParNew和Parallel Scavenge 垃圾收集器并没有太大的区别,能让ParNew出名的一个核心因素是因为它是唯一一个能与CMS配合一起使用新生代收集器,而因为CMS的优秀所以让ParNew 也出了名,这个就有点傍到大款的感觉。
原理:
- ParNew收集流程和Parallel Scavenge一样 ,同样是先停止应用程序线程,再进行多线程同时收集,整个收集过程都会暂停用户线程(Stop the Word),直到垃圾收集完毕。
特点:
- 收集区域: 新生代。
- 使用算法: 标记复制法。
- 搜集方式: 多线程。
- 搭配收集器: CMS。
- 优势: 多线程收集,CPU多核环境下效率要比serial高,新生代唯一一个能与CMS配合的收集器。
- 劣势: 整个搜集过程需要停顿用户线程。
CMS(Concurrent Mark Sweep)收集器
随着硬件技术的发展,我们的服务器可用的内存越来越大,JVM各个区域可分配的空间越来越大,这样的好处就是相对于以前内存可以创建更多的对象了,然后垃圾收集总体次数也会减少,不过与此同时也伴随着另外一个问题出现了,因为内存空间的增大,JVM每次进行垃圾收集的时间就变得越来越长了,这个时候垃圾的收集经常会耗费几秒甚至几十秒,而之前的垃圾收集器进行收集的整个过程都是需要“停止用户线程”的,试想一下如果你点击系统的一个按钮十几秒都没有响应,我估计你是无法忍受的,所以这个时候就需要一种能不停止用户线程的垃圾收集器,让垃圾收集的同时也能处理“用户线程”的工作,这也就是CMS诞生的初衷。
原理:
为了尽量减少用户线程的停顿时间,CMS采用了一种全新的策略使得在垃圾回收过程中的某些阶段用户线程和垃圾回收线程可以一起工作,这样就避免了因为长时间的垃圾回收而使用户线程一直处于等待之中。
整个过程就像我们打扫房间的时候可以让大家留在房间里工作,等我把房间的其他地方都打扫完,只剩大家工作的那部分区域的垃圾,这个时候再让大家到房间外面去,我再把房间里那些剩下的地方清理干净就行了,这样做的好处就是大家的工作时间变长了,在房间外等待的时间变短了。
CMS 也是按这个逻辑把整个垃圾收集的过程分成四个阶段,分别是初始标记、并发标记、重新标记、并发清理四个阶段,然后CMS会根据每个阶段不同的特性来决定是否停顿用户线程。
阶段一:初始标记
初始标记的目的是先把所有GC Root直接引用的对象进行标记,因为需要避免在标记GC Root的过程还有程序在继续产生GC Root对象,所以这个过程是需要需要停止用户线程 ,因为这个过程只会标记GC Root的直接引用,并不会对整个GC Root的引用进行遍历,所以这个过程速度也是所有阶段中最快的。
阶段二:并发标记
并发标记阶段的工作就是把阶段一标记好的GC Root对象进行深度的遍历,找到所有与GC Root关联的对象并进行标记,这个过程中是采用多线程的方式进行遍历标记,对整个JVM 的GC Root进行遍历的过程是垃圾收集过程中最耗时的一步,CMS为了考虑尽量不停顿用户线程,所以这个阶段是不停止用户线程的,也就是说这个阶段JVM会分配一些资源给用户线程执行任务,通过这样的方式减少用户线程的停顿时间。
阶段三:重新标记
因为在阶段二的时候用户线程同时也在运行,这个过程中又会产生新的垃圾,所以重新标记阶段主要任务是把上一个阶段中产生的新垃圾进行标记( 使用多线程标记),很显然这个过程是对上一个阶段用户线程运行遗留的垃圾进行标记,所以数量 是非常少执行时间也是最短的,当然为了避免这个过程再次产生新的垃圾,所以重新标记的过程是会停顿用户线程的。
阶段四:并发清理
并发清理阶段是对那些被标记为可回收的对象进行清理,在一般情况下并发清理阶段是使用的标记清除法,因为这个过程不会牵扯到对象的地址变更,所以CMS在并发清理阶段是不需要停止用户线程的。也正因为并发清理阶段用户线程也可以同时运行,所以在用户线程运行的过程中自然也会产生新的垃圾,这也就是导致CMS收集器会产生“浮动垃圾”的原因。
当然,在一种情况下并发清理阶段CMS也会停顿用户线程,这就和我们之前说过的CMS选用的垃圾回收算法有关系,因为一般情况下使用的都是标记清除法,但是标记清除法的弊端就是在于会产生空间碎片,所以当空间碎片到达了一定程度时,此时CMS会使用标记整理法解决空间碎片的问题,不过因为标记整理法会将对象的位置进行挪动并更新对象的引用的指向地址,那么这个过程中用户线程同时运行的话会产生并发问题,所以当CMS进行碎片整理的时候必须得停止用户线程。
特点:
- 收集区域: 老年代。
- 使用算法: 标记清除法+标记整理法。
- 搜集方式: 多线程。
- 搭配收集器: ParNew。
- 优势: 多线程收集,收集过程不停止用户线程,所以用户请求停顿时间短。
CMS遗留的问题:
CMS收集器开辟了一条垃圾收集的新思路,不过这么好的垃圾收集器却一直没有被Hospot虚拟机纳入到默认的垃圾收集器,到Jdk8使用的默认收集器都还是 Parallel scavenge 和 Parallel old,这其中非常重要的原因就是CMS遗留了几个比较头疼的问题。
浮动垃圾
在并发清理阶段因为需垃圾收集线程是和用户线程同时执行任务的,这个时候用户线程运行时产生的垃圾是无法在当前阶段进行回收的,所以这段时间用户线程产生的新垃圾只能遗留到下一次收集,这些在垃圾收集过程中新产生的垃圾我们称为浮动垃圾。
CMS 算法对象消失问题解决方案(多标和错标),https://baijiahao.baidu.com/s?id=1719715247743913571&wfr=spider&for=pc,https://blog.csdn.net/weixin_45970271/article/details/123508686
- 重新标记阶段
- Incremental Update和Snapshot At The Beginning
空间碎片整理造成卡顿
CMS在平常情况下会使用标记清除法进行回收,只有在老年代的空间碎片达到一定程度,这个时候就会使用标记整理法对内存的空间碎片进行整理,因为标记整理的过程需要移动对象的位置,所以这个过程只能Stop the word,这个时候内存越大那么这个收集时间就越长,造成这种卡顿现象。
可能导致系统长时间的假死。
因为在并发清除阶段会有新的对象产生,在有担保机制的情况下,当新生代垃圾清理的时候存活的对象大多,导致Survior区无法容纳全部的对象,这时就会触发担保机制,这里存活的对象里面会有一部分会直接进入老年代,所以在每次GC的时候老年代需要预留一部分内存出来,所以通常CMS 在老年代占用了大概百分之七八十的时候就进行FullGC。
不过这段时间的产生对象的总体大小是未知的,如果新生代存活的对象非常多,这些担保的对象转移到老年代的时候可能导致老年代预留的空间也不足以容纳,那么此时CMS不得不进行一次Stop the word 的Full GC ,因为此时堆空间已经完全占满,这个时候已经无法使用并发的清理方式进行收集了,所以此时只能停止用户线程来专心进行垃圾收集,而这时候老年代收集器不得不从CMS切换成Serial old垃圾收集器来进行垃圾收集 。
至于这里为什么要使用单线程的Serial old,而不选择多线程的Parallel Old,那是因为CMS的新生代收集器是ParNew,而ParNew只能与CMS和Serial Old配合),所以这也是个无奈的选择。而切换成Serial old来进行垃圾收集的时候就有问题了,Serial old收集器是单线程的,它只适用于内存大小在几十到上百M的大小,而往往我们现在的内存大小都是几G到几十G,所以这种情况下整个垃圾收集的时间可能会特别特别长,有时候可能达到几个小时甚至好几天的都有可能。
G1收集器
CMS开创了垃圾收集器的一个新时代,它实现了垃圾收集和用户线程同时执行,达到垃圾收集的过程不停止用户线程的目标,这个思路作为后面的收集器提供了一个很好的典范。时代向前优化不止,除了需要解决了CMS遗留了的几个问题外,硬件资源的升级换代,可用的内存资源越来越多一直是促进垃圾收集器发展的一个核心驱动力,可使用的内存资源变多对于软件来说这当然是个好事,不过对于垃圾收集器来说就变得越来越麻烦了,随着发展我们发现传统垃圾收集器的收集方式已经不适用于这种大内存的垃圾收集了。
不管是Serial系列、Parallel系列、CMS系列,它们都是基于把内存进行物理分区的形式把JVM内存分成老年代、新生代、永久代或MetaSpace,这种分区模式下进行垃圾收集时必须对某个区域进行整体性的收集(比如整个新生代、整个老年代收集或者整个堆), 原来的内存空间都不是很大,一般就是几G到几十G,但现在的硬件资源发展可用的内存达到几百G甚至上T的程度,那么JVM中的某一个分代区域就可能会有几十上百G的大小,那么如果这时候采用传统模式下的物理分区的收集的话,每次垃圾扫描内存区域变大了、那么需要的清理时间自然就会变得更加长了;换做打扫卫生来说,原来你只需要打扫几个小办公室就行了,但是随着公司业务发展整栋楼是都是你公司了,这个时候你需要打扫公司卫生的时间无疑也会变得特别长。
所以问题出现了,那么自然就有人会来解决的,G1就是在这种环境下诞生的,G1首先吸取了CMS优良的思路,还是使用并发收集的模式,但是更重要的是G1摒弃了原来的物理分区,而是把整个内存分成若干个大小的Region区域,然后由不同的Region在逻辑上来组合成各个分代,这样做的好处是G1进行垃圾回收的时候就可以用Region作为单位来进行更细粒度的回收了,每次回收可以只针对某一个或多个Region来进行回收。
Region(局部收集):G1最核心的分区基本单位Region ,G1没有像之前一样把堆内存划分为固定连续的几块区域,而是完全舍弃了进行内存上的物理分区,而是把堆内存拆分成了大小为1M-32M的Region块,然后以Region为单位自由的组合成新生代、老年代、Eden区、survior区、大对象区(Humonggous Region),随着垃圾回收和对象分配每个Region也不会一直固定属于哪个分代,我们可以认为Region可以随时扮演任何一个分代区域的内存。
Collect Set(智能收集):在G1里面会维护一个Collect Set集合,这个里面记录了待回收的Region块信息同时也包括了每个Region块可回收的大小空间,也正是因为有了这个有了这个CSet信息G1在进行垃圾收集的时候,就可以根据用户设定的可接受停顿时间来进行分析,在设定的时间范围内收集哪些区域最划算而择优先收集的区域,这样不仅每次可以优先收集垃圾最多的Region,还可以根据用户的设定之间来计算收集哪些Region达到用户所期望的垃圾收集时间,通过CSet让G1的垃圾回收性价比非常高,并且可以通过这个实现可预测的停顿时间要求,让垃圾回收变得智能化,当然用户设定的时间也不能脱离实际,官方建议是在100ms-300ms之间。
原理:
G1的回收流程和CMS逻辑大致相同,分别进行初始标记、并发标记、重新标记、筛选清除,区别在最后一个阶段G1不会直接进行清除,而是会根据设置的停顿时间进行智能的筛选和局部的回收。
阶段一:初始标记
初始标记额目的是先把所有GC Root直接引用的对象进行标记,因为需要避免在标记GC Root的过程还有程序在继续产生GC Root对象,所以这个过程是需要停止用户线程 ,因为这个过程并不会对整个GC Root的引用进行遍历,所以这个过程速度是非常快的。
阶段二:并发标记
并发标记阶段的工作就是把阶第一段标记好的GC Root对象进行深度的遍历,找到所有与GC Root关联的对象并进行标记,这个过程中是采用多线程的方式进行遍历标记,对整个JVM 的GC Root进行遍历的过程是垃圾收集过程中最耗时的一步,为了尽量不停顿用户线程,所以这个阶段GC线程会和用户线程同时运行,通过这样的方式减少用户线程的停顿时间。
阶段三:最终标记
因为在上个阶段用户线程同时也在运行,用户线程运行的过程中又会产生新的垃圾,所以重新标记阶段主要任务是把上一个阶段中产生的新垃圾进行标记( 使用多线程标记),很显然这个过程是对上一个阶段用户线程运行遗留的垃圾进行标记,所以数量是非常少执行时间也是非常短的,当然为了避免这个过程再次产生新的垃圾,所以重新标记的过程是会停顿用户线程的。
阶段四:筛选回收
把存活的对象复制到空闲Region区域,再根据Collect Set记录的可回收Region信息进行筛选,计算Region回收成本,根据用户设定的停顿时间值制定回收计划,根据回收计划筛选合适的Region区域进行回收。
**回收算法:**从局部来说G1是使用的标记复制法,把存活对象从一个Region复制到另外的Region,但从整个堆来说G1的逻辑又相当于是标记整理法,每次垃圾收集时会把存活的对象整理到其他对应区域的Region里,再把原来的Region标记为可回收区域记录到CSet里,所以G1的每一次回收都是一次整理过程,所以也就不会产生空间碎片问题。
特点:
- 收集区域: 整个堆内存。
- 使用算法: 标记复制法
- 搜集方式: 多线程。
- 搭配收集器: 无需其他收集器搭配。
- 优势: 停顿时间可控,吞吐量高,可根据具体场景选择吞吐量有限还是停顿时间有限,不需要额外的收集器搭配。
- 劣势: 因为需要维护的额外信息比较多,所以需要的内存空间也要大,6G以上的内存才能考虑使用G1收集器。
为什么要有两个survivor区域
不同的场景使用适宜的垃圾回收器
在选择Java虚拟机(JVM)中的垃圾回收器时,需要考虑应用程序的特定需求,如吞吐量、停顿时间、内存消耗等因素。以下是针对Serial+Serial Old、PS+PO、ParNew+CMS、G1垃圾回收器适用场景的分析:
1. Serial+Serial Old垃圾回收器
适用场景:
- 单CPU或硬件配置有限的场景:Serial是一种单线程串行回收年轻代的垃圾回收器,Serial Old是其老年代版本,两者都适合在资源受限的环境中运行,如单核CPU或硬件配置较低的服务器。
- 小型应用和客户端程序:对于不需要高并发和高吞吐量的Java应用程序,如简单的桌面应用或小型服务器应用,Serial+Serial Old是一个经济有效的选择。
2. PS+PO(Parallel Scavenge+Parallel Old)垃圾回收器
适用场景:
- 后台批处理任务:PS+PO组合关注系统的吞吐量,适合执行后台任务,这些任务不需要与用户交互,并且可以容忍较长的停顿时间。
- 大数据处理和大文件导出:在处理大量数据或导出大文件时,PS+PO能够高效地管理内存,同时提供可接受的停顿时间。
- 多核CPU环境:Parallel Scavenge和Parallel Old都是多线程并行回收器,能够充分利用多核CPU的优势,提高垃圾回收的效率。
3. ParNew+CMS(Concurrent Mark Sweep)垃圾回收器
适用场景:
- 大型互联网系统:在大型互联网系统中,用户请求数和数据量都很大,对系统的响应时间和吞吐量都有较高要求。CMS垃圾回收器关注系统的暂停时间,可以减少用户线程的等待时间,提高用户体验。
- 高并发场景:如订单接口、商品接口等高频次访问的场景,CMS能够减少因垃圾回收导致的服务中断,保证系统的稳定性和可用性。
然而,需要注意的是,CMS垃圾回收器存在内存碎片问题、退化问题以及浮动垃圾问题,因此在选择时需要权衡这些因素。
4. G1(Garbage First)垃圾回收器
适用场景:
- 多CPU和大内存服务器:G1垃圾回收器设计用于具有多个CPU和大内存的服务器环境,如电商秒杀服务器、多路直播服务器等。
- 对停顿时间敏感的应用:G1能够在保证系统吞吐量的同时,提供可预测的停顿时间,这对于需要快速响应的应用程序至关重要。
- 大内存需求:G1将堆内存划分为多个区域(Region),能够处理大内存堆,并提供高效的垃圾回收性能。
综上所述,在选择垃圾回收器时,需要根据应用程序的具体需求和运行环境进行综合考虑。每种垃圾回收器都有其优势和局限性,选择最适合的回收器可以显著提高应用程序的性能和稳定性。
GC如何影响应用性能呢?
如果系统卡顿很明显,大概率就是频繁执行GC垃圾回收,频繁进入STW状态产生停顿的缘故。
todo 尝试研究,但没有成功发现不同的垃圾回收器如何影响应用性能?
GC相关命令
查看当前默认使用的垃圾回收器
java -XX:+PrintCommandLineFlags查看支持的非标准参数列表
java -XX:+PrintFlagsFinal打印GC日志到文件
varDirectory=`date '+%Y-%m-%d-%H-%M-%S'` && mkdir $varDirectory && java -jar -XX:+UseSerialGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./$varDirectory/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=128m java-performance.jar内存溢出时HeapDumpOnOutOfMemoryError
java -jar -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data -Xmx512m -Xms512m java-performance.jar指定JVM垃圾回收器
todo G1日志格式
指定
serial+serial old组合bashjava -jar -XX:+UseSerialGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=128m -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError -XX:HeapDumpPath=./ -Xmx2g -Xms2g java-performance.jarGC日志解析:2024-07-08T19:36:37.382+0800: 3.693: [Full GC (Metadata GC Threshold) 2024-07-08T19:36:37.382+0800: 3.693: [Tenured: 0K->14956K(6990528K), 0.1947035 secs] 894792K->14956K(10136256K), [Metaspace: 20303K->20303K(1067008K)], 0.1965993 secs] [Times: user=0.15 sys=0.05, real=0.20 secs]
这条日志是Java虚拟机(JVM)的垃圾收集(GC)日志,它详细记录了JVM在特定时间执行的一次全面垃圾收集(Full GC)的过程和结果。下面是对这条日志的详细解析:
- 时间戳:
2024-07-08T19:36:37.382+0800表示这次GC操作发生的时间是2024年7月8日,晚上7点36分37秒382毫秒,时区是东八区(+0800)。 - GC类型:
[Full GC (Metadata GC Threshold)]表示这是一次由元数据空间(Metaspace)GC阈值触发的全面垃圾收集。在Java中,Metaspace用于存储类的元数据,如类的结构信息。当Metaspace的使用量达到某个阈值时,JVM可能会触发Full GC来清理元数据空间中的无用数据。 - GC前后堆内存变化:
- Tenured(老年代):
0K->14956K(6990528K)表示在老年代(Tenured Generation)中,垃圾收集前占用空间为0KB(可能是因为之前的GC已经清理得很干净,或者这是应用启动后的第一次GC),收集后占用空间为14956KB,老年代的总容量为6990528KB。 - 总堆内存:
894792K->14956K(10136256K)表示在垃圾收集前,整个堆内存(包括年轻代和老年代)的占用为894792KB,收集后减少到14956KB,堆内存的总容量为10136256KB。
- Tenured(老年代):
- Metaspace变化:
[Metaspace: 20303K->20303K(1067008K)]表示在Metaspace中,GC前后的占用量都是20303KB,没有变化(这可能是因为Metaspace的GC主要关注于清理无用的类加载器及其关联的元数据,而当前可能没有这样的资源需要清理),Metaspace的总容量为1067008KB。 - GC耗时:
0.1965993 secs表示这次Full GC操作总共耗时约0.196秒。 - 时间分解:
[Times: user=0.15 sys=0.05, real=0.20 secs]提供了GC操作的时间分解,其中user时间表示CPU在用户模式下执行GC所花费的时间(0.15秒),sys时间表示CPU在内核模式下执行GC所花费的时间(0.05秒),real时间表示GC操作从开始到结束所经过的墙钟时间(0.20秒)。
总的来说,这次由Metadata GC Threshold触发的Full GC有效地清理了堆内存中的大部分对象,但Metaspace的占用并未显著变化。这种GC通常是为了保持JVM的稳定性和性能,尤其是在处理大量类和动态类加载时。
2024-07-08T19:48:19.203+0800: 705.514: [GC (Allocation Failure) 2024-07-08T19:48:19.203+0800: 705.514: [DefNew: 2796224K->113725K(3145728K), 0.4536438 secs] 2829917K->147418K(10136256K), 0.4542744 secs] [Times: user=0.20 sys=0.25, real=0.45 secs]
这条日志是Java虚拟机(JVM)的垃圾收集(GC)日志,记录了JVM在特定时间执行的一次年轻代(Young Generation)垃圾收集的过程和结果。下面是对这条日志的详细解析:
- 时间戳:
2024-07-08T19:48:19.203+0800表示这次GC操作发生的时间是2024年7月8日,晚上7点48分19秒203毫秒,时区是东八区(+0800)。 - GC类型:
[GC (Allocation Failure)]表示这是一次由于分配失败(即JVM在年轻代中没有足够的空间来为新对象分配内存)触发的年轻代GC。 - GC前后年轻代内存变化:
- DefNew(默认年轻代):
2796224K->113725K(3145728K)表示在年轻代中,GC前占用空间为2796224KB,GC后占用空间减少到113725KB,年轻代的总容量为3145728KB。这表明GC有效地清理了大量不再使用的对象。
- DefNew(默认年轻代):
- GC前后总堆内存变化:
- 总堆内存:
2829917K->147418K(10136256K)表示在GC前,整个堆内存(包括年轻代和老年代)的占用为2829917KB,GC后减少到147418KB,堆内存的总容量为10136256KB。这表明除了年轻代外,老年代中可能也有一部分对象被回收了(尽管老年代的变化量没有在这次日志中直接体现,但可以通过总堆内存的变化来推断)。
- 总堆内存:
- GC耗时:
0.4542744 secs表示这次GC操作总共耗时约0.454秒。
- 时间分解:
[Times: user=0.20 sys=0.25, real=0.45 secs]提供了GC操作的时间分解,其中user时间表示CPU在用户模式下执行GC所花费的时间(0.20秒),sys时间表示CPU在内核模式下执行GC所花费的时间(0.25秒),real时间表示GC操作从开始到结束所经过的墙钟时间(0.45秒)。这里real时间稍大于user和sys时间之和,可能是由于多线程执行或其他系统调度开销造成的。
总的来说,这次由分配失败触发的年轻代GC有效地回收了大量年轻代中的内存空间,减少了堆内存的占用,并保持了JVM的稳定性和性能。这种GC是JVM在运行时自动进行的,以确保有足够的空间来创建新对象。
2024-07-08T20:05:30.260+0800: 143.876: [GC (Allocation Failure) 2024-07-08T20:05:30.261+0800: 143.878: [DefNew: 619441K->619441K(629120K), 0.0017777 secs]2024-07-08T20:05:30.263+0800: 143.879: [Tenured: 1375522K->1398143K(1398144K), 0.7715911 secs] 1994963K->1464902K(2027264K), [Metaspace: 37579K->37579K(1083392K)], 0.7754386 secs] [Times: user=0.74 sys=0.04, real=0.78 secs]
这条日志记录了Java虚拟机(JVM)在特定时间执行的一次混合垃圾收集(Mixed GC,但通常在这种情况下指的是年轻代(Young Generation)和老年代(Old Generation)同时或相继进行的GC)的过程和结果。不过,从日志中可以看到一些不寻常的情况,我将逐一解析:
时间戳:
2024-07-08T20:05:30.260+0800表示GC操作开始的时间。GC类型:
[GC (Allocation Failure)]表明这次GC是由于内存分配失败触发的。年轻代(DefNew)GC:
619441K->619441K(629120K), 0.0017777 secs这部分显示年轻代GC前后占用空间没有变化,都是619441KB,且年轻代的总容量为629120KB。通常这不应该发生,因为GC的目的就是回收不再使用的内存空间。然而,这里可能发生了几种情况:
- 晋升失败:年轻代中的对象在GC时没有被回收,而是全部晋升到了老年代,但由于某种原因(如老年代空间不足),晋升操作被挂起或失败,导致年轻代空间看起来没有变化。
- 日志错误或误导:有时GC日志可能因为JVM内部状态或日志记录机制的问题而显示不准确的信息。
- 并发GC行为:如果使用的是并发GC算法(如CMS或G1),可能在日志记录时刻GC尚未完全完成或正在处理中。
老年代(Tenured)GC:
1375522K->1398143K(1398144K), 0.7715911 secs这表明老年代在GC前占用空间为1375522KB,GC后增加到1398143KB(这通常是不寻常的,因为GC通常应该减少内存占用),老年代的总容量为1398144KB。这种增加可能是由于年轻代中的对象晋升到老年代,但老年代没有足够的空间进行压缩或合并,导致整体占用增加。
总堆内存变化:
1994963K->1464902K(2027264K)表示GC前总堆内存占用为1994963KB,GC后减少到1464902KB,堆的总容量为2027264KB。这表明虽然年轻代和老年代的单独变化看起来有些不寻常,但总体上GC还是成功回收了一些内存。
元空间(Metaspace):
[Metaspace: 37579K->37579K(1083392K)]表示元空间(用于存储类的元数据)在GC前后没有变化,都是37579KB,且元空间的总容量为1083392KB。
GC耗时:
0.7754386 secs表示整个GC操作(包括年轻代和老年代)的总耗时约为0.775秒。
时间分解:
[Times: user=0.74 sys=0.04, real=0.78 secs]提供了GC操作的时间分解,其中user时间表示CPU在用户模式下执行GC所花费的时间(0.74秒),sys时间表示CPU在内核模式下执行GC所花费的时间(0.04秒),real时间表示GC操作从开始到结束所经过的墙钟时间(0.78秒)。
这次GC日志显示了一些不寻常的行为,特别是年轻代GC后空间没有减少,以及老年代GC后空间反而增加。这可能是由于JVM内部的特殊状态、GC算法的行为、或日志记录的问题导致的。建议进一步检查JVM的配置、GC算法的选择、以及是否有其他并发操作(如大量对象创建和销毁)影响了GC的行为。如果这种情况持续发生,可能需要调整JVM的堆内存设置或GC策略。
2024-07-08T20:05:34.684+0800: 148.300: [Full GC (Allocation Failure) 2024-07-08T20:05:34.685+0800: 148.301: [Tenured: 1398143K->1398143K(1398144K), 1.1595215 secs] 2027263K->1467832K(2027264K), [Metaspace: 37513K->37513K(1083392K)], 1.1602240 secs] [Times: user=1.14 sys=0.01, real=1.16 secs]
这条日志记录了一次全GC(Full GC)的过程和结果,它是在内存分配失败(Allocation Failure)时触发的。不过,从日志中我们可以看到几个不寻常的点,我将逐一分析:
- 老年代(Tenured)GC结果:
1398143K->1398143K(1398144K), 1.1595215 secs表示老年代在GC前和GC后的占用空间都是1398143KB,这几乎占满了老年代的全部容量1398144KB。通常,GC会回收不再使用的对象,并减少老年代的占用空间。但在这里,GC似乎没有成功回收任何内存。- 这可能是由于老年代中几乎所有的对象都是活跃的(即正在被引用),或者存在内存泄漏,导致GC无法回收它们。
- 总堆内存变化:
2027263K->1467832K(2027264K)表示GC前总堆内存占用为2027263KB(几乎占满了整个堆的容量2027264KB),而GC后减少到1467832KB。尽管老年代空间没有减少,但整个堆的占用却有所下降,这可能是由于年轻代(日志中没有直接显示年轻代的GC情况,但通常Full GC会包含年轻代和老年代的GC)中的对象被回收了。
- 元空间(Metaspace):
[Metaspace: 37513K->37513K(1083392K)]表示元空间在GC前后没有变化,都是37513KB,且元空间的总容量为1083392KB。这通常意味着没有类加载或卸载活动导致元空间的使用量发生变化。
- GC耗时:
1.1602240 secs表示整个Full GC操作的总耗时约为1.16秒。[Times: user=1.14 sys=0.01, real=1.16 secs]提供了GC操作的时间分解,其中user时间表示CPU在用户模式下执行GC所花费的时间(1.14秒),sys时间表示CPU在内核模式下执行GC所花费的时间(0.01秒),real时间表示GC操作从开始到结束所经过的墙钟时间(1.16秒)。
这次Full GC虽然触发了,但老年代的空间占用并没有减少,这很可能是由于老年代中的对象几乎都是活跃的,或者存在内存泄漏问题。
- 时间戳:
指定
PS+PO组合bashjava -jar -XX:+UseParallelGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=128m -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError -XX:HeapDumpPath=./ -Xmx2g -Xms2g java-performance.jarGC日志解析:2024-07-08T20:20:06.653+0800: 9.772: [GC (Metadata GC Threshold) [PSYoungGen: 251231K->19565K(611840K)] 266179K->34585K(2010112K), 0.0607529 secs] [Times: user=0.11 sys=0.03, real=0.07 secs]
这条日志是Java虚拟机(JVM)在进行垃圾收集(Garbage Collection, GC)时产生的,具体解释如下:
- 时间戳:
2024-07-08T20:20:06.653+0800表示这条日志发生的时间是2024年7月8日,晚上8点20分06秒653毫秒,时区是东八区(UTC+08:00)。 - GC类型:
[GC (Metadata GC Threshold)]表示这次GC是由于元数据(Metadata)的GC阈值被触发而进行的。在HotSpot JVM中,元数据区(Metaspace)用于存储类的元数据,如类的结构信息。当元数据区使用的内存达到某个阈值时,JVM会触发GC尝试回收一些无用的类元数据。 - GC区域:
[PSYoungGen: 251231K->19565K(611840K)]这部分描述了年轻代(Young Generation)的GC情况。PSYoungGen指的是Parallel Scavenge收集器管理的年轻代。251231K->19565K表示GC前年轻代使用了约251MB的空间,GC后年轻代剩余约19MB的空间。(611840K)表示年轻代的总容量为约612MB。 - 堆内存变化:
266179K->34585K(2010112K)这部分描述了整个堆(Heap)在GC前后的内存使用情况。GC前堆使用了约266MB的空间,GC后剩余约34MB的空间。(2010112K)表示堆的总容量为约2GB。 - GC耗时:
0.0607529 secs表示这次GC操作耗时约0.06秒。 - 时间分配:
[Times: user=0.11 sys=0.03, real=0.07 secs]这部分提供了GC操作的时间分配情况。user=0.11表示用户态(User Mode)下CPU花费的时间,sys=0.03表示内核态(System Mode)下CPU花费的时间,real=0.07 secs表示从GC开始到结束所经过的墙钟时间(Wall Clock Time),即实际经过的时间。
总的来说,这条日志表明JVM进行了一次由于元数据GC阈值触发的GC操作,主要清理了年轻代的内存,使得整个堆的内存使用量大幅下降,GC操作耗时较短,对系统性能的影响较小。
2024-07-08T20:20:06.716+0800: 9.835: [Full GC (Metadata GC Threshold) [PSYoungGen: 19565K->0K(611840K)] [ParOldGen: 15019K->24145K(1398272K)] 34585K->24145K(2010112K), [Metaspace: 33545K->33545K(1079296K)], 0.1940970 secs] [Times: user=0.49 sys=0.06, real=0.19 secs]
这条日志提供了关于Java虚拟机(JVM)进行了一次完全垃圾收集(Full GC)的详细信息,具体解释如下:
- 时间戳:
2024-07-08T20:20:06.716+0800表示GC事件发生在2024年7月8日晚上8点20分06秒716毫秒,时区是东八区(UTC+08:00)。 - GC类型:
[Full GC (Metadata GC Threshold)]表示这是一次由于元数据(Metaspace)GC阈值被触发而导致的完全垃圾收集。完全GC会清理整个堆内存(包括年轻代和老年代),以及可能的永久代或元空间(取决于JVM版本)。 - 年轻代GC详情:
[PSYoungGen: 19565K->0K(611840K)]表示在年轻代(Young Generation)中,GC前使用了约19MB的空间,GC后空间被完全清空,即没有对象存活。年轻代的总容量为约612MB。 - 老年代GC详情:
[ParOldGen: 15019K->24145K(1398272K)]表示在老年代(Old Generation)中,GC前使用了约15MB的空间,GC后剩余约24MB的空间。老年代的总容量为约1.39GB。注意,这里GC后空间使用量的增加表明有存活的对象从年轻代晋升到了老年代。 - 堆内存变化:
34585K->24145K(2010112K)表示整个堆在GC前后的内存使用情况。GC前堆使用了约34MB的空间,GC后剩余约24MB的空间。堆的总容量为约2GB。 - 元空间GC详情:
[Metaspace: 33545K->33545K(1079296K)]表示在元空间(Metaspace)中,GC前后的使用量没有变化,都是约33MB。元空间的总容量为约1.08GB。这表明这次GC并没有回收任何元空间中的内存,可能是因为没有类被卸载。 - GC耗时:
0.1940970 secs表示这次完全GC操作耗时约0.19秒。 - 时间分配:
[Times: user=0.49 sys=0.06, real=0.19 secs]提供了GC操作的时间分配情况。user=0.49表示用户态下CPU花费的时间,sys=0.06表示内核态下CPU花费的时间,real=0.19 secs表示从GC开始到结束所经过的墙钟时间。
总的来说,这次完全GC主要清理了年轻代中的内存,但老年代的使用量有所增加,表明有存活的对象被晋升。元空间的使用量没有变化。GC操作耗时较短,对系统性能的影响相对较小。然而,如果频繁发生完全GC,可能会对系统性能造成较大影响,需要关注并优化应用的内存使用情况。
2024-07-08T20:22:08.444+0800: 131.563: [GC (Allocation Failure) [PSYoungGen: 295040K->28384K(478720K)] 1624659K->1386758K(1876992K), 0.0360143 secs] [Times: user=0.10 sys=0.05, real=0.03 secs]
这条日志是Java虚拟机(JVM)在进行一次年轻代(Young Generation)垃圾收集(Garbage Collection, GC)时产生的,具体解释如下:
- 时间戳:
2024-07-08T20:22:08.444+0800表示GC事件发生在2024年7月8日晚上8点22分08秒444毫秒,时区是东八区(UTC+08:00)。 - GC类型:
[GC (Allocation Failure)]表示这次GC是由于年轻代中无法为新对象分配内存(Allocation Failure)而触发的。当年轻代中没有足够的空间来存放新创建的对象时,JVM会触发一次年轻代GC以尝试回收内存空间。 - 年轻代GC详情:
[PSYoungGen: 295040K->28384K(478720K)]表示在年轻代中,GC前使用了约295MB的空间,GC后剩余约28MB的空间。年轻代的总容量为约479MB。这表明GC回收了大部分年轻代中的对象,但仍有约28MB的对象存活下来。 - 堆内存变化:
1624659K->1386758K(1876992K)表示整个堆在GC前后的内存使用情况。GC前堆使用了约1625MB的空间,GC后剩余约1387MB的空间。堆的总容量为约1877MB。这表明除了年轻代之外,老年代(Old Generation)和其他区域(如永久代/元空间,如果有的话)也参与了内存回收,但整体堆的使用量仍然很高。 - GC耗时:
0.0360143 secs表示这次GC操作耗时约0.036秒。 - 时间分配:
[Times: user=0.10 sys=0.05, real=0.03 secs]提供了GC操作的时间分配情况。user=0.10表示用户态下CPU花费的时间,sys=0.05表示内核态下CPU花费的时间,real=0.03 secs表示从GC开始到结束所经过的墙钟时间(Wall Clock Time)。这里real时间最短,说明GC操作在CPU上几乎没有等待,执行效率较高。
总的来说,这次年轻代GC有效地回收了部分内存,但堆的整体使用量仍然很高,可能需要关注应用的内存使用情况,以及是否有内存泄漏等问题。此外,年轻代的容量从612MB减少到了479MB,这可能是由于JVM动态调整了年轻代的大小,或者是在之前的GC中回收了部分年轻代的内存空间并将其用于其他用途。
2024-07-08T20:24:43.901+0800: 287.020: [Full GC (Ergonomics) [PSYoungGen: 232960K->69998K(465920K)] [ParOldGen: 1398267K->1398267K(1398272K)] 1631227K->1468266K(1864192K), [Metaspace: 37493K->37493K(1083392K)], 0.3468202 secs] [Times: user=1.95 sys=0.05, real=0.35 secs]
这条日志是Java虚拟机(JVM)进行了一次完全垃圾收集(Full GC)的详细记录,具体解释如下:
- 时间戳:
2024-07-08T20:24:43.901+0800表示GC事件发生在2024年7月8日晚上8点24分43秒901毫秒,时区是东八区(UTC+08:00)。 - GC类型:
[Full GC (Ergonomics)]表示这次完全GC是由JVM的自动调优机制(Ergonomics)触发的。JVM的自动调优机制会根据堆的使用情况和其他因素来决定何时执行完全GC。 - 年轻代GC详情:
[PSYoungGen: 232960K->69998K(465920K)]表示在年轻代(Young Generation)中,GC前使用了约233MB的空间,GC后剩余约70MB的空间,但其中有大量对象仍然存活(没有被回收)。年轻代的总容量为约466MB。 - 老年代GC详情:
[ParOldGen: 1398267K->1398267K(1398272K)]表示在老年代(Old Generation)中,GC前后的使用量没有变化,都是约1398MB。老年代的总容量为约1398MB。这表明老年代中没有可回收的对象,或者回收的收益很低,因此JVM没有对其进行实质性的清理。 - 堆内存变化:
1631227K->1468266K(1864192K)表示整个堆在GC前后的内存使用情况。尽管GC被执行了,但堆的整体使用量仅从约1631MB减少到约1468MB,减少的量相对较少。堆的总容量为约1864MB。 - 元空间GC详情:
[Metaspace: 37493K->37493K(1083392K)]表示在元空间(Metaspace)中,GC前后的使用量没有变化,都是约37MB。元空间的总容量为约1083MB。这表明元空间中也没有可回收的类元数据。 - GC耗时:
0.3468202 secs表示这次完全GC操作耗时约0.35秒。 - 时间分配:
[Times: user=1.95 sys=0.05, real=0.35 secs]提供了GC操作的时间分配情况。user=1.95表示用户态下CPU花费的时间,sys=0.05表示内核态下CPU花费的时间,real=0.35 secs表示从GC开始到结束所经过的墙钟时间(Wall Clock Time)。这里real时间非常接近用户态和内核态时间的总和,说明GC操作在CPU上几乎没有等待。
问题分析:
- 老年代未回收:老年代在GC前后的使用量没有变化,这可能是由于老年代中的对象都是存活的,或者存在内存泄漏的情况。需要进一步检查应用逻辑和内存使用情况,以确定是否有不必要的对象被长时间保留在内存中。
- 年轻代回收效率低:尽管年轻代在GC后释放了一些空间,但仍有大量对象存活(约70MB),这可能表明年轻代的大小设置不当,或者应用创建了大量短命但存活时间较长的对象。
- 性能影响:虽然这次GC的耗时相对较短(约0.35秒),但如果频繁发生完全GC,仍然会对应用的性能产生较大影响。
- 时间戳:
指定
ParNew+CMS组合bashjava -jar -XX:+UseConcMarkSweepGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=128m -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError -XX:HeapDumpPath=./ -Xmx2g -Xms2g java-performance.jartodo
GC日志解析:指定
G1bashjava -jar -XX:+UseG1GC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=128m -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError -XX:HeapDumpPath=./ -Xmx2g -Xms2g java-performance.jartodo
GC日志解析:
在线查看jvm GC情况
- 使用
jstat命令在线查看GC情况,请参考 - 使用
arthas的memory、jvm、dashboard命令查看内存和GC情况 - 参考上面
GC相关命令配置java启动命令打印应用GC信息
OOM分析
获取heapdump文件
- 使用
jmap获取heapdump文件 - 使用
arthas获取heapdump文件 - 使用
-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=/data在应用OOM时heapdump
分析heapdump文件
使用
jprofiler分析heapdump文件为何选择jprofiler?因为jhat解析1g以上的heapdump非常慢,mat解析8g以上的heapdump会报告OOM
找出可疑的内存泄露步骤:
使用
heapwalker>Current Object Set>Classes视图找出数量或占用字节最多的class右键使用上面步骤的
class选择上下文菜单中的Use Selected Objects功能后弹出New Object Set面板,在面板中选择Biggest objects视图后点击OK按钮。此时跳转到
Biggest objects视图,通过分析大对象推断出代码内存泄漏位置
使用
jvisualvm分析heapdump文件jvisualvm没有jprofiler的Merged incoming references的方便功能,需要通过抽样分析内存对象追溯导致内存泄漏的位置。
OOM分析
dump堆文件
- 通过
java命令行参数设置OOM错误时自动dump堆文件 - 使用
arthas dump堆文件 - 使用
jmap命令dump堆文件
使用jprofiler分析堆文件
通过jprofiler的heap walker的Biggest objects视图推断出内存泄漏的代码位置
合理设置jvm内存
todo
- 是否有更加科学的方法通过模拟去收集和判断在生成环境应该分配多大内存给
jvm? - 做实验证明频繁
gc对并发性能的影响
理论分析
以一个每日百万级别的交易支付系统作为背景,来分析一下,在线上部署一个系统时,应该如何根据系统的业务来合理的设置JVM对内存的大小。
如果每日有百万笔交易,在JVM的角度来看,就是每天会在JVM中创建上百万个新的支付订单对象
假设每天有100万个支付订单一般用户交易行为会集中在几个小时内的高峰期中。
对于高峰期而言,每秒需要能够支持处理100笔订单左右。
假设我们的支付系统部署了3台机器,每台机器实际上每秒需要处理30笔订单。
支付订单需求
- 一次支付请求,需要在JVM中创建一个支付订单对象,填充数据,然后办这个支付订单写入数据库,进行付款等等。
- 假设一次支付请求的处理,从头到尾,总共大概需要1秒的时间。
- 那么对于每台机器而言,一秒钟接受到30笔支付的请求,然后在JVM的新生代理创建了30 个支付订单对象。
- 1秒钟之后,对这30个支付订单进行处理,就可以对它们的引用进行回收,这些订单对象就成为JVM新生代里面,没人引用的垃圾对象了。
支付对象大小:
- 直接根据支付订单类中的实例变量的类型来计算,比如:
- Integer类型的变量数据是4个字节
- Long类型变量数据是8个字节
- 一般来说,支付订单这种核心累呢,按照20-30个实例变量来计算,一个对象大概也就是在1kb左右。
- 对于一台机器来说,每秒钟处理30笔支付订单的请求,大概占据的空间最多就是30*1kb而已。
运行过程
- 但是真实的支付系统在运行过程中,肯定每秒还会创建大量的其他对象。我们估算一下,每秒钟除了支付订单对象,还会创建其他对象
- 对于30个订单对象,需要30*1KB=30KB的内存空间,算上其他对象,那么每秒钟创建出来的对象,一共大概需要接近1MB左右。
- 然后下一秒,对新的请求继续创建大概1MB左右的对象,放在新生代里面。
- 循环多次后,新生代垃圾太多,就会触发Minor GC回收掉这些垃圾。
JVM堆内存设置
- 其实一般来说线上业务系统,常见的机器配置是2核4G,或者是4核8G
- 如果用2核4G的机器来部署,机器4G内存,JVM进程估计最多也就是2G内存。
- 这2G还得分给元空间、栈内存、堆内存几块区域,那么堆内存估计可能也就1个多G的内存空间。堆内存还分为新生代和老年代,那么新生代可能也就几百MB的内存
- 整个系统每秒需要1MB左右的内存空间,新生代只有几百MB,运行几百秒也就是大概五六分钟左右,新生代内存空间就满了,肯定会触发Minor GC。如果这么频繁的触发Minor GC,必然会影响线上系统的性能稳定性
证明上面理论分析
编译 demo-java-assistant 演示
bashmvn package运行演示
bashjava -jar target/demo.jar memallocpeak使用
jstat或者arthas memory查看堆内存使用结论:通过查看堆内存使用情况,可以看到老年代内存使用接近
512m符合以上理论预期
合理设置内存
编译和运行 demo-springboot-performance 演示,协助分析垃圾回收相关信息
bash# 编译 mvn package # 启动springboot项目 # 使用-XX:NewSize=64m和-XX:MaxNewSize=64m设置过小的内存导致频繁YoungGC和FullGC java -jar -Xmx1g -Xms1g -XX:NewSize=64m -XX:MaxNewSize=64m -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=128m -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError -XX:HeapDumpPath=./ target/demo-springboot-performance.jar使用
jmeter打开并运行 memory负载.jmx查看
jvm内存和GC情况并通过-Xmx -Xms调整jvm内存bashtail -f gc.log.0.current结论
jvm内存设置过低会报告OOM错误jvm内存设置过低如果不报告OOM错误也会导致频繁Full GC影响并发性能- 暂时发现
PS+PO、CMS、G1垃圾回收器对性能影响相同,所以使用默认的垃圾回收器即可 - 使用
-XX:NewSize和-XX:MaxNewSize设置过小的新生代区会导致较多的新生代GC,进而导致老年代区占用迅速膨胀,最后导致频繁Full GC影响并发性能 - 线程设置数量和内存设置成正比,否则会导致较多的新生代
GC,进而导致老年代区占用迅速膨胀,最后导致频繁Full GC影响并发性能 - 如果不是内存泄漏问题,一般通过
-Xmx、-Xms把内存调大能够解决频繁GC引起的稳定性和性能问题