java JVM

JVM的组成

Java虚拟机(JVM)是执行Java字节码的运行时环境。它由以下几个主要部分组成:

  1. **类加载器(ClassLoader)**:
  • 负责加载Java类的字节码到JVM中,并进行链接和初始化。

    关于Java的类加载器,以下是一些关键点,它们对于理解Java类如何被加载、链接和初始化非常重要:

    1. 类加载过程

      • Java的类加载过程包括三个主要步骤:加载(Loading)、链接(Linking)、初始化(Initialization)。
    2. 加载

      • 加载是类加载器读取类文件的二进制数据,并将其转换为方法区中的运行时数据结构的过程。
    3. 链接

      • 链接包括验证(Verification)、准备(Preparation)和解析(Resolution)三个子阶段。验证确保加载的类信息符合JVM规范;准备负责为静态变量分配内存并设置默认初始值;解析将符号引用转换为直接引用。
    4. 初始化

      • 初始化是为静态变量赋予正确的初始值,并执行静态代码块的过程。
    5. 类加载器类型

      • Java提供了多种类型的类加载器:
        • 启动类加载器(Bootstrap ClassLoader):负责加载Java核心库,如rt.jar
          加载lib目录下的类库
        • 扩展类加载器(Extension ClassLoader):负责加载扩展目录中的类库。
          加载ext目录下的类库
        • 系统类加载器(System ClassLoader):负责加载应用程序类路径(-classpath参数)上的类。
          也称为应用类加载器,负责加载用户类路径(-classpath参数或系统属性java.class.path)上指定的类库。
        • 自定义类加载器(User-Defined ClassLoader):应用程序可以自定义类加载器来控制类的加载过程。
    6. 双亲委派模型(Parent Delegation Model)

      • 类加载器使用双亲委派模型来查找类。当一个类请求被加载时,它会首先委托给它的父类加载器去尝试加载这个类,只有当父类加载器无法完成这个请求时,子类加载器才会尝试自己去加载。
    7. 类的唯一性

      • 在同一个Java虚拟机中,任何一个类只有一个实例,即使由不同的类加载器加载也是如此。
    8. 类加载器的层次结构

      • 类加载器形成了一个层次结构,通常从启动类加载器到系统类加载器,再到自定义类加载器。
    9. 安全性

      • 类加载器确保来自不同源的类是隔离的,例如,从网络上加载的类不会影响系统类加载器加载的类。
    10. 资源优化

      • 类加载器允许应用程序在运行时动态地加载和卸载类,这有助于资源管理和优化。
    11. 类卸载

      • 在某些情况下,如类不再被使用,JVM可以卸载这些类以释放内存。
    12. 类加载器的实现

      • Java允许开发者通过继承java.lang.ClassLoader类来实现自定义的类加载器。

    了解类加载器的工作原理对于Java开发者来说非常重要,特别是在需要动态加载类、隔离类版本、扩展应用程序功能或处理不同类路径场景时。

  1. **运行时数据区(Runtime Data Areas)**:
  • 包括以下几个部分:

  • **堆(Heap)**:存储所有Java对象实例和数组。

  • **方法区(Method Area)**:存储已被虚拟机加载的类信息、常量、静态变量等。在Java 8之前,这部分被称为永久代(Permanent Generation,PermGen)。

  • **栈(Stack)**:每个线程都有自己的栈,用于存储局部变量和方法调用信息。

  • **程序计数器(Program Counter)**:每个线程都有一个独立的程序计数器,用于跟踪当前执行的字节码指令。

  • **本地方法栈(Native Method Stack)**:用于存储本地方法(如JNI调用)的调用信息。

  1. **执行引擎(Execution Engine)**:
  • 负责解释执行字节码或通过即时编译器(JIT)将字节码编译为本地机器代码后执行。
  1. **垃圾回收器(Garbage Collector, GC)**:
  • 负责自动回收不再使用的对象,以管理堆内存。
  1. **本地接口(Native Interface)**:
  • 允许Java代码调用和使用本地应用程序编程接口(API),例如JNI(Java Native Interface)。
  1. **即时编译器(Just-In-Time Compiler, JIT)**:
  • 将热点代码(经常执行的代码)编译为机器代码,以提高执行效率。
  1. **内存管理器(Memory Manager)**:
  • 负责管理JVM的内存分配和回收。
  1. **安全管理系统(Security Manager)**:
  • 控制对系统资源的访问,确保代码的安全性。
  1. **线程调度器(Thread Scheduler)**:
  • 管理线程的创建、调度和执行。
  1. **编译系统和解释器(Compiler and Interpreter)**:
  • 解释器负责直接执行字节码,而编译系统负责将字节码编译为本地代码。
  1. **性能分析器(Profiler)**:
  • 用于监控和分析JVM的性能。

