JVM的组成
Java虚拟机(JVM)是执行Java字节码的运行时环境。它由以下几个主要部分组成:
- **类加载器(ClassLoader)**:
-
负责加载Java类的字节码到JVM中,并进行链接和初始化。
关于Java的类加载器,以下是一些关键点,它们对于理解Java类如何被加载、链接和初始化非常重要:
-
类加载过程:
- Java的类加载过程包括三个主要步骤:加载(Loading)、链接(Linking)、初始化(Initialization)。
-
加载:
- 加载是类加载器读取类文件的二进制数据,并将其转换为方法区中的运行时数据结构的过程。
-
链接:
- 链接包括验证(Verification)、准备(Preparation)和解析(Resolution)三个子阶段。验证确保加载的类信息符合JVM规范;准备负责为静态变量分配内存并设置默认初始值;解析将符号引用转换为直接引用。
-
初始化:
- 初始化是为静态变量赋予正确的初始值,并执行静态代码块的过程。
-
类加载器类型:
- Java提供了多种类型的类加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载Java核心库,如
rt.jar
。
加载lib目录下的类库 - 扩展类加载器(Extension ClassLoader):负责加载扩展目录中的类库。
加载ext目录下的类库 - 系统类加载器(System ClassLoader):负责加载应用程序类路径(
-classpath
参数)上的类。
也称为应用类加载器,负责加载用户类路径(-classpath参数或系统属性java.class.path)上指定的类库。 - 自定义类加载器(User-Defined ClassLoader):应用程序可以自定义类加载器来控制类的加载过程。
- 启动类加载器(Bootstrap ClassLoader):负责加载Java核心库,如
- Java提供了多种类型的类加载器:
-
双亲委派模型(Parent Delegation Model):
- 类加载器使用双亲委派模型来查找类。当一个类请求被加载时,它会首先委托给它的父类加载器去尝试加载这个类,只有当父类加载器无法完成这个请求时,子类加载器才会尝试自己去加载。
-
类的唯一性:
- 在同一个Java虚拟机中,任何一个类只有一个实例,即使由不同的类加载器加载也是如此。
-
类加载器的层次结构:
- 类加载器形成了一个层次结构,通常从启动类加载器到系统类加载器,再到自定义类加载器。
-
安全性:
- 类加载器确保来自不同源的类是隔离的,例如,从网络上加载的类不会影响系统类加载器加载的类。
-
资源优化:
- 类加载器允许应用程序在运行时动态地加载和卸载类,这有助于资源管理和优化。
-
类卸载:
- 在某些情况下,如类不再被使用,JVM可以卸载这些类以释放内存。
-
类加载器的实现:
- Java允许开发者通过继承
java.lang.ClassLoader
类来实现自定义的类加载器。
- Java允许开发者通过继承
了解类加载器的工作原理对于Java开发者来说非常重要,特别是在需要动态加载类、隔离类版本、扩展应用程序功能或处理不同类路径场景时。
-
- **运行时数据区(Runtime Data Areas)**:
-
包括以下几个部分:
-
**堆(Heap)**:存储所有Java对象实例和数组。
-
**方法区(Method Area)**:存储已被虚拟机加载的类信息、常量、静态变量等。在Java 8之前,这部分被称为永久代(Permanent Generation,PermGen)。
-
**栈(Stack)**:每个线程都有自己的栈,用于存储局部变量和方法调用信息。
-
**程序计数器(Program Counter)**:每个线程都有一个独立的程序计数器,用于跟踪当前执行的字节码指令。
-
**本地方法栈(Native Method Stack)**:用于存储本地方法(如JNI调用)的调用信息。
- **执行引擎(Execution Engine)**:
- 负责解释执行字节码或通过即时编译器(JIT)将字节码编译为本地机器代码后执行。
- **垃圾回收器(Garbage Collector, GC)**:
- 负责自动回收不再使用的对象,以管理堆内存。
- **本地接口(Native Interface)**:
- 允许Java代码调用和使用本地应用程序编程接口(API),例如JNI(Java Native Interface)。
- **即时编译器(Just-In-Time Compiler, JIT)**:
- 将热点代码(经常执行的代码)编译为机器代码,以提高执行效率。
- **内存管理器(Memory Manager)**:
- 负责管理JVM的内存分配和回收。
- **安全管理系统(Security Manager)**:
- 控制对系统资源的访问,确保代码的安全性。
- **线程调度器(Thread Scheduler)**:
- 管理线程的创建、调度和执行。
- **编译系统和解释器(Compiler and Interpreter)**:
- 解释器负责直接执行字节码,而编译系统负责将字节码编译为本地代码。
- **性能分析器(Profiler)**:
- 用于监控和分析JVM的性能。
这些组件共同工作,为Java应用程序提供了一个稳定、安全且高效的运行环境。JVM的架构设计允许它在不同的操作系统和硬件平台上运行,确保了Java语言的跨平台特性。
JDK8 JVM内存结构
在 JDK 8 中,JVM 的内存结构主要包括以下几个运行时数据区域:
- **堆(Heap)**:
-
堆是JVM中最大的一块内存区域,主要用于存储对象实例和数组。
-
堆是垃圾回收器管理的主要区域,经常发生垃圾回收操作。
JVM堆是Java虚拟机中用于存储对象实例和数组的主要内存区域。以下是JVM堆中存储的一些主要内容:
-
对象实例:
- 所有通过Java关键字
new
创建的对象实例都存储在堆中。
- 所有通过Java关键字
-
数组:
- 所有的数组,无论其元素类型如何(原始类型或引用类型),都存储在堆中。
-
实例变量:
- 对象的非静态成员变量也存储在堆中,与对象实例一起。
-
匿名内部类:
- 匿名内部类(即使没有具体名称的内部类)的实例同样存储在堆中。
-
反射对象:
- 通过Java反射API创建的类对象也存储在堆中。
-
字符串常量:
- 尽管字符串常量可能会存储在字符串常量池中,但通过
new
操作符创建的字符串对象实例仍然存储在堆中。
- 尽管字符串常量可能会存储在字符串常量池中,但通过
-
封装类对象:
- 封装类(如
Integer
、Double
等)的实例存储在堆中。
- 封装类(如
-
枚举实例:
- 枚举类型(
enum
)的实例也存储在堆中。
- 枚举类型(
-
异常对象:
- 当抛出异常时,异常对象的实例会存储在堆中,直到异常被处理。
-
软引用、弱引用、虚引用:
- 这些引用类型指向的对象也存储在堆中,尽管它们可能在垃圾回收时被回收。
-
动态代理对象:
- 使用Java动态代理API创建的代理对象实例存储在堆中。
-
Java本地方法接口(JNI)对象:
- 通过JNI创建的对象实例同样存储在堆中。
JVM堆是垃圾回收器管理的主要区域,因为大多数对象都是短暂的,并在此处进行分配和回收。堆内存的大小可以通过JVM启动参数(如
-Xms
和-Xmx
)进行配置。了解堆中存储的内容有助于开发者进行内存管理和性能优化。JVM堆中的内存被划分为不同的代,主要是为了更有效地进行垃圾回收。主要分为以下两个部分:
- 新生代(Young Generation):
- 新生代是新创建的对象存储的地方。
- 它通常占据堆内存的较小部分,并且被进一步划分为三个区域:
- Eden区:大多数新对象首先被分配到Eden区。
- Survivor区:为了能够回收内存,经过一次垃圾回收后仍然存活的对象会被复制到Survivor区。Survivor区有两个,通常被称为S0和S1,它们交替使用。
- 新生代使用复制算法进行垃圾回收,称为Minor GC。这个过程涉及将存活的对象从Eden区和Survivor区复制到另一个Survivor区,然后清理Eden区和当前使用的Survivor区。
Eden区和S0和S1内存划分比例值是8:1:1
Eden区内存满了就会触发Minor GC
- 老年代(Old Generation或Tenured Generation):
- 老年代用于存储在新生代中经过多次垃圾回收后仍然存活的对象。
- 这些对象通常已经存在较长时间,并且被认为不太可能在近期内变得垃圾。
- 老年代占据堆内存的较大部分,并使用不同的垃圾回收算法,如标记-清除或标记-清除-整理算法。
- 老年代的垃圾回收,称为Major GC或Full GC(如果涉及到整个堆的回收),通常比新生代的回收要慢,并且可能造成应用程序的停顿。
线上项目中一定要尽量避免老年代内存占满,老年代内存满了后就会触发Full GC 会产生Stop the world,这个时候除了Full GC线程会继续执行,其他线程用户线程都会暂停,等待Full GC线程执行完,其他用户线程才会继续执行。
晋升(Promotion):
GC扫一次没有清理,那么年龄就会+1,达到一定年龄(默认是15【CMS GC 默认是6岁】)了就会晋升到老年代。
对象在新生代中经过多次垃圾回收后,会根据其年龄(由垃圾回收次数决定)被晋升到老年代。这个过程有助于减少老年代中的对象数量,因为只有那些长期存活的对象才会被移动到那里。分代收集策略:
JVM的分代垃圾回收策略基于这样一个观察:大多数对象都是短暂存在的,而只有少数对象会长期存活。通过在新生代和老年代使用不同的垃圾回收算法,JVM可以优化内存回收的效率和速度。了解新生代和老年代的概念对于分析和优化Java应用程序的内存使用和垃圾回收性能至关重要。
-
- **方法区(Method Area)**:
-
也称为永久代(PermGen),但在JDK 8中已经被元空间(Metaspace)所取代。
-
方法区用于存储类信息、常量、静态变量、方法字节码等数据。
- **栈(Stack)**:
-
每个线程都有自己的虚拟机栈,用于存储局部变量、方法参数、方法调用和返回值。方法开始的时候会进栈,方法执行完成会出栈,相当于清空了数据,所以不用进行GC操作。
-
栈帧(Stack Frame)是栈的基本单位,每个方法调用都会创建一个新的栈帧。
- **程序计数器(Program Counter)**:
- 每个线程都有一个独立的程序计数器,用于记录当前线程执行的字节码指令地址。
- **本地方法栈(Native Method Stack)**:
- 用于存储本地方法(如JNI调用)的调用信息。
- **元空间(Metaspace)**:
-
JDK 8中引入,用于替代JDK 7及以前版本的永久代。
-
元空间不位于虚拟机内存中,而是使用本地内存,用于存储类的元数据信息。
- **代码缓存(Code Cache)**:
- 用于存储JIT编译器编译后的本地机器代码,以提高性能。
- **运行时常量池(Runtime Constant Pool)**:
- 属于方法区的一部分,用于存储类中的常量,如字符串字面量、数字常量等。
- **直接内存(Direct Memory)**:
- 不是JVM运行时数据区的一部分,但Java程序可以通过NIO操作直接内存。
在 JDK 8 中,除了上述内存区域,还引入了一些新的垃圾回收特性,如G1垃圾回收器的进一步优化,以及用于提高性能的JVM参数调整等。了解这些内存结构对于进行JVM调优和性能分析非常重要。
JVM调优参数
JVM 提供了一系列参数,可以通过命令行启动Java应用程序时进行调整。这些参数分为不同的类别,包括内存设置、垃圾收集器配置、性能监控和日志记录等。以下是一些常见的JVM参数:
1. 内存管理参数:
-
`-Xms<size>`:设置JVM启动时的初始堆内存大小。
-
`-Xmx<size>`:设置JVM可以使用的最大堆内存大小。
-
`-Xss<size>`:设置每个线程的栈大小。
-
`-XX:NewSize=<size>`:设置新生代的初始大小。设置新生代(Young Generation)的初始大小,但请注意,设置新生代大小可能会间接影响到老年代的大小,因为整个堆的大小(由
-Xmx
参数设置)是固定的。 -
`-XX:MaxNewSize=<size>`:设置新生代的最大大小。同样,这会影响老年代可用的内存。
-XX:OldSize=<size>:直接设置老年代的初始内存大小。
java -XX:MaxOldSize=4g -jar YourApplication.jar
-XX:MaxOldSize=<size>:设置老年代的最大内存大小。
-XX:NewRatio=<value>:设置新生代与老年代的比率。例如,如果设置为3,意味着新生代占总堆大小的1/4,老年代占3/4。
-XX:SurvivorRatio=<value>:设置新生代中Eden区与Survivor区的比例。这个比例会影响到每次Minor GC后存活对象晋升到老年代的速率。
-XX:InitiatingHeapOccupancyPercent=<value>:设置触发老年代垃圾回收的堆占用阈值。当老年代的内存占用达到这个百分比时,会触发Full GC。
-
`-XX:PermSize=<size>`(Java 8之前):设置永久代的初始大小。
-
`-XX:MaxPermSize=<size>`(Java 8之前):设置永久代的最大大小。
-
`-XX:MetaspaceSize=<size>`(Java 8及之后):设置元空间的初始大小。
-
`-XX:MaxMetaspaceSize=<size>`(Java 8及之后):设置元空间的最大大小。
2. 垃圾收集器参数:
-
`-XX:+UseSerialGC`:使用串行垃圾收集器。
-
`-XX:+UseParallelGC`:使用并行垃圾收集器。
-
`-XX:+UseConcMarkSweepGC`:使用CMS垃圾收集器。
-
`-XX:+UseG1GC`:使用G1垃圾收集器。
-
`-XX:+UseZGC`(Java 11及之后):使用ZGC垃圾收集器(实验性)。
-
`-XX:+UseShenandoahGC`:使用Shenandoah垃圾收集器(实验性)。
3. 性能监控参数:
-
`-XX:+PrintGC`:打印GC发生的情况。
-
`-XX:+PrintGCDetails`:打印GC的详细日志。
-
`-XX:+PrintGCDateStamps`:在GC日志中添加时间戳。
-
`-Xloggc:<file>`:将GC日志输出到指定文件。
-
`-XX:+UseGCLogFileRotation`:启用GC日志文件轮替。
-
`-XX:NumberOfGCLogFiles=<files>`:指定GC日志文件的数量。
-
`-XX:GCLogFileSize=<size>`:指定GC日志文件的大小。
4. JIT编译器参数:
-
`-XX:+PrintCompilation`:打印JIT编译方法的信息。
-
`-XX:+PrintInlining`:打印JIT编译过程中的内联信息。
5. 线程参数:
-
`-XX:ThreadStackSize=<size>`:设置线程栈的大小。
-
`-XX:+UseLargePages`(某些系统):使用大页内存,可以减少内存占用和提升性能。
6. 其他参数:
-
`-Djava.security.egd=file:/dev/./urandom`:设置随机数生成器的熵源。
-
`-XX:+HeapDumpOnOutOfMemoryError`:在发生OOM时生成堆转储。
-
`-XX:HeapDumpPath=<path>`:指定堆转储文件的路径。
7. 实验性参数(可能在不同版本中有所不同):
-
`-XX:+UnlockExperimentalVMOptions`:解锁实验性参数。
-
`-XX:+UseContainerSupport`(Java 19及之后):启用对容器化环境的支持。
这些参数可以根据应用程序的具体需求和运行环境进行调整。建议在进行调优时,结合实际的性能测试结果和监控数据来决定最合适的参数配置。
垃圾回收机制
判断什么是垃圾
在Java中,判断一个对象是否成为垃圾,即是否可被垃圾回收器(Garbage Collector,GC)回收,主要基于以下条件:
- **无法到达性(Unreachability)**:
对象没有任何引用与之相连,即从GC Roots开始无法到达该对象。
- **GC Roots的起点**:
GC Roots是垃圾收集器进行可达性分析时的起始点,包括:
-
静态字段(`static`字段)中的对象引用。
-
局部变量表(栈帧中的局部变量)中的对象引用。
-
活跃线程的引用,包括Java方法栈和本地方法栈。
-
同步锁(`synchronized`关键字所创建的锁)的对象。
-
被Java虚拟机引用的对象,如系统类加载器。
- **可达性分析(Reachability Analysis)**:
垃圾回收器会定期进行可达性分析,从GC Roots开始遍历所有可达对象。所有不可达的对象被认为是垃圾。
- **finalize()方法**:
如果对象没有被引用,并且没有被垃圾回收器回收,它可能会成为垃圾回收的候选对象。如果对象定义了`finalize()`方法,并且该方法还未被调用,垃圾回收器可能会调用这个方法。但自Java 9起,`finalize()`方法已被标记为过时,并推荐使用`Cleaner`类来实现类似功能。
- **引用类型**:
-
**强引用(Strong References)**:普通的引用,如`Object obj = new Object();`。
-
**软引用(Soft References)**:`java.lang.ref.SoftReference`,当内存不足时会被回收。
-
**弱引用(Weak References)**:`java.lang.ref.WeakReference`,一旦成为弱引用,就会被垃圾回收器回收。
-
**虚引用(Phantom References)**:`java.lang.ref.PhantomReference`,无法通过虚引用访问对象,仅用于跟踪对象被回收的状态。
- **回收时机**:
即使对象已经被视为垃圾,垃圾回收器也不一定会立即回收它们。回收时机取决于多种因素,如GC算法、堆内存的使用情况等。
- **回收过程**:
对象被判定为垃圾后,GC会在合适的时机进行回收。在回收过程中,对象占用的内存会被释放。
开发者可以通过以下方式帮助JVM管理内存:
-
及时释放不再使用的引用,如将对象设置为`null`。
-
使用适当的数据结构和算法,减少内存占用。
-
避免内存泄漏,如避免循环引用或确保及时关闭资源。
记住,Java的垃圾回收是自动的,但开发者可以通过编写良好的代码来辅助JVM更高效地进行内存管理。
垃圾回收算法
标记-清除(Mark-Sweep):
首先标记所有需要回收的对象。
清除所有被标记的对象,释放内存。
缺点是会产生内存碎片,并且标记和清除过程可能较慢。
复制(Copying):
将内存分为两个相等的区域,每次只使用一个区域。
当一个区域满了,将存活的对象复制到另一个区域,并清空当前区域。
优点是解决了内存碎片问题,缺点是空间利用率降低。
标记-整理(Mark-Compact):
先进行标记阶段,找出存活对象。
然后将存活对象向内存的一端移动,并清空剩余区域。
优点是解决了内存碎片问题,缺点是移动对象可能较耗时。
增量回收(Incremental or Generational GC):
将堆分为不同的代(通常是新生代和老年代),并假设大部分对象都是短暂存活的。
新生代使用复制算法,老年代使用标记-清除或标记-整理算法。
增量回收尝试减少单次GC的停顿时间。
分代收集(Generational Collection):
基于增量回收的概念,进一步优化,将对象分配到不同代。
新生代对象存活率低,使用复制算法;老年代对象存活率高,使用标记-清除或标记-整理算法。
并发标记-清除(Concurrent Mark-Sweep):
允许垃圾回收器的某些阶段与应用程序并发运行,减少停顿时间。
G1(Garbage-First):
一种服务器端的垃圾回收算法,旨在提供可预测的停顿时间。
将堆分割成多个区域,并优先回收那些包含大量垃圾的区域。
ZGC(Z Garbage Collector):
一种低延迟垃圾回收器,使用彩色标记和并发处理。
它允许应用程序在非常大的堆上运行,同时保持低延迟。
Shenandoah:
一种与应用程序并发运行的垃圾回收器,几乎没有停顿。
它使用全局并发标记和编译器支持的引用处理。
Epsilon:
一种无操作垃圾回收器,不执行任何垃圾回收,用于性能基准测试。
垃圾回收器
在JDK 8中,主要的垃圾回收器(Garbage Collectors,GC)有以下几种:
-
**Serial GC**:这是单线程的垃圾回收器,使用一个线程进行垃圾回收,适合内存资源受限的环境和小数据量的应用场景。它在新生代使用复制算法,在老年代使用标记-整理算法。
-
**ParNew GC**:ParNew是Serial GC的多线程版本,同样在新生代使用复制算法,适用于多CPU环境。它是许多运行在server模式下的虚拟机中首选的新生代收集器。
-
**Parallel Scavenge GC**:这个收集器是一个关注吞吐量的新生代收集器,使用复制算法。它提供了一些参数,如 `-XX:MaxGCPauseMillis` 用于控制最大垃圾收集停顿时间,以及 `-XX:GCTimeRatio` 用于设置吞吐量大小。
它的目标是达到一个可控的吞吐量(吞吐量=运行用户代码的时间 /(运行用户代码时间+垃圾收集时间)),例子:虚拟机一共运行了100分钟,垃圾收集器用了1分钟,用户代码运行时间99分钟,那么吞吐量就是99%。
-
**Parallel Old GC**:这是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。它适用于注重吞吐量和CPU资源的场合。
-
**CMS (Concurrent Mark Sweep) GC**:CMS是一种以最小化GC停顿时间为目标的收集器,采用标记-清除算法。它的垃圾收集过程分为四个步骤:初始标记、并发标记、重新标记和并发清除。CMS在JDK 9中被标记为过时,并在JDK 14中被移除。
-
**G1 (Garbage-First) GC**:G1是一种服务器端的垃圾回收器,旨在提供可预测的停顿时间,同时保持高吞吐量。它将堆内存分割成多个区域,并优先回收那些包含大量垃圾的区域。G1在JDK 9之后成为默认的垃圾回收器。
-
**ZGC (Z Garbage Collector)**:虽然ZGC是在JDK 11中引入的,但它是一种低延迟的垃圾回收器,能够在保持高吞吐量的同时,将停顿时间控制在毫秒级别。ZGC支持最大16TB的堆内存,适合需要极低延迟和大堆内存的应用场景。
JDK 8的默认垃圾回收器组合是Parallel Scavenge GC用于新生代,Parallel Old GC用于老年代。开发者可以根据应用的具体需求和JVM的性能特性来选择合适的垃圾回收器。
Java 垃圾收集器(Garbage Collector,GC)日志分析工具
Universal JVM GC analyzer - Java Garbage collection log analysis made easy
GC Easy 是一款在线的 Java 垃圾回收日志分析工具,它通过机器学习技术辅助用户快速理解 GC 日志,定位内存泄漏和优化垃圾回收性能。使用 GC Easy 非常简单,用户只需上传 GC 日志文件,GC Easy 就会自动解析日志并生成包含多种图表和详细指标的分析报告,帮助用户直观地了解应用程序的内存使用情况和垃圾回收性能 。
打印GC日志
打印GC(Garbage Collection,垃圾回收)日志是监控和调优Java应用程序性能的重要手段。以下是一些常用的JVM参数,用于控制GC日志的打印:
- **`-Xlog:gc*`**:
这个参数会打印GC日志,`*`可以替换为更具体的日志选项,例如`-Xlog:gc`只打印GC日志,而`-Xlog:gc+heap=info`会打印GC和堆信息。
- **`-XX:+PrintGC`**:
启用这个参数会打印每次GC事件的简要信息。
- **`-XX:+PrintGCDetails`**:
如果需要更详细的GC日志,可以使用此参数,它会打印GC的详细日志,包括GC的类型、花费的时间、回收了多少内存等。
- **`-XX:+PrintGCTimeStamps`**:
这个参数会让GC日志包含时间戳,这对于分析GC事件的发生时间非常有用。
- **`-XX:+PrintGCDateStamps`**:
与`-XX:+PrintGCTimeStamps`类似,但这个参数会打印日期和时间戳。
- **`-Xloggc:<file-path>`**:
通过这个参数可以指定GC日志输出到文件而不是控制台。
- **`-XX:+UseGCLogFileRotation`**:
启用日志文件轮转,防止日志文件无限增长。
- **`-XX:NumberOfGCLogFiles=<files>`**:
设置轮转的日志文件数量。
- **`-XX:GCLogFileSize=<size>`**:
设置每个GC日志文件的最大大小。
- **`-XX:HeapDumpPath=<path>`**:
设置在发生OOM(Out of Memory)时Heap Dump文件的路径。
- **`-XX:+HeapDumpOnOutOfMemoryError`**:
在发生OOM时自动生成Heap Dump。
以下是一个示例,展示如何使用这些参数启动Java应用程序:
```sh
java -Xmx1024m -Xms512m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/path/to/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump.hprof YourApplication.jar
```
在这个示例中,我们设置了堆的最大和初始大小,启用了详细GC日志和时间戳,并将GC日志输出到指定的文件。同时,如果发生OOM错误,会自动在指定路径生成Heap Dump文件。
请注意,这些参数可能会根据JDK的版本和实现有所不同,建议查阅具体版本的官方文档以获取最准确的信息。
演示案例
代码
public class TestGC {
public static void main(String[] args) throws InterruptedException {
final int STRING_COUNT = 100000; // 要生成的字符串数量
List<String> arr = new ArrayList<>();
for (int i = 0; i < STRING_COUNT; i++) {
String replace = UUID.randomUUID().toString().replace("-", "");
System.out.println(replace);
arr.add(replace);
replace = null;
}
arr.clear();
System.gc();
Thread.sleep(5000);
}
}
设置jvm参数
-Xmx128m -Xms128m -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
在Universal JVM GC analyzer - Java Garbage collection log analysis made easy 上传你的gc日志
然后分析
你会发现新生代内存空间占分配的128m栈内存的三分之一左右,老年代占三分之2左右。
通过下面这张图:
新生代内存满了之后,JVM会触发Minor GC(也称为Young GC),在这次垃圾回收过程中,不再有存活价值的对象会被回收,而仍然存活的对象会根据其年龄(age)进行处理。在HotSpot JVM中,对象在新生代中有一个年龄计数器,每次经历一次Minor GC后,如果对象仍然存活,其年龄计数器会增加。当对象的年龄达到一定的阈值(可以通过-XX:MaxTenuringThreshold
参数设置,默认值通常是15),对象就会被晋升到老年代(Old Generation或Tenured Generation)。
晋升到老年代的对象会有更多的生存时间,JVM会认为这些对象的生命周期较长,因此不需要在每次Minor GC时都进行回收。老年代的垃圾回收(Major GC或Full GC)发生的频率远低于新生代,这样可以减少垃圾回收的开销。
总结来说,新生代内存满了会触发Minor GC,在此过程中,满足年龄条件的对象会被晋升到老年代,而未满足条件的存活对象则仍然留在新生代中。这个过程有助于JVM更有效地管理内存,减少垃圾回收的频率和成本。
下图,平均GC执行时间,和最大GC执行时间
下图,发现Full GC 触发了一次,我们调优就是为了尽量避免Full GC的触发,System.gc()很大可能会触发,我们上面代码写了System.gc(),所以Full GC了一次,如果你去掉System.gc()代码,然后再分析日志,你会发现触发次数是0了。下面是FullGC触发了条件
Full GC(Full Garbage Collection)是JVM中的老年代垃圾回收,它是一种成本较高的垃圾回收操作,因为它会回收整个堆内存(包括新生代和老年代)。Full GC的触发条件通常包括以下几种情况:
-
**老年代空间不足**:当老年代中没有足够的内存空间去容纳新生代晋升的对象时,会触发Full GC 。
-
**Metaspace区内存达到阈值**:从JDK8开始,永久代(PermGen)被废弃,取而代之的是Metaspace。当Metaspace区域的内存使用达到一定阈值时,会触发Full GC 。
-
**统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间**:Hotspot为了避免新生代对象晋升到老年代导致空间不足,在进行Minor GC时会做一个判断,如果统计得到的晋升平均大小大于老年代剩余空间,则直接触发Full GC 。
-
**堆中分配很大的对象**:当创建一个很大的对象,且该对象需要的内存空间大于老年代的剩余空间时,会触发Full GC 。
-
**CMS GC时出现promotion failed和concurrent mode failure**:在使用CMS垃圾收集器时,如果出现晋升失败或并发模式失败,也会触发Full GC 。
-
**显式调用System.gc()**:虽然只是建议JVM进行Full GC,但在大多数情况下会增加Full GC的次数,导致系统性能下降 。
在进行Full GC时,JVM会尝试清理整个堆内存中的垃圾对象,这可能会导致应用程序的线程暂停,从而影响性能。因此,理解并监控Full GC的触发条件和频率对于优化Java应用程序的性能至关重要。开发者可以通过监控工具和调整JVM参数来优化垃圾回收过程,例如设置堆的最大大小、新生代与老年代的比例、使用合适的垃圾回收器等 。
JVM调优总结
JVM调优是一个综合性的过程,涉及到多个方面的参数调整,主要包括内存设置、垃圾收集器选择、性能监控等。以下是一些常见的JVM调优参数和策略:
-
**堆内存设置**:使用 `-Xms` 和 `-Xmx` 参数来设置JVM堆的初始大小和最大大小,这有助于优化内存使用并减少动态调整的开销。
-
**垃圾收集器选择**:根据应用的特点和需求,选择合适的垃圾收集器。例如,`-XX:+UseG1GC` 用于选择G1垃圾收集器,它适合于大堆内存和多核处理器的场景,可以提供平衡的吞吐量和较低的延迟。
-
**性能监控**:启用 `-XX:+PrintGCDetails` 参数打印详细的GC日志,这有助于监控垃圾收集的性能和优化垃圾收集策略。
-
**元空间(Metaspace)**:设置 `-XX:MetaspaceSize` 和 `-XX:MaxMetaspaceSize` 参数来控制元空间的大小,避免因元空间无限增长导致的问题。
-
**日志和监控**:使用 `-Xloggc` 将GC日志写入指定文件,并使用 `-XX:+UseGCLogFileRotation` 开启GC日志文件的轮替,以便于监控和分析。
-
**JVM性能调优**:使用 `-XX:+UseStringDeduplication` 开启字符串去重功能,减少堆内存的占用;使用 `-XX:+DisableExplicitGC` 禁用System.gc()的显式调用,避免可能的性能问题。
-
**同步优化**:在多线程应用中,合理使用同步机制,避免因过度同步导致的性能损耗。
-
**JIT编译器优化**:JIT编译器是JVM性能优化的重要手段,通过调整JIT编译器的参数和关闭不必要的优化,提高程序的执行效率。
-
**内存分析工具**:使用内存分析工具,如VisualVM和MAT,帮助定位内存泄漏问题并解决。
-
**监控工具**:使用jstat、jvisualvm、jconsole等JVM监控工具,监控和分析Java应用的性能。
需要注意的是,JVM调优应根据应用程序的具体需求和运行情况进行,逐步调整并观察每次调整的效果。此外,JVM调优是一个持续的过程,需要根据应用的运行情况不断进行优化和调整。