JVM(Java 虚拟机)

news2025/2/11 18:55:39

Java语言的解释性和编译性(通过JVM 的执行引擎)

 Java 代码(.java 文件)要先使用 javac 编译器编译为 .class 文件(字节码),紧接着再通过JVM 的执行引擎(Execution Engine) 负责处理 Java 字节码并执行,它的主要组成部分包括:

  • 解释器(Interpreter):逐行解释字节码执行,启动快但执行速度较慢。
  • JIT 编译器(Just-In-Time Compiler):将热点字节码编译为本地机器码,提高执行效率。
  • 垃圾回收器(Garbage Collector, GC):管理 Java 堆中的对象回收,提升内存管理效率。

(1)Java 代码先编译成字节码

  • Java 代码(.java 文件)使用 javac 编译器编译为 .class 文件(字节码)。
  • 这种字节码与平台无关,可以在不同的 JVM 上运行。

(2)JVM 先解释执行,再逐步编译为机器码

  • 当 JVM 启动 Java 应用程序时,解释器(Interpreter)逐条解析字节码并执行,优点是启动速度快,但缺点是运行速度慢。
  • JVM 会分析热点代码(执行次数较多的代码),并使用 JIT(即时编译器) 将热点代码直接编译成机器码,提高性能。

(3)JIT 编译器优化热点代码

  • JIT 编译器(如 C1、C2 编译器)会在代码执行时,将热点字节码转换为本地机器码,提升执行速度。
  • 解释执行 + JIT 编译 的混合模式确保 Java 既有较快的启动速度,又能提升长时间运行的性能

总结:
解释器负责启动时快速执行,JIT 编译器负责优化热点代码。 这就是 Java 既有解释语言的灵活性,又有编译语言的高效性的原因。

1. JVM 运行时数据区(Runtime Data Areas)

1.1. 程序计数器(Program Counter Register)

概述

程序计数器(PC Register)是 JVM 中一个小型的内存区域,它的作用是记录当前线程正在执行的字节码指令地址。

特点

  • 线程私有(每个线程都有一个独立的 PC 寄存器)。
  • 存储字节码指令地址,用于指示当前线程下一条要执行的指令。
  • 执行 Java 方法时,PC 计数器存储正在执行的字节码指令地址。
  • 执行本地方法(Native Method)时,PC 计数器值 undefined(未定义)。
  • 该区域是 JVM 唯一不会发生 OutOfMemoryError 的区域

作用

  • 记录当前线程的执行位置(类似于 CPU 的 PC 寄存器)。
  • 线程切换时,保证线程恢复后能继续执行正确的指令。
  • 实现 Java 代码的多线程并发(JVM 通过线程轮流切换,每个线程都有自己的 PC 寄存器)。

1.2. Java 虚拟机栈(JVM Stack)

概述

Java 虚拟机栈(JVM Stack)用于存储 Java 方法执行时的栈帧(Stack Frame),是 Java 方法执行的基础。

特点

  • 线程私有,每个线程有独立的栈。
  • 栈的大小可以通过 -Xss 参数设置(例如 -Xss1M 设置栈大小为 1MB)。
  • 栈的生命周期与线程相同,线程结束时栈也随之销毁。

栈帧(Stack Frame)的组成

每个栈帧对应一个正在执行的方法,包含:

  • 局部变量表(Local Variable Table)

    • 存放方法中的基本数据类型(int、long、float、double 等)对象引用(reference)
    • 局部变量表的大小在编译期确定,运行时不会改变。
    • longdouble 类型占 两个 存储单元,其它数据类型占 一个 存储单元。
  • 操作数栈(Operand Stack)

    • 作为字节码指令的操作数临时存储区,用于方法调用时传递参数、计算临时结果等。
    • JVM 指令基于栈操作(如 iadd 指令会从操作数栈中弹出两个整数相加后再压入栈中)。
  • 动态链接(Dynamic Linking)

    • 指向方法区中的运行时常量池,用于解析方法引用(即方法调用时如何找到方法地址)。
  • 方法返回地址(Return Address)

    • 记录方法执行完毕后,返回到调用者的位置,以便继续执行。

栈的空间大小

  • 栈的大小由 -Xss 参数控制,通常 默认 1MB,可根据需求调整:
    java -Xss512k MyApplication
    
    • 每个线程的栈大小 512KB
    • 栈过大可能导致 StackOverflowError(递归调用过深)。

为什么栈是从高地址向低地址增长?

  • LIFO 机制(后进先出)

    • 栈用于存储方法调用信息,每次调用新方法时,会创建一个栈帧(Stack Frame),压入栈顶
    • 方法执行完后,栈帧被弹出,栈顶回到上一个方法的栈帧。
  • CPU 设计 & 指针运算优化

    • 许多计算机体系结构(如 x86、ARM)都使用“向下增长”的栈
    • 这允许 ESP(栈指针寄存器)直接递减来分配新的栈帧,提高性能。

可能出现的异常

  • StackOverflowError:递归过深导致栈空间耗尽。
  • OutOfMemoryError:JVM 栈的大小动态扩展失败(一般在线程数量过多时)。

1.3. 本地方法栈(Native Method Stack)

概述

本地方法栈(Native Method Stack)与 JVM 栈类似,但它用于存储 Native 方法的执行信息

特点

  • 线程私有,生命周期与线程相同。
  • 主要用于 JNI(Java Native Interface)调用 C/C++ 代码。
  • 可能出现:
    • StackOverflowError
    • OutOfMemoryError

作用

  • 维护 Native 方法调用时的参数、返回地址、Native 方法执行环境等。
  • 例如,调用 System.arraycopy() 方法时,JVM 需要通过本地方法栈进入 C 代码执行内存拷贝。

1.4. 堆(Heap)

概述

堆(Heap)是 JVM 内存中最大的区域,用于存储所有对象实例

特点

  • 线程共享(所有线程都能访问)。
  • GC(垃圾回收器)管理的区域。
  • 堆的大小可通过 -Xms(初始大小)和 -Xmx(最大大小)参数控制。
  • 可能抛出 OutOfMemoryError: Java heap space

堆的分代

  • 新生代(Young Generation)
    • Eden 区(对象最初分配在这里)。
    • Survivor 0(S0)、Survivor 1(S1)(少量存活对象在两者之间交替存储)。
  • 老年代(Old Generation)
    • 长期存活对象存放这里,GC 频率较低。

垃圾回收

  • Minor GC(新生代 GC):采用复制算法(对象生命周期短,适合快速回收)清理 Eden 和 Survivor 区。
  • Major GC / Full GC:清理整个堆,采用标记-整理算法(对象生命周期长)清理老年代。
  •  Major GC主要针对老年代进行清理,Full GC对整个堆内存(包括年轻代、老年代以及元空间)进行回收。

堆的空间大小

  • 堆的大小可以通过 JVM 参数设置

    • -Xms堆的初始大小(默认通常是 1/64 物理内存)
    • -Xmx堆的最大大小(默认通常是 1/4 物理内存)
    • 例如:
      java -Xms256m -Xmx1024m MyApplication
      
      • 最小堆内存 256MB
      • 最大堆内存 1024MB
  • 默认情况下,堆的大小会随着 GC 调整,但不能超过 -Xmx 设定的上限。