这些组件共同工作,为Java应用程序提供了一个稳定、安全且高效的运行环境。JVM的架构设计允许它在不同的操作系统和硬件平台上运行,确保了Java语言的跨平台特性。

JDK8 JVM内存结构

在 JDK 8 中,JVM 的内存结构主要包括以下几个运行时数据区域:

  1. **堆(Heap)**:
  • 堆是JVM中最大的一块内存区域,主要用于存储对象实例和数组。

  • 堆是垃圾回收器管理的主要区域,经常发生垃圾回收操作。

    JVM堆是Java虚拟机中用于存储对象实例和数组的主要内存区域。以下是JVM堆中存储的一些主要内容:

    1. 对象实例

      • 所有通过Java关键字new创建的对象实例都存储在堆中。
    2. 数组

      • 所有的数组,无论其元素类型如何(原始类型或引用类型),都存储在堆中。
    3. 实例变量

      • 对象的非静态成员变量也存储在堆中,与对象实例一起。
    4. 匿名内部类

      • 匿名内部类(即使没有具体名称的内部类)的实例同样存储在堆中。
    5. 反射对象

      • 通过Java反射API创建的类对象也存储在堆中。
    6. 字符串常量

      • 尽管字符串常量可能会存储在字符串常量池中,但通过new操作符创建的字符串对象实例仍然存储在堆中。
    7. 封装类对象

      • 封装类(如IntegerDouble等)的实例存储在堆中。
    8. 枚举实例

      • 枚举类型(enum)的实例也存储在堆中。
    9. 异常对象

      • 当抛出异常时,异常对象的实例会存储在堆中,直到异常被处理。
    10. 软引用、弱引用、虚引用

      • 这些引用类型指向的对象也存储在堆中,尽管它们可能在垃圾回收时被回收。
    11. 动态代理对象

      • 使用Java动态代理API创建的代理对象实例存储在堆中。
    12. Java本地方法接口(JNI)对象

      • 通过JNI创建的对象实例同样存储在堆中。

    JVM堆是垃圾回收器管理的主要区域,因为大多数对象都是短暂的,并在此处进行分配和回收。堆内存的大小可以通过JVM启动参数(如-Xms-Xmx)进行配置。了解堆中存储的内容有助于开发者进行内存管理和性能优化。

    JVM堆中的内存被划分为不同的代,主要是为了更有效地进行垃圾回收。主要分为以下两个部分:

    1. 新生代(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
    2. 老年代(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应用程序的内存使用和垃圾回收性能至关重要。

  1. **方法区(Method Area)**:
  • 也称为永久代(PermGen),但在JDK 8中已经被元空间(Metaspace)所取代。

  • 方法区用于存储类信息、常量、静态变量、方法字节码等数据。

  1. **栈(Stack)**:
  • 每个线程都有自己的虚拟机栈,用于存储局部变量、方法参数、方法调用和返回值。方法开始的时候会进栈,方法执行完成会出栈,相当于清空了数据,所以不用进行GC操作。

  • 栈帧(Stack Frame)是栈的基本单位,每个方法调用都会创建一个新的栈帧。

  1. **程序计数器(Program Counter)**:
  • 每个线程都有一个独立的程序计数器,用于记录当前线程执行的字节码指令地址。
  1. **本地方法栈(Native Method Stack)**:
  • 用于存储本地方法(如JNI调用)的调用信息。
  1. **元空间(Metaspace)**:
  • JDK 8中引入,用于替代JDK 7及以前版本的永久代。

  • 元空间不位于虚拟机内存中,而是使用本地内存,用于存储类的元数据信息。

  1. **代码缓存(Code Cache)**:
  • 用于存储JIT编译器编译后的本地机器代码,以提高性能。
  1. **运行时常量池(Runtime Constant Pool)**:
  • 属于方法区的一部分,用于存储类中的常量,如字符串字面量、数字常量等。
  1. **直接内存(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)回收,主要基于以下条件:

  1. **无法到达性(Unreachability)**:

对象没有任何引用与之相连,即从GC Roots开始无法到达该对象。

  1. **GC Roots的起点**:

GC Roots是垃圾收集器进行可达性分析时的起始点,包括:

  • 静态字段(`static`字段)中的对象引用。

  • 局部变量表(栈帧中的局部变量)中的对象引用。

  • 活跃线程的引用,包括Java方法栈和本地方法栈。

  • 同步锁(`synchronized`关键字所创建的锁)的对象。

  • 被Java虚拟机引用的对象,如系统类加载器。

  1. **可达性分析(Reachability Analysis)**:

垃圾回收器会定期进行可达性分析,从GC Roots开始遍历所有可达对象。所有不可达的对象被认为是垃圾。

  1. **finalize()方法**:

如果对象没有被引用,并且没有被垃圾回收器回收,它可能会成为垃圾回收的候选对象。如果对象定义了`finalize()`方法,并且该方法还未被调用,垃圾回收器可能会调用这个方法。但自Java 9起,`finalize()`方法已被标记为过时,并推荐使用`Cleaner`类来实现类似功能。

  1. **引用类型**:
  • **强引用(Strong References)**:普通的引用,如`Object obj = new Object();`。

  • **软引用(Soft References)**:`java.lang.ref.SoftReference`,当内存不足时会被回收。

  • **弱引用(Weak References)**:`java.lang.ref.WeakReference`,一旦成为弱引用,就会被垃圾回收器回收。

  • **虚引用(Phantom References)**:`java.lang.ref.PhantomReference`,无法通过虚引用访问对象,仅用于跟踪对象被回收的状态。

  1. **回收时机**:

即使对象已经被视为垃圾,垃圾回收器也不一定会立即回收它们。回收时机取决于多种因素,如GC算法、堆内存的使用情况等。

  1. **回收过程**:

对象被判定为垃圾后,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)有以下几种:

  1. **Serial GC**:这是单线程的垃圾回收器,使用一个线程进行垃圾回收,适合内存资源受限的环境和小数据量的应用场景。它在新生代使用复制算法,在老年代使用标记-整理算法。

  2. **ParNew GC**:ParNew是Serial GC的多线程版本,同样在新生代使用复制算法,适用于多CPU环境。它是许多运行在server模式下的虚拟机中首选的新生代收集器。

  3. **Parallel Scavenge GC**:这个收集器是一个关注吞吐量的新生代收集器,使用复制算法。它提供了一些参数,如 `-XX:MaxGCPauseMillis` 用于控制最大垃圾收集停顿时间,以及 `-XX:GCTimeRatio` 用于设置吞吐量大小。

它的目标是达到一个可控的吞吐量(吞吐量=运行用户代码的时间 /(运行用户代码时间+垃圾收集时间)),例子:虚拟机一共运行了100分钟,垃圾收集器用了1分钟,用户代码运行时间99分钟,那么吞吐量就是99%。

  1. **Parallel Old GC**:这是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。它适用于注重吞吐量和CPU资源的场合。

  2. **CMS (Concurrent Mark Sweep) GC**:CMS是一种以最小化GC停顿时间为目标的收集器,采用标记-清除算法。它的垃圾收集过程分为四个步骤:初始标记、并发标记、重新标记和并发清除。CMS在JDK 9中被标记为过时,并在JDK 14中被移除。

  3. **G1 (Garbage-First) GC**:G1是一种服务器端的垃圾回收器,旨在提供可预测的停顿时间,同时保持高吞吐量。它将堆内存分割成多个区域,并优先回收那些包含大量垃圾的区域。G1在JDK 9之后成为默认的垃圾回收器。

  4. **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日志的打印:

  1. **`-Xlog:gc*`**:

这个参数会打印GC日志,`*`可以替换为更具体的日志选项,例如`-Xlog:gc`只打印GC日志,而`-Xlog:gc+heap=info`会打印GC和堆信息。

  1. **`-XX:+PrintGC`**:

启用这个参数会打印每次GC事件的简要信息。

  1. **`-XX:+PrintGCDetails`**:

如果需要更详细的GC日志,可以使用此参数,它会打印GC的详细日志,包括GC的类型、花费的时间、回收了多少内存等。

  1. **`-XX:+PrintGCTimeStamps`**:

这个参数会让GC日志包含时间戳,这对于分析GC事件的发生时间非常有用。

  1. **`-XX:+PrintGCDateStamps`**:

与`-XX:+PrintGCTimeStamps`类似,但这个参数会打印日期和时间戳。

  1. **`-Xloggc:<file-path>`**:

通过这个参数可以指定GC日志输出到文件而不是控制台。

  1. **`-XX:+UseGCLogFileRotation`**:

启用日志文件轮转,防止日志文件无限增长。

  1. **`-XX:NumberOfGCLogFiles=<files>`**:

设置轮转的日志文件数量。

  1. **`-XX:GCLogFileSize=<size>`**:

设置每个GC日志文件的最大大小。

  1. **`-XX:HeapDumpPath=<path>`**:

设置在发生OOM(Out of Memory)时Heap Dump文件的路径。

  1. **`-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的触发条件通常包括以下几种情况:

  1. **老年代空间不足**:当老年代中没有足够的内存空间去容纳新生代晋升的对象时,会触发Full GC 。

  2. **Metaspace区内存达到阈值**:从JDK8开始,永久代(PermGen)被废弃,取而代之的是Metaspace。当Metaspace区域的内存使用达到一定阈值时,会触发Full GC 。

  3. **统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间**:Hotspot为了避免新生代对象晋升到老年代导致空间不足,在进行Minor GC时会做一个判断,如果统计得到的晋升平均大小大于老年代剩余空间,则直接触发Full GC 。

  4. **堆中分配很大的对象**:当创建一个很大的对象,且该对象需要的内存空间大于老年代的剩余空间时,会触发Full GC 。

  5. **CMS GC时出现promotion failed和concurrent mode failure**:在使用CMS垃圾收集器时,如果出现晋升失败或并发模式失败,也会触发Full GC 。

  6. **显式调用System.gc()**:虽然只是建议JVM进行Full GC,但在大多数情况下会增加Full GC的次数,导致系统性能下降 。

在进行Full GC时,JVM会尝试清理整个堆内存中的垃圾对象,这可能会导致应用程序的线程暂停,从而影响性能。因此,理解并监控Full GC的触发条件和频率对于优化Java应用程序的性能至关重要。开发者可以通过监控工具和调整JVM参数来优化垃圾回收过程,例如设置堆的最大大小、新生代与老年代的比例、使用合适的垃圾回收器等 。

JVM调优总结

JVM调优是一个综合性的过程,涉及到多个方面的参数调整,主要包括内存设置、垃圾收集器选择、性能监控等。以下是一些常见的JVM调优参数和策略:

  1. **堆内存设置**:使用 `-Xms` 和 `-Xmx` 参数来设置JVM堆的初始大小和最大大小,这有助于优化内存使用并减少动态调整的开销。

  2. **垃圾收集器选择**:根据应用的特点和需求,选择合适的垃圾收集器。例如,`-XX:+UseG1GC` 用于选择G1垃圾收集器,它适合于大堆内存和多核处理器的场景,可以提供平衡的吞吐量和较低的延迟。

  3. **性能监控**:启用 `-XX:+PrintGCDetails` 参数打印详细的GC日志,这有助于监控垃圾收集的性能和优化垃圾收集策略。

  4. **元空间(Metaspace)**:设置 `-XX:MetaspaceSize` 和 `-XX:MaxMetaspaceSize` 参数来控制元空间的大小,避免因元空间无限增长导致的问题。

  5. **日志和监控**:使用 `-Xloggc` 将GC日志写入指定文件,并使用 `-XX:+UseGCLogFileRotation` 开启GC日志文件的轮替,以便于监控和分析。

  6. **JVM性能调优**:使用 `-XX:+UseStringDeduplication` 开启字符串去重功能,减少堆内存的占用;使用 `-XX:+DisableExplicitGC` 禁用System.gc()的显式调用,避免可能的性能问题。

  7. **同步优化**:在多线程应用中,合理使用同步机制,避免因过度同步导致的性能损耗。

  8. **JIT编译器优化**:JIT编译器是JVM性能优化的重要手段,通过调整JIT编译器的参数和关闭不必要的优化,提高程序的执行效率。

  9. **内存分析工具**:使用内存分析工具,如VisualVM和MAT,帮助定位内存泄漏问题并解决。

  10. **监控工具**:使用jstat、jvisualvm、jconsole等JVM监控工具,监控和分析Java应用的性能。

需要注意的是,JVM调优应根据应用程序的具体需求和运行情况进行,逐步调整并观察每次调整的效果。此外,JVM调优是一个持续的过程,需要根据应用的运行情况不断进行优化和调整。

相关推荐
古月居GYH11 分钟前
在C++上实现反射用法
java·开发语言·c++
请你打开电视看看1 小时前
Jvm知识点
jvm
儿时可乖了1 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol1 小时前
java基础概念37:正则表达式2-爬虫
java
xmh-sxh-13141 小时前
jdk各个版本介绍
java
天天扭码2 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶2 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺2 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端