为什么堆是从低地址向高地址增长?

  • 堆是动态分配的,大小不固定

    • JVM 通过 -Xms(最小堆)和 -Xmx(最大堆)设置堆的大小。
    • 由于对象的创建是动态的,JVM 需要扩展堆的大小,通常向高地址扩展。
  • 操作系统内存管理

    • 在 C 语言的 malloc()JVMnew 语义中,分配的堆空间通常从低地址向高地址增长
    • 堆的增长方向使得堆的可扩展性更好,能动态调整大小。

1.5. 方法区(Method Area,又称元空间 Metaspace)

概述

存储类的元数据(方法信息、静态变量、运行时常量池)。关于类的信息存储在这里。

JDK 7 及以前

  • 方法区是堆中的永久代(PermGen)
  • -XX:PermSize-XX:MaxPermSize 限制。

JDK 8 及以后

  • 方法区改为单独的元空间(Metaspace)
  • 使用本地内存,不再受堆大小限制。
  • -XX:MetaspaceSize 控制其大小。

1.6 运行时常量池(在方法区中)字符串常量池(在堆中) 

常量池类型存储位置(JDK 6 及以前)存储位置(JDK 7+)存储内容作用
类文件常量池(Class File Constant Pool).class 文件.class 文件字面量(数值、字符串)、符号引用编译时生成,在运行时加载到运行时常量池
运行时常量池(Runtime Constant Pool)方法区(永久代 PermGen)方法区(元空间 Metaspace)从类文件加载的常量(字面量、符号引用)、运行时生成的常量(String.intern()动态解析符号引用、存储运行时常量
字符串常量池(String Pool)方法区(永久代 PermGen)堆(Heap)String 字面量、intern() 方法存入的字符串优化字符串存储,减少内存占用
常量类型说明
字面量编译时生成的常量,如 final 修饰的常量、字符串字面量、数值(int、float、double、long)等。
符号引用类名、字段名、方法名的符号引用(未解析为具体地址),用于支持动态链接。
方法引用方法的符号引用,如方法的名称、描述符等。

1.7 总结

内存区域线程私有/共享主要作用可能抛出的异常
程序计数器线程私有记录当前线程执行的字节码地址
JVM 栈线程私有存储局部变量表、操作数栈StackOverflowErrorOutOfMemoryError
本地方法栈线程私有执行 Native 方法StackOverflowErrorOutOfMemoryError
线程共享存储对象实例OutOfMemoryError: Java heap space
方法区(元空间)线程共享存储类信息、静态变量OutOfMemoryError: Metaspace
直接内存线程共享用于高效 I/O(如 NIO)OutOfMemoryError: Direct Buffer Memory

2. 类加载机制

2.1 类加载器

(1)启动类加载器(Bootstrap ClassLoader)

  • 负责加载JDK 核心类库rt.jarcharsets.jar 等)。
  • C++ 代码实现,无法直接获取其实例(即 null)。
  • 只能加载JDK 自带的核心类库,无法加载用户定义的 .class 文件。

主要加载的类包括:

  • java.lang.*(如 StringIntegerSystem
  • java.util.*(如 ArrayListHashMap
  • java.io.*(如 FileInputStream
  • java.nio.*java.net.*

(2)扩展类加载器(ExtClassLoader)

  • 负责加载 lib/ext/ 目录下的扩展类库(如 javax.crypto.*)。
  • Java 代码实现,可通过 ClassLoader.getSystemClassLoader().getParent() 获取。
  • JDK 9 以后,扩展类加载器被移除,改为平台类加载器(PlatformClassLoader)

主要加载的类包括:

  • javax.crypto.*(加密库)
  • javax.sound.*(声音处理库)
  • javax.imageio.*(图像处理库)

(3)应用类加载器(AppClassLoader/ SystemClassLoader)

  • 默认的类加载器:如果你没有手动指定类加载器,默认由它加载。
  • 可以通过 ClassLoader.getSystemClassLoader() 获取到它
  • 支持动态加载 JAR 包:当你添加 JAR 依赖(如 Spring Boot 依赖的 JAR),它会动态加载这些类。

 主要加载的类包括:

  • com.example.MyClass
  • org.springframework.*
  • 任何放在 classpath 下的 .class 文件

(4)自定义类加载器 

  • 默认类加载器仅加载 classpath 下的类,如果需要从网络、数据库、加密文件中加载 .class 文件,必须使用自定义类加载器。 
  • 默认的 AppClassLoader 共享 classpath,如果多个模块的类存在相同的包名,可能会发生类冲突
  • 防止 Java 反编译:Java .class 文件容易被反编译,我们可以加密 .class 文件,并使用自定义类加载器在运行时解密。

如何自定义类加载器?

  • 方式 1:继承 ClassLoader。Java 提供了 ClassLoader 抽象类,允许我们创建自己的类加载器。

  • 方式 2:继承 URLClassLoader。如果 .class 文件存放在 JAR 或远程服务器上,我们可以继承 URLClassLoader 来动态加载类。

2.2 双亲委派机制(Parent Delegation Model)

1. 什么是双亲委派机制?

双亲委派机制 是指 类加载器在加载一个类时,先委托其父类加载器加载,只有当父类加载器无法加载该类时,子类加载器才会尝试自己加载

2. 双亲委派机制的工作流程

  1. 当一个 ClassLoader 需要加载一个类时,它不会自己直接加载,而是先委托给父类加载器
  2. 父类加载器递归向上委托,最终到 Bootstrap ClassLoader(顶层)。
  3. 如果 Bootstrap ClassLoader 无法加载该类(即不是核心类库),那么父类加载器会逐层返回,直到应用类加载器(AppClassLoader)。
  4. 如果所有的父类加载器都无法加载该类,那么当前类加载器才会自己尝试加载

3. 为什么要使用双亲委派机制?

保证 Java 运行时的安全性

  • 避免核心 API 被篡改java.lang.String 始终由 Bootstrap ClassLoader 加载)。
  • 防止类的重复加载类冲突

提高类加载的效率

  • 先尝试加载已经加载过的类,避免重复加载。

2.3 类加载过程

类加载的五个阶段

阶段说明
加载(Loading)读取 .class 文件,将字节码转换为 Class 对象。
验证(Verification)检查字节码是否合法,防止恶意代码执行。
准备(Preparation)分配静态变量的内存,初始化默认值(不赋具体值)。
解析(Resolution)将符号引用转换为直接引用(方法地址、变量地址)。
初始化(Initialization)执行 static 代码块,赋值静态变量。

2.3.1. 加载(Loading)

步骤:

  • 读取 .class 文件(从硬盘、网络、JAR 包等加载类的字节码)。
  • 转换字节码为 JVM 识别的 Class 对象,存入方法区。
  • 在堆(Heap)中创建 Class 对象,表示该类的运行时信息。

示例

Class<?> clazz = Class.forName("java.lang.String");
  • Class.forName() 方法会触发类加载

2.3.2. 连接(Linking)

(1)验证(Verification)

  • 检查 .class 文件格式是否正确,防止恶意代码(字节码验证)。
  • 例如,检查字节码指令是否合法,是否会破坏 JVM 运行。

(2)准备(Preparation)

  • 为类的静态变量 分配内存,并设置默认值(如 int 变量默认值为 0)。
  • 例如:
public static int a = 10;  // 在准备阶段 a=0,在初始化阶段才变成10

(3)解析(Resolution)

  • 符号引用(如 java.lang.String)转换为直接引用(JVM 内存地址)。

2.3.3. 初始化(Initialization)

  • 执行 <clinit>() 静态代码块,初始化静态变量(准备阶段是为静态变量赋默认值,而这里是要设置你所定义的值)。
  • 只有第一次使用该类时才执行初始化,确保类只初始化一次。

示例

class Example {
    static {
        System.out.println("Static block executed");
    }

    public static int value = 10;
}

public class Test {
    public static void main(String[] args) {
        System.out.println(Example.value);
    }
}

3. Java 对象的创建过程

当 Java 代码执行 new 关键字创建对象时,JVM 需要完成以下步骤:

步骤 1:检查类是否已被加载

  • JVM 先检查目标类的元信息是否已加载到方法区(元空间 Metaspace)
    • 如果 类未加载,JVM 先通过 类加载器(ClassLoader) 加载 .class 文件,并完成 类加载、验证、准备、解析、初始化 过程(即 类的五个生命周期阶段)。
    • 如果 类已加载,跳过此步骤。

步骤 2:为对象分配内存

JVM 在堆(Heap)中为新对象分配内存,分配策略取决于 内存是否连续

  • 指针碰撞(Bump the Pointer)(内存连续时):

    • 堆内存按顺序分配,JVM 仅需将指针移动到新的可用地址
    • 适用于 使用 GC 压缩后 的堆。
  • 空闲列表(Free List)(内存不连续时):

    • 维护空闲内存块列表,找到合适的内存块进行分配。
    • 适用于 堆内存碎片较多 的情况。
  • 线程私有分配缓冲区(TLAB, Thread Local Allocation Buffer)

    • JVM 允许每个线程在堆中新建私有缓存区域,提高对象分配效率,减少同步锁竞争。

步骤 3:初始化对象的默认值

  • JVM 将对象字段初始化为默认值(不调用构造方法)。
class Example {
    int x;    // 默认值 0
    boolean y; // 默认值 false
    String s; // 默认值 null
}

这里需要注意,类加载过程中也会有对变量赋默认值的操作,但二者是不同的,类加载过程中的是为类的静态变量赋默认值,而这里是对对象的属性进行赋默认值。 

步骤 4:设置对象的元数据

  • Mark Word(标记字段)

    • 存储 哈希码、GC 状态、锁信息
    • 在对象加锁、GC 过程中会被修改。
  • Class Pointer(类指针)

    • 指向对象所属的类元信息(方法区中的 Class 对象)。
    • 通过此指针可以找到对象的类型信息

步骤 5:执行构造方法

JVM 调用 构造方法 <init>(),执行初始化逻辑:

class Example {
    int num;
    Example() {
        num = 10;
        System.out.println("Constructor executed!");
    }
}

public class Test {
    public static void main(String[] args) {
        Example obj = new Example();
    }
}
  • JVM 调用构造方法num = 10

这里需要注意,类加载过程中的赋值操作与这里不同,类加载过程中只是单纯为类的静态变量赋值,而这里是调用构造函数对对象的属性进行赋值。


4. Java 对象的内存分配

4.1 对象的内存结构

Java 采用 堆 + 栈 的模式进行对象的内存管理。

存储位置存储内容
堆(Heap)对象本身(实例变量、数组)
栈(Stack)对象引用(局部变量表)
方法区(Method Area)类的元信息(方法、静态变量)
方法区:
A 类的静态变量 b = 20
A 类的方法信息

栈:
obj1 -> 指向堆中的 A 对象
obj2 -> 指向另一个 A 对象

堆:
obj1 的实例变量 a = 10
obj2 的实例变量 a = 10

4.2 具体对象内存分配示例 

class Person {
    static String species = "Human";  // 静态变量(方法区)
    String name;   // 实例变量(堆)
    int age;       // 实例变量(堆)

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void sayHello() {
        System.out.println("Hello, my name is " + name);
    }
}

public class MemoryDemo {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 25);  // 在堆中创建对象
        Person p2 = new Person("Bob", 30);    // 在堆中创建另一个对象

        p1.sayHello();
        p2.sayHello();
    }
}

 直观的内存示意图

方法区(存储类信息 + 静态变量)
-------------------------------------------------
| 类名:Person                                   
| 静态变量:species = "Human"                   
| 方法:sayHello()                              
-------------------------------------------------

栈(存储局部变量/对象引用)
---------------------------------
| main() 方法的栈帧                          
| p1 -> 指向 堆中的对象 1                     
| p2 -> 指向 堆中的对象 2                     
---------------------------------

堆(存储对象实例)
-------------------------------------
| Person 对象 1 (p1)                     
| name = "Alice"                          
| age = 25                                
-------------------------------------
| Person 对象 2 (p2)                     
| name = "Bob"                            
| age = 30                                
-------------------------------------

4.3 对象的内存分配策略

JVM 根据对象的生命周期和大小,决定其分配的位置:

  • 新生代(Young Generation)

    • 大部分对象先在 Eden 区分配,如果对象存活过 GC,则进入 Survivor 区。
    • 新生代 GC(Minor GC)频繁执行,但速度快。
  • 老年代(Old Generation)

    • 生命周期长的对象 会晋升到老年代(如缓存对象)。
    • 大对象无法放入新生代,直接进入老年代。
  • 栈上分配(逃逸分析)

    • JVM 可能优化对象分配,将短生命周期对象存放在栈上,减少 GC 压力。
    • 需要开启 -XX:+DoEscapeAnalysis

5. JVM 垃圾回收机制(GC) 

5.1 为什么需要垃圾回收?

Java 采用自动内存管理

  • 在 C 语言中,开发者需要手动申请和释放内存(malloc() / free()),容易导致 内存泄漏(Memory Leak)悬空指针(Dangling Pointer)
  • 在 Java 中JVM 通过 GC 自动回收不再使用的对象,避免手动管理内存的复杂性。

解决对象生命周期管理问题

  • Java 采用 堆(Heap)存储对象,但对象的生命周期不同:
    • 短生命周期对象(局部变量、循环创建的对象)
    • 长生命周期对象(缓存、全局对象)
    • 永久对象(static 变量)
  • GC 需要智能回收短生命周期对象,并优化长期存活对象的管理

5.2 判断对象是否需要垃圾回收的两种方法

5.2.1. 引用计数法(已淘汰)

  • 原理:每个对象有一个引用计数器引用 +1,解除引用 -1,当引用计数变为 0,说明对象可被回收。
  • 缺陷:由于无法处理循环引用(两个对象相互引用,但不再使用)的问题,现已淘汰。

5.2.2 可达性分析法

基本原理

可达性分析法是基于图遍历(Graph Traversal)的方式进行垃圾对象检测:

  1. GC Roots(垃圾回收根对象) 作为起点(根节点)。
  2. 从 GC Roots 开始遍历所有可以访问到的对象,标记为存活。
  3. 未被遍历到的对象 被认为是不可达(Garbage),可以被回收。

什么是 GC Roots(垃圾回收根对象) / GC Roots 的来源

在可达性分析中,JVM 会选择一组特殊的对象作为根对象(GC Roots),从这些根开始查找所有可达对象

GC Roots 类型存储位置示例
栈帧中的局部变量栈(Stack)方法内的局部变量 Object obj = new Object();
静态变量(Static)方法区(Metaspace)static Object obj = new Object();
常量池中的引用方法区(Metaspace)String s = "Hello";
JNI(Native 方法)引用的对象本地方法栈(Native Stack)通过 JNI 访问的 Java 对象
线程对象线程管理区运行中的线程对象 Thread.currentThread()

5.3 垃圾回收算法

JVM 采用不同的 GC 算法来优化垃圾回收,主要包括:

GC 算法原理优缺点
标记-清除(Mark-Sweep)标记存活对象 → 清除未标记对象产生内存碎片,影响分配效率
复制(Copying)复制存活对象到新区域,清空旧区域内存利用率低(50% 内存浪费)
标记-整理(Mark-Compact)标记存活对象 → 移动对象(向一端移动) → 回收未使用空间解决碎片问题,性能较高
分代回收(Generational GC)新生代 采用复制算法老年代 采用标记整理算法适用于大规模应用
  • 新生代(Young Generation)

    • 采用复制算法(对象生命周期短,适合快速回收)。
    • 包括 Eden、Survivor 0、Survivor 1
    • Minor GC 发生在新生代,速度快。
  • 老年代(Old Generation)

    • 采用标记-整理算法(对象生命周期长)。
    • Major GC / Full GC 发生在老年代,通常比 Minor GC 慢。

5.4 常见垃圾回收器

GC新生代算法老年代算法适用场景
Serial GC复制标记整理单线程,适用于小型应用
Parallel GC复制标记整理多线程高吞吐量
CMS GC标记清除标记清除低延迟,适用于 Web 应用
G1 GCRegion 化管理Region 化管理大内存应用,JDK 9+ 推荐
ZGC并发并发超低延迟,JDK 11+

STW(Stop-The-World)的概念

STW(Stop-The-World) 是指 JVM 在执行 GC 时,会暂停所有应用线程,以便垃圾回收器安全地回收对象。这意味着:

  • 所有应用线程停止执行,等待 GC 完成后再继续运行。
  • STW 发生时,Java 代码暂停执行,系统响应变慢,可能导致卡顿。

5.4.1 CMS GC(Concurrent Mark-Sweep)

CMS(Concurrent Mark-Sweep)是 JDK 1.4 引入的 低延迟 GC,适用于Web 服务器、金融系统等低停顿时间应用

(1)CMS GC 的核心特点

最小化 STW(低延迟),适用于交互式应用。
并发执行 GC,不影响应用线程运行。
"标记-清除" 算法,回收时不会整理堆内存(容易产生内存碎片)。
垃圾碎片问题严重,可能导致 Full GC(STW 变长)。
CPU 资源开销大,GC 线程与应用线程竞争 CPU 资源。


(2)CMS GC 的工作原理

1️⃣ CMS GC 的堆内存结构

  • 采用 "分代回收"(Generational GC)策略
    • 新生代(Young Generation):使用 "复制" 算法进行垃圾回收(Minor GC)。
    • 老年代(Old Generation):使用 "标记-清除" 算法进行垃圾回收(Major GC)。
    • 方法区。

2️⃣ CMS GC 的垃圾回收流程

CMS GC 的核心思想是:并发执行垃圾回收,尽可能减少 STW 时间

这里的并发执行指的是和应用线程并发执行,不用暂停应用线程也能进行垃圾回收。

垃圾回收流程如下:

  1. 初始标记(Initial Mark,STW):标记 GC Roots 直接关联的对象,STW 时间短。

  2. 并发标记(Concurrent Marking):应用程序运行的同时,遍历对象图,标记可达对象。

  3. 重新标记(Remark,STW):由于并发标记时,可能有对象状态发生变化,因此需要再次 STW,重新标记存活对象

  4. 并发清除(Concurrent Sweep):应用程序运行的同时,并发清除垃圾对象,释放内存。

  5. Full GC(当 CMS GC 失败时触发,STW 时间长):由于 CMS 不进行内存整理(Compaction),可能导致碎片化问题。当大对象无法分配到连续空间时,触发 Full GC(可能造成严重 STW(通常几百毫秒到几秒))。


(3)CMS GC 的垃圾碎片问题

为什么 CMS GC 会产生垃圾碎片?

  • CMS 采用"标记-清除"算法,不进行内存整理,导致老年代中存在很多不连续的空闲内存(碎片)。
  • 大对象需要分配时,如果没有足够的连续空间,JVM 可能触发 Full GC 进行内存整理(STW 时间长)。

解决方案:

  • 参数优化

    • -XX:+UseCMSCompactAtFullCollection(在 Full GC 后进行整理)。
    • -XX:CMSFullGCsBeforeCompaction=3(每 3 次 Full GC 后执行一次内存整理)。
  • 改用 G1 GC

    • G1 GC 通过 Region 化管理和混合回收,可以避免碎片化问题

5.4.2 G1 GC(Garbage First)

G1 GC(Garbage First GC)是 JDK 7u4 引入,并在 JDK 9 成为 默认 GC
适用于大内存应用(4GB 以上),相比 CMS GC 减少了碎片化问题,提供更可预测的 GC 停顿时间

(1)G1 GC 的核心特点

Region(分区化管理),动态调整新生代和老年代比例。
可预测的 GC 停顿时间(-XX:MaxGCPauseMillis)。
并发执行回收,减少 STW 停顿时间。
自动整理内存(不会产生碎片化问题)。
相比 CMS,CPU 开销更高。
吞吐量略低于 Parallel GC。


(2)G1 GC 的工作原理

1️⃣ G1 GC 的堆内存结构

  • 不同于 CMS GC 的 "分代管理",G1 GC 采用 "Region(分区)管理"
    • Eden(新生代)
    • Survivor(新生代)
    • Old(老年代)
    • Humongous(存放大对象)
    • Free(未使用的 Region)

2️⃣ G1 GC 的垃圾回收流程

  1. 年轻代 GC(Minor GC,STW):复制存活对象到 Survivor 或老年代,清空 Eden。

  2. 并发标记(Concurrent Marking,避免 STW):识别老年代中垃圾最多的 Region,准备回收。

  3. 混合回收(Mixed GC,减少 Full GC):同时回收年轻代和部分老年代,减少老年代空间不足问题。

  4. Full GC(极少发生):只有当 G1 GC 失败时才会触发 Full GC。


(3)G1 GC 避免垃圾碎片
  • 通过 Region 化管理对象,回收垃圾最多的 Region,避免碎片化问题。
  • 当需要整理时,可以逐步迁移存活对象,减少 STW 时间

5.4.3 CMS GC vs G1 GC 对比

JDK 9默认使用G1 GC

对比项CMS GCG1 GC
适用场景低延迟应用(Web 服务器)大内存应用(4GB+)
回收策略标记-清除,不整理内存Region 化管理,减少碎片
STW 时间可能较长(Full GC)可预测(-XX:MaxGCPauseMillis
碎片化问题可能严重,影响 Full GC 频率通过 Region 避免碎片
吞吐量较高,但 Full GC 影响较大较稳定,整体吞吐量较优
Full GC 触发碎片化严重时容易触发极少发生

5.5 GC不仅会对堆进行GC还会对方法区GC

  • 堆(Heap)GC:

    • 主要回收 Java 对象(实例)。
    • 频繁触发 GC(Minor GC、Major GC、Full GC)
  • 方法区(Method Area)GC:

    • 主要回收 类的元数据、常量池、JIT 编译后的代码缓存
    • 较少触发 GC(通常在类卸载时进行)。

方法区 GC 主要回收哪些内容?

(1)废弃的常量

  • 字符串常量池(String Pool)
  • 运行时常量池中的数值、类名、方法名、字段名

(2)无用的类

类的卸载(Class Unloading) 发生在以下条件都满足时:

  1. 该类的所有实例都被 GC 回收(即堆中不再存在该类的对象)。
  2. 加载该类的 ClassLoader 本身已经被回收
  3. 该类没有被静态变量(static)引用

注意:JVM 默认不会主动卸载类,通常只有在动态加载和卸载 ClassLoader 时才会发生(如 Web 服务器动态部署)


(3)JIT 编译缓存

JVM 的 JIT 编译器(Just-In-Time Compiler) 会将热点代码编译成本地机器码并缓存到 代码缓存(Code Cache)。当缓存空间不足时,JVM 可能会触发 GC 清除不常用的编译代码。


方法区 GC 触发时机

  • 动态代理、反射、CGLIB 生成的类较多时(如 Spring 框架)。
  • 大量的字符串常量、方法名、字段名 存入常量池。
  • 频繁卸载 ClassLoader(如 Web 服务器重新加载 WAR 包)。
  • JIT 编译器缓存过多代码(如长时间运行的大型 Java 程序)。

6. JVM 内存泄漏

6.1 什么是 JVM 内存泄漏?

在 JVM 中,内存泄漏(Memory Leak) 指的是程序运行过程中,不再使用的对象仍然被引用,导致 GC 无法回收它们,进而导致堆内存(Heap)不断膨胀,最终可能触发 OutOfMemoryError(OOM)

尽管 Java 有 垃圾回收机制(GC),但如果对象仍然被可达引用(Reachable),即使程序不再使用它们,GC 也不会回收这些对象。这就形成了内存泄漏


6.2 内存泄漏的表现

1. 堆内存持续增长

  • JVM 运行时间越长,内存占用越高,甚至 OOM
  • GC 频率升高,但老年代(Old Generation)对象无法释放

2. 应用性能下降

  • 内存占用增加,导致频繁 GC
  • 应用响应时间变慢,甚至崩溃

3. OutOfMemoryError: Java heap space

  • 堆空间耗尽,程序崩溃
  • 发生在 大量对象未释放大对象占用过多内存 的情况下。

6.3 JVM 内存泄漏的常见原因

Java 内存泄漏的根本原因是 无用对象仍然被引用,GC 无法回收它们。常见的几种情况如下:


6.3.1 静态集合类 / 静态变量导致的内存泄漏

原因

  • 静态变量(static)属于类,生命周期与 JVM 一致,不会被 GC 自动回收。
  • 若静态变量持有大量对象引用,即使对象本身不再使用,也不会被回收,从而造成 堆积

示例

import java.util.*;

public class StaticCollectionLeak {
    private static final List<byte[]> memoryLeakList = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            byte[] largeObject = new byte[1024 * 1024]; // 1MB
            memoryLeakList.add(largeObject);
        }
    }
}

问题memoryLeakListstatic,导致所有对象即使不再需要,仍然不会被 GC 回收。 

解决方案

  • 避免静态集合存储大量对象
  • 使用 WeakReferenceSoftReference
  • 手动调用集合的clear()方法清理

6.3.2 监听器 & 观察者模式导致的泄漏

原因

  • 监听器(Listener)或观察者模式(Observer)会使对象之间形成强引用,即使对象不再使用,监听器仍然会保持对它的引用,导致 GC 无法回收。

示例

import java.util.ArrayList;
import java.util.List;

class EventSource {
    private final List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }
}

interface EventListener {
    void onEvent();
}

public class ListenerLeak {
    public static void main(String[] args) {
        EventSource eventSource = new EventSource();
        EventListener listener = () -> System.out.println("Event received!");
        eventSource.addListener(listener);
    }
}

问题

  • listeners 集合会一直持有 EventListener 对象的引用,即使它们不再被使用,导致 GC 不能回收它们。

解决方案

  • 使用 WeakReference 弱引用
private final List<WeakReference<EventListener>> listeners = new ArrayList<>();

6.3.3 线程本地变量(ThreadLocal)泄漏

原因

  • ThreadLocal 绑定的变量存储在线程的 ThreadLocalMap,但如果不手动清理,线程池复用线程时可能会导致数据泄漏。

示例

public class ThreadLocalLeak {
    private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            threadLocal.set(new byte[10 * 1024 * 1024]); // 10MB
        });
        thread.start();
    }
}

问题:线程执行完后,ThreadLocal 变量没有被清理,导致占用 10MB 内存无法释放。

解决方案

  • finally 语句中手动清理 ThreadLocal 变量
try {
    threadLocal.set(new byte[10 * 1024 * 1024]);
} finally {
    threadLocal.remove();
}

6.3.4 内部类 & 匿名类导致的泄漏

原因

  • 非静态内部类匿名类 持有对外部类的隐式引用,如果外部类仍然存活,内部类不会被 GC 回收。

示例

public class InnerClassLeak {
    private byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB

    public void createAnonymousClass() {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println(largeArray.length);
            }
        };
        new Thread(task).start();
    }

    public static void main(String[] args) {
        new InnerClassLeak().createAnonymousClass();
    }
}

问题

task 作为匿名内部类会持有 InnerClassLeak 的引用?

在 Java 中,非静态内部类和匿名类会隐式地持有外部类实例的引用,这就是 隐式引用(Implicit Reference)。这意味着:

  • 匿名内部类 taskrun() 方法内部,访问了 largeArrayInnerClassLeak 的成员变量)。
  • 由于 largeArrayInnerClassLeak 的实例变量,所以 匿名类 task 需要持有 InnerClassLeak 的引用,才能访问 largeArray
  • 这种 外部类的隐式引用 可能会导致 InnerClassLeak 对象无法被 GC 回收,从而导致内存泄漏

解决方案

  • 使用静态内部类,避免隐式引用
public class InnerClassLeak {
    private byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB

    public void createStaticInnerClass() {
        new Thread(new StaticTask(largeArray)).start(); // 直接传递 largeArray
    }

    // 变为静态内部类
    private static class StaticTask implements Runnable {
        private final byte[] arrayRef;

        StaticTask(byte[] arrayRef) {
            this.arrayRef = arrayRef;
        }

        @Override
        public void run() {
            System.out.println(arrayRef.length);  // 访问传入的参数,而不是外部类变量
        }
    }

    public static void main(String[] args) {
        new InnerClassLeak().createStaticInnerClass();
    }
}

为什么静态内部类可以避免内存泄漏?

  • 静态内部类不会持有外部类 InnerClassLeak 的隐式引用

    • StaticTask 使用 static 修饰后,就不再与 InnerClassLeak 绑定,它变成了独立的类
    • StaticTask 不会自动持有 InnerClassLeak 的实例引用
  • 显式传递 largeArray,避免隐式引用

    new Thread(new StaticTask(largeArray)).start();
    
    • 我们显式地将 largeArray 传递给 StaticTask 构造方法,这样 StaticTask 只持有 largeArray 的引用,而不是 InnerClassLeak 的整个实例。
    • 即使 InnerClassLeak 被 GC 回收,StaticTask 仍然可以正常运行
方案是否会导致内存泄漏?原因
匿名内部类可能会泄漏持有外部类 InnerClassLeak 的隐式引用,导致 largeArray 无法回收
静态内部类不会泄漏不再持有 InnerClassLeak 的引用,只持有 largeArray,可以安全回收

最佳实践

  • 避免匿名类访问外部类的实例变量,否则可能会无意间创建隐式引用,导致对象不能被 GC
  • 如果必须使用内部类,建议使用 static 内部类,并通过构造方法传递所需数据,避免隐式引用外部类。

6.3.5 数据库连接 / IO 资源未关闭

原因

  • 数据库连接(JDBC)、文件流、Socket 连接未正确关闭,导致资源泄漏,最终耗尽可用内存。
  • GC 无法自动回收这些外部资源

示例

public class ConnectionLeak {
    public static void main(String[] args) throws Exception {
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT * FROM users");

        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }
        // 🚨 资源未关闭,泄漏
    }
}

解决方案

  • 使用 try-with-resources
    try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");
         Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
        
        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }
    }
    
    try-with-resources 确保所有资源自动关闭!

6.4 如何检测 JVM 内存泄漏?

(1) 使用 jmap 查看堆内存

jmap -histo:live <pid>
  • 分析大对象占用情况,找出无法被回收的对象。

(2)使用 jconsole 监控 JVM 内存

  • 实时观察堆内存使用趋势,发现是否存在不断增长且无法释放的对象

(3)使用 VisualVM 进行 Heap Dump 分析

jmap -dump:format=b,file=heapdump.hprof <pid>
  • 导入 VisualVM,分析对象引用关系,找出无法被 GC 回收的对象

(4)使用 MAT(Memory Analyzer Tool)

  • MAT(Eclipse Memory Analyzer) 可以分析 .hprof 文件,找出GC Root 保持的对象,定位泄漏点。

7. JVM 内存溢出(OutOfMemoryError, OOM) 

7.1. 什么是 JVM 内存溢出?

JVM 内存溢出(OutOfMemoryError,简称 OOM) 是指 JVM 试图分配内存,但由于内存不足或内存无法回收,导致 JVM 运行失败并抛出 java.lang.OutOfMemoryError 异常

JVM 主要的内存区域

  • 堆(Heap):存储对象实例。
  • 栈(Stack):存储方法调用帧、局部变量。
  • 方法区(Method Area)(JDK 8+ 称为 Metaspace):存储类元数据、方法、静态变量等。
  • 直接内存(Direct Memory):NIO 直接分配的操作系统内存。

7.2. 常见的 OOM 类型

JVM 内存溢出 通常发生在以下几种区域

OOM 错误类型发生区域主要原因
java.lang.OutOfMemoryError: Java heap space堆(Heap)- 对象过多,无法回收,导致堆空间耗尽(如集合无限增长,缓存未清理)。
- 单个大对象分配失败(如一次性分配超大数组)。
java.lang.OutOfMemoryError: GC overhead limit exceeded堆(Heap)- GC 频繁运行但每次回收内存极少,导致 CPU 资源被大量消耗。
java.lang.OutOfMemoryError: Metaspace方法区(Metaspace)- 类加载过多(如 Spring 频繁创建代理类,动态类加载)。
- 类无法卸载(如自定义 ClassLoader 造成内存泄漏)。
java.lang.StackOverflowError栈(Stack)- 方法递归过深,导致栈帧溢出(如无限递归)。
- 每个线程栈空间不足,导致溢出。
java.lang.OutOfMemoryError: unable to create new native thread本地内存(OS 线程数)- 线程创建过多,超出 OS 允许的最大线程数(如无限创建 new Thread())。
- 每个线程栈大小过大,导致系统无法分配新线程。
java.lang.OutOfMemoryError: Direct buffer memory直接内存(Direct Memory)- NIO 直接缓冲区 (ByteBuffer.allocateDirect()) 分配过多,超过 MaxDirectMemorySize 限制。
java.lang.OutOfMemoryError: Swap space操作系统 Swap 交换空间- 应用分配内存过多,导致 OS 交换空间耗尽(一般在物理内存不足时发生)。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit堆(Heap)- 试图分配超大数组(如 new int[Integer.MAX_VALUE])。
java.lang.OutOfMemoryError: Compressed class space方法区(Metaspace, Class Space)- JVM 运行时加载类过多,超出 CompressedClassSpaceSize 限制。

7.3 各种 JVM 内存溢出情况

7.3.1. 堆内存溢出(java.lang.OutOfMemoryError: Java heap space)

原因

  • 创建对象过多,堆空间不断增长,无法回收(如:缓存未清理、集合不断增长)。
  • 单个大对象分配失败(如:一次性分配一个超大数组)。
  • 内存泄漏,无用对象仍然被引用,GC 无法清理。

示例

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[10 * 1024 * 1024]); // 每次分配 10MB
        }
    }
}

解决方案

增大堆空间(适用于对象确实需要更多内存的情况):

java -Xms2g -Xmx4g HeapOOM

优化 GC 策略

java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 HeapOOM

检测内存泄漏

  • 使用 jmap
jmap -histo:live <pid>
  • 使用 Heap Dump
jmap -dump:format=b,file=heapdump.hprof <pid>
  • 使用 VisualVMMAT(Memory Analyzer Tool)分析 heapdump.hprof

7.3.2. 栈内存溢出(StackOverflowError 或 Stack Space OOM)

原因

  • 递归调用过深,导致栈帧不断压入,最终超过栈空间大小。
  • 创建大量线程,导致 JVM 线程栈空间耗尽。

示例 1:递归调用导致 StackOverflowError

public class StackOverflowDemo {
    public void recursiveMethod() {
        recursiveMethod(); // 无限递归
    }

    public static void main(String[] args) {
        new StackOverflowDemo().recursiveMethod();
    }
}

示例 2:创建大量线程导致 OutOfMemoryError: Unable to create new native thread

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadOOM {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(1000);
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(() -> {
                while (true) {}  // 每个线程执行无限循环
            });
        }
    }
}

解决方案

减少递归深度增大栈空间

java -Xss1m StackOverflowDemo

控制线程池大小

ExecutorService executor = Executors.newFixedThreadPool(100);

7.3.3. 方法区/元空间溢出(Metaspace OOM)

原因

  • 大量动态生成的类(如:大量使用 CGLIBJavassist 动态代理)。
  • Spring Boot 等框架频繁加载新类,导致 Metaspace 过满。
  • 应用长时间运行,但类卸载不及时,导致 Metaspace 持续增长。

示例

import javassist.ClassPool;

public class MetaspaceOOM {
    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            classPool.makeClass("com.example.GeneratedClass" + i).toClass();
        }
    }
}

解决方案

增加 Metaspace 大小

java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m MetaspaceOOM

减少动态生成的类


7.3.4. GC 过载导致 OOM(java.lang.OutOfMemoryError: GC Overhead limit exceeded)

原因

  • GC 运行时间过长,超过 98% CPU,但回收的内存不足 2%,JVM 触发此 OOM 保护机制。
  • 堆内存不足,导致 GC 频繁执行,但对象回收效果不佳。

示例

import java.util.HashMap;
import java.util.Map;

public class GCOverheadOOM {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        int i = 0;
        while (true) {
            map.put(i, "OOM Test " + i++); // 不断填充 HashMap
        }
    }
}

解决方案

增大堆空间,减少 GC 触发

java -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 GCOverheadOOM

使用 -XX:-UseGCOverheadLimit 关闭 GC 限制

java -Xmx4g -XX:-UseGCOverheadLimit GCOverheadOOM

7.3.5. 直接内存溢出(Direct Buffer Memory OOM)

原因

  • NIO ByteBuffer 分配过多,导致 Direct Memory 耗尽。
  • JVM 直接内存上限太低,无法满足 ByteBuffer.allocateDirect() 分配请求。

示例

import java.nio.ByteBuffer;

public class DirectMemoryOOM {
    public static void main(String[] args) {
        while (true) {
            ByteBuffer.allocateDirect(1024 * 1024); // 每次申请 1MB 直接内存
        }
    }
}

解决方案

增大直接内存

java -XX:MaxDirectMemorySize=512m DirectMemoryOOM

避免无限制分配

ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
buffer.clear(); // 复用 Buffer,避免反复分配

8. JVM 常见参数及其作用

在 JVM 运行 Java 应用时,我们可以使用 JVM 参数 来控制内存分配、垃圾回收(GC)策略、性能优化等。本文将详细介绍 JVM 常见参数的分类、作用、以及如何设置这些参数。


8.1 JVM 参数设置方法

在生产环境中,JVM 参数通常通过以下方式进行设置:

(1)直接通过 java 命令行设置

适用于 独立 Java 应用、测试环境,例如:

java -Xms2g -Xmx4g -XX:+UseG1GC -jar myapp.jar

(2)在 JAVA_OPTSJAVA_TOOL_OPTIONS 环境变量中设置

适用于 Web 服务器(Tomcat、Spring Boot、微服务)

export JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC"

(3)在 Docker 容器中设置

容器化部署时,一般通过环境变量 JAVA_OPTS 传递:

docker run -e "JAVA_OPTS=-Xmx4g -XX:+UseG1GC" my-java-app

(4)在 Kubernetes(K8s)中设置

对于 K8s 部署的 Java 应用,可以在 Deployment 配置文件中设置:

env:
  - name: JAVA_OPTS
    value: "-Xms2g -Xmx4g -XX:+UseG1GC"

8.2 常用 JVM 参数及生产环境实践

8.2.1 内存管理参数

作用:控制 JVM 的 堆(Heap)、栈(Stack)、方法区(Metaspace) 大小,影响 GC 频率和性能。

参数作用生产环境建议
-Xms<size>初始堆大小(默认 1/64 物理内存)设置与 -Xmx 相同,避免运行时扩展
-Xmx<size>最大堆大小(默认 1/4 物理内存)根据可用内存大小设置,如 -Xmx4g
-XX:NewRatio=n新生代:老年代 比例(默认 2,即 1:2推荐 NewRatio=2,适用于吞吐量型应用
-XX:SurvivorRatio=nEden:Survivor 比例(默认 8:1:1保持默认 SurvivorRatio=8
-Xss<size>每个线程的栈大小(默认 1MB)适用于高并发应用,如 -Xss512k 减少栈内存占用
-XX:MetaspaceSize=256mJDK 8+ 方法区大小推荐 256m
-XX:MaxMetaspaceSize=512m元空间最大值防止 Metaspace OOM,推荐 512m

8.2.2 GC(垃圾回收)策略

作用:选择合适的 GC 机制,降低 STW(Stop-The-World) 停顿时间,提高吞吐量。

参数作用生产环境建议
-XX:+UseSerialGC单线程 GC(适用于小型应用)不推荐用于生产环境
-XX:+UseParallelGC多线程吞吐量 GC适用于批处理任务、Kafka、Spark
-XX:+UseG1GC低延迟 GC(默认)适用于 Web 服务器 / 微服务
-XX:+UseZGC超低延迟 GC(JDK 11+)适用于金融、超大堆(TB 级)应用
-XX:MaxGCPauseMillis=200最大 GC 停顿时间适用于 G1 GC,控制 STW 时长

8.2.3 JIT(Just-In-Time 编译)优化

作用:优化 JIT 编译,提高 Java 代码执行性能。

参数作用生产环境建议
-XX:+TieredCompilation分层 JIT 编译默认启用,适用于高并发应用
-XX:+PrintCompilation打印 JIT 编译方法调试时启用

8.2.4 线程管理

作用:控制并发线程数,提高 CPU 资源利用率。

参数作用生产环境建议
-XX:ParallelGCThreads=<n>GC 并行线程数推荐 CPU 核心数 / 2
-XX:ConcGCThreads=<n>G1 / ZGC 并发线程数适用于低延迟应用

8.2.5 监控与日志

作用:启用 GC 日志,监控应用运行状态。

参数作用生产环境建议
-XX:+HeapDumpOnOutOfMemoryErrorOOM 生成 Heap Dump强烈建议启用
-XX:HeapDumpPath=<path>Heap Dump 存储路径推荐 /var/logs/heapdump.hprof
-XX:+PrintGCDetails打印 GC 详情生产环境推荐
-Xloggc:/var/logs/gc.logGC 日志存储路径用于 GC 监控分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2296466.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

利用二分法进行 SQL 盲注

什么是sql注入&#xff1f; SQL 注入&#xff08;SQL Injection&#xff09;是一种常见的 Web 安全漏洞&#xff0c;攻击者可以通过构造恶意 SQL 语句来访问数据库中的敏感信息。在某些情况下&#xff0c;服务器不会直接返回查询结果&#xff0c;而是通过布尔值&#xff08;Tr…

大模型数据集全面整理:444个数据集下载地址

本文针对Datasets for Large Language Models: A Comprehensive Survey 中的 444 个数据集&#xff08;涵盖8种语言类别和32个领域&#xff09;进行完整下载地址整理收集。 2024-02-28&#xff0c;由杨刘、曹家欢、刘崇宇、丁凯、金连文等作者编写&#xff0c;深入探讨了大型语…

Linux 创建进程 fork()、vfork() 与进程管理

Linux 创建进程 fork、vfork、进程管理 一、Linux的0号、1号、2号进程二、Linux的进程标识三、fork() 函数1、基本概念2、函数特点3、用法以及应用场景&#xff08;1&#xff09;父子进程执行不同的代码&#xff08;2&#xff09;进程执行另一个程序 4、工作原理 四、vfork() 函…

2025web寒假作业二

一、整体功能概述 该代码构建了一个简单的后台管理系统界面&#xff0c;主要包含左侧导航栏和右侧内容区域。左侧导航栏有 logo、管理员头像、导航菜单和安全退出按钮&#xff1b;右侧内容区域包括页头、用户信息管理内容&#xff08;含搜索框和用户数据表格&#xff09;以及页…

鸿蒙NEXT API使用指导之文件压缩和邮件创建

鸿蒙NEXT API 使用指导 一、前言二、邮件创建1、拉起垂类应用2、 UIAbilityContext.startAbilityByType 原型2.1、wantParam2.2、abilityStartCallback 与 callback 3、拉起邮箱类应用3.1、单纯拉起邮箱应用3.2、传入带附件的邮件 三、压缩文件1、认识 zlib2、压缩处理2.1、单文…

javaEE-10.CSS入门

目录 一.什么是CSS ​编辑二.语法规则: 三.使用方式 1.行内样式: 2.内部样式: 3.外部样式: 空格规范 : 四.CSS选择器类型 1.标签选择器 2.类选择器 3.ID选择器 4.通配符选择器 5.复合选择器 五.常用的CSS样式 1.color:设置字体颜色 2.font-size:设置字体大小 3…

Spring Boot牵手Redisson:分布式锁实战秘籍

一、引言 在当今的分布式系统架构中,随着业务规模的不断扩大和系统复杂度的日益增加,如何确保多个服务节点之间的数据一致性和操作的原子性成为了一个至关重要的问题。在单机环境下,我们可以轻松地使用线程锁或进程锁来控制对共享资源的访问,但在分布式系统中,由于各个服务…

制药行业 BI 可视化数据分析方案

一、行业背景 随着医药行业数字化转型的深入&#xff0c;企业积累了海量的数据&#xff0c;包括销售数据、生产数据、研发数据、市场数据等。如何利用这些数据&#xff0c;挖掘其价值&#xff0c;为企业决策提供支持&#xff0c;成为医药企业面临的重大挑战。在当今竞争激烈的…

[学习笔记] Kotlin Compose-Multiplatform

Compose-Multiplatform 原文&#xff1a;https://github.com/zimoyin/StudyNotes-master/blob/master/compose-multiplatform/compose.md Compose Multiplatform 是 JetBrains 为桌面平台&#xff08;macOS&#xff0c;Linux&#xff0c;Windows&#xff09;和Web编写Kotlin UI…

Golang 并发机制-7:sync.Once实战应用指南

Go的并发模型是其突出的特性之一&#xff0c;但强大的功能也带来了巨大的责任。sync.Once是由Go的sync包提供的同步原语。它的目的是确保一段代码只执行一次&#xff0c;而不管有多少协程试图执行它。这听起来可能很简单&#xff0c;但它改变了并发环境中管理一次性操作的规则。…

【AI实践】Cursor上手-跑通Hello World和时间管理功能

背景 学习目的&#xff1a;熟悉Cursor使用环境&#xff0c;跑通基本开发链路。 本人背景&#xff1a;安卓开发不熟悉&#xff0c;了解科技软硬件常识 实践 基础操作 1&#xff0c;下载安装安卓Android Studio 创建一个empty project 工程&#xff0c;名称为helloworld 2&am…

【多模态大模型】系列4:目标检测(ViLD、GLIP)

目录 1 ViLD2 GLIP 1 ViLD OPEN-VOCABULARY OBJECT DETECTION VIA VISION AND LANGUAGE KNOWLEDGE DISTILLATION 从标题就能看出来&#xff0c;作者是把CLIP模型当成一个Teacher&#xff0c;去蒸馏他自己的网络&#xff0c;从而能Zero Shot去做目标检测。 现在的目标检测数据…

计算机网络结课设计:通过思科Cisco进行中小型校园网搭建

上学期计算机网络课程的结课设计是使用思科模拟器搭建一个中小型校园网&#xff0c;当时花了几天时间查阅相关博客总算是做出来了&#xff0c;在验收后一直没管&#xff0c;在寒假想起来了简单分享一下&#xff0c;希望可以给有需求的小伙伴一些帮助 目录 一、设计要求 二、…

从零到一:基于Rook构建云原生Ceph存储的全面指南(下)

接上篇&#xff1a;《从零到一&#xff1a;基于Rook构建云原生Ceph存储的全面指南&#xff08;上&#xff09;》 链接: link 六.Rook部署云原生CephFS文件系统 6.1 部署cephfs storageclass cephfs文件系统与RBD服务类似&#xff0c;要想在kubernetes pod里使用cephfs&#…

AutoMQ 如何实现没有写性能劣化的极致冷读效率

前言 追赶读&#xff08;Catch-up Read&#xff0c;冷读&#xff09;是消息和流系统常见和重要的场景。 削峰填谷&#xff1a;对于消息来说&#xff0c;消息通常用作业务间的解耦和削峰填谷。削峰填谷要求消息队列能将上游发送的数据堆积住&#xff0c;让下游在容量范围内消费…

【Rabbitmq篇】高级特性----TTL,死信队列,延迟队列

目录 一.TTL ???1.设置消息的TTL 2.设置队列的TTL 3.俩者区别? 二.死信队列 定义&#xff1a; 消息成为死信的原因&#xff1a; 1.消息被拒绝&#xff08;basic.reject 或 basic.nack&#xff09; 2.消息过期&#xff08;TTL&#xff09; 3.队列达到最大长度? …

【Java】多线程和高并发编程(三):锁(中)深入ReentrantLock

文章目录 3、深入ReentrantLock3.1 ReentrantLock和synchronized的区别3.2 AQS概述3.3 加锁流程源码剖析3.3.1 加锁流程概述3.3.2 三种加锁源码分析3.3.2.1 lock方法3.3.2.2 tryLock方法3.3.2.3 lockInterruptibly方法 3.4 释放锁流程源码剖析3.4.1 释放锁流程概述3.4.2 释放锁…

电路笔记(元器件):AD 5263数字电位计(暂记)

AD5263 是四通道、15 V、256位数字电位计&#xff0c;可通过SPI/I2C配置具体电平值。 配置模式&#xff1a; W引脚作为电位器的抽头&#xff0c;可在A-B之间调整任意位置的电阻值。也可将W与A(或B)引脚短接&#xff0c;A-W间的电阻总是0欧姆&#xff0c;通过数字接口调整电位器…

webpack【初体验】使用 webpack 打包一个程序

打包前 共 3 个文件 dist\index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Webpack 示例&…

VMware安装CentOS 7(全网超详细图文保姆版教程)

文章目录 一、下载及安装 VMware1.1 VMware下载1.2 CentOS下载 二、搭建虚拟机环境2.1 创建新虚拟机2.2 选择自定义2.3 选择虚拟机硬件兼容性2.4 选择稍后安装操作系统2.5 选择Linux系统 版本选择 centos 7 64位2.6 设备你虚拟机的名字和保存位置&#xff08;保存位置建议在编辑…