当前位置: 首页 > news >正文

深入剖析 JVM 内存模型

前言:

下面分别介绍了新生代和老年代的不同收集器及其相关子类型,并附有示例代码和说明,感兴趣的朋友可以参考一下。


简介:

在 Java 虚拟机(JVM)的世界里,内存模型是其核心架构之一,它决定了 Java 程序如何存储和管理数据,深刻影响着程序的性能和稳定性。了解 JVM 内存模型,对于优化 Java 应用、排查内存相关问题至关重要。


一、类加载器子系统

类加载器子系统在 JVM 中扮演着数据 “搬运工” 的角色,负责将字节码文件加载到 JVM 中,并进行一系列处理,确保其能被 JVM 正确执行。

(一)类加载的过程

  1. 加载:这是类加载的起始步骤,通过类的全限定名找到对应的二进制字节流。然后,将字节流代表的静态存储结构转化为方法区的运行时数据结构,并在内存中生成一个java.lang.Class对象,作为访问该类各种数据的入口。比如,当我们编写一个简单的HelloWorld类,运行时类加载器就会找到HelloWorld.class文件并加载它。
  2. 验证:如同质量检测员,验证步骤确保被加载的类是正确无误的。它包括文件格式验证(检查是否以魔数0xCAFEBABE开头)、元数据验证(比如类是否有合法的父类等)、字节码验证(检测字节码指令语义是否合法)以及符号引用验证(确保符号引用指向的目标存在且可访问)。一旦验证不通过,JVM 会抛出异常,阻止类的加载。
  3. 准备:准备阶段为类的静态变量分配内存,并设置默认初始值,这些内存都在方法区分配。例如,对于static int num = 10;,在准备阶段num会被分配内存并初始化为 0,而不是 10,10 是在后续初始化阶段才赋值的。
  4. 解析:该阶段将常量池中的符号引用替换为直接引用。符号引用是间接的,像类的全限定名;而直接引用则是能直接指向目标的指针、相对偏移量等。在解析时,类中对其他类的引用会从符号引用转为直接引用,方便 JVM 直接访问。
  5. 初始化:此阶段执行类构造器()方法,为类的静态变量赋予正确初始值,同时执行静态代码块。例如:
public class StaticInit {static {System.out.println("Static block is executed");}static int num = 10;
}

当StaticInit类初始化时,静态代码块先执行,然后num被赋值为 10。

(二)双亲委派机制

双亲委派机制是类加载器的核心机制,它的工作流程就像一个严谨的 “任务分配链”。

  1. 当一个类加载器收到类加载请求时,它不会立刻自己去加载,而是先把请求委托给父类加载器。
  2. 父类加载器同样会把请求继续向上委托,直到到达启动类加载器。
  3. 启动类加载器尝试加载这个类,如果成功,就返回对应的Class对象;若失败,子类加载器才会尝试自己加载。

常见的类加载器有以下几种:

  1. 启动类加载器:由 C++ 实现,是 JVM 的一部分,负责加载 Java 核心类库,如java.lang包下的类,加载路径是rt.jar等核心库所在路径。
  2. 拓展类加载器:用 Java 实现,继承自ClassLoader类,负责加载 Java 的拓展类库,加载路径一般是jre/lib/ext目录下的类库。
  3. 应用程序类加载器:也叫系统类加载器,同样是 Java 实现,负责加载应用程序的类路径(classpath)下的所有类。开发中我们自己编写的类和第三方依赖库的类,大多由它加载。
  4. 自定义类加载器:开发者可根据需求自定义,继承ClassLoader类并重写相关方法。在一些特殊场景,如从网络或加密存储介质加载类时会用到。

双亲委派机制保障了 Java 核心类库的安全性和一致性。例如,java.lang.Object类在任何应用中都由启动类加载器加载,避免了不同类加载器加载出不同版本的Object类而引发混乱。

(三)双亲委派机制工作原理的深入剖析

双亲委派机制的实现主要依赖于ClassLoader类中的loadClass方法。下面是简化后的源代码示例:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// 先检查该类是否已被加载Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent!= null) {// 父类加载器不为空,委托父类加载c = parent.loadClass(name, false);} else {// 父类加载器为空,说明到了启动类加载器,尝试由其加载c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 父类加载器加载失败,子类加载器自己尝试加载}if (c == null) {long t1 = System.nanoTime();// 子类加载器自己加载类c = findClass(name);sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}

从代码可知,loadClass方法首先检查类是否已加载,若未加载,则按双亲委派规则,先委托父类加载器加载。若父类加载器加载失败(抛出ClassNotFoundException异常),子类加载器才调用自己的findClass方法尝试加载。


二、本地方法库与本地接口库

(一)本地方法库

本地方法库是 JVM 中存放用 C、C++ 等语言编写的本地方法的代码库。当 Java 程序调用本地方法时,JVM 通过本地接口库找到对应的实现。例如,System.currentTimeMillis()方法获取当前时间,实际是调用了本地 C 或 C++ 代码,因为底层操作系统提供了更高效的时间获取机制,通过本地方法可直接利用这些底层功能。

(二)本地接口库

本地接口库是 Java 与本地方法库之间的桥梁,提供了 Java 代码调用本地方法,以及本地方法访问 Java 对象和数据的机制。JNI(Java Native Interface)是最常用的本地接口,通过它,Java 代码能调用 C、C++ 编写的函数,还能在 Java 和本地代码间传递基本类型、对象等数据。比如,在 Java 程序中调用 C++ 编写的图像处理库,就可通过 JNI 实现交互。


三、执行引擎

执行引擎是 JVM 的 “动力核心”,负责执行字节码指令。

(一)即时编译器

即时编译器(JIT,Just - In - Time Compiler)是执行引擎的重要部分,它在运行时将字节码编译成机器码,提升程序执行效率。JIT 编译器主要有两种类型:

  1. Client Compiler(C1 编译器):编译速度快,适用于启动时间敏感的应用,如桌面应用程序。它采用简单优化策略,如方法内联(将被调用方法的代码直接插入调用处)。
  2. Server Compiler(C2 编译器):编译速度相对较慢,但会进行更复杂、深度的优化,适用于长时间运行且对性能要求高的服务器端应用。它会进行逃逸分析(分析对象作用域是否会逃出当前方法)、锁消除(若发现锁对象只在一个线程中使用,消除不必要的锁操作)等高级优化。

(二)垃圾收集

垃圾收集是执行引擎的另一重要功能,负责回收不再使用的内存空间。JVM 中有多种垃圾收集算法:

  1. 标记 - 清除算法:先标记所有需要回收的对象,标记完成后统一回收。其缺点是会产生大量不连续的内存碎片,可能导致后续程序分配较大对象时找不到足够连续内存。
  2. 复制算法:将内存分为大小相等的两块,每次只用一块。当这块内存用完,将存活对象复制到另一块,然后清理已使用的内存空间。该算法适用于新生代,因为新生代对象存活率低,复制操作成本相对较低。
  3. 标记 - 整理算法:与标记 - 清除算法类似,但标记完成后,不是直接清理被标记对象,而是将所有存活对象向一端移动,然后清理端边界以外的内存。此算法适用于老年代,因为老年代对象存活率高,复制算法成本高。

四、运行时数据区

运行时数据区是 JVM 运行时使用的内存区域,包含以下几个部分:

(一)本地方法栈

本地方法栈与虚拟机栈类似,不过它是为执行本地方法服务的。主要用于存储本地方法的局部变量表、操作数栈、动态连接、方法出口等信息。当 Java 程序调用本地方法时,JVM 会在本地方法栈中为该方法创建一个栈帧,存储方法执行过程中的各种数据。比如调用 C++ 编写的本地方法时,JVM 会在本地方法栈为其分配栈帧,保存参数、局部变量等信息。

(二)程序计数器

程序计数器是一块较小的线程私有内存空间。每个线程都有自己的程序计数器,它记录当前线程执行的字节码指令地址。如果线程执行的是 Java 方法,计数器记录虚拟机字节码指令地址;若执行的是本地方法,计数器值为空(Undefined)。例如,线程执行循环语句时,程序计数器不断更新,指向循环体中当前要执行的字节码指令,确保线程按顺序正确执行代码。

(三)虚拟机栈

虚拟机栈也是线程私有的,描述 Java 方法执行的内存模型。每个方法执行时都会创建一个栈帧,栈帧包含以下部分:

  1. 局部变量表:用于存储方法的参数和局部变量。局部变量表容量以变量槽(Slot)为单位,每个变量槽可存放一个 32 位以内的数据类型,如 int、short、char 等。对于 64 位数据类型(如 long、double),则需占用两个连续变量槽。
  2. 操作数栈:是一个后入先出(LIFO)栈,用于保存方法执行过程中的中间计算结果。例如执行加法运算int result = a + b;时,先将a和b的值压入操作数栈,执行加法后将结果再压入,最后赋值给result。
  3. 动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,用于支持方法调用过程中的动态连接。在解析阶段,符号引用转换为直接引用,存储在动态连接中。
  4. 方法出口:方法执行完成后,需从调用它的方法返回,方法出口就是处理方法返回相关事宜的,包括恢复上层方法的局部变量表和操作数栈等。

(四)方法区

在 JDK 1.8 之前,方法区用于存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,是各个线程共享的内存区域。JDK 1.8 之后,方法区的实现发生变化,将永久代替换为元空间(Metaspace)。元空间不在 JVM 的堆内存中,而是使用本地内存(Native Memory)。这样做主要是为了解决永久代容易出现的内存溢出问题,因为元空间大小只受限于本地内存大小,不像永久代受限于 JVM 的堆内存大小。比如在使用大量动态生成类的应用场景中,如 Spring 框架的动态代理机制,若使用永久代,很容易因不断生成新类导致永久代内存溢出,而元空间则可避免这种情况。

(五)堆

堆是 JVM 中最大的内存区域,被所有线程共享,主要用于存储对象实例和数组。堆又可细分为老年代和新生代。

  1. 新生代:新创建的对象首先存放在新生代。它分为一个 Eden 区和两个 Survivor 区(S0 和 S1)。新对象创建时先分配到 Eden 区,当 Eden 区满时,触发一次 Minor GC(新生代垃圾回收)。在 Minor GC 过程中,Eden 区和 Survivor 区中存活的对象会被复制到另一个 Survivor 区(若目标 Survivor 区空间不足,会通过分配担保机制进入老年代)。如果一个对象在 Survivor 区经历 15 次(默认值,可通过参数调整)垃圾回收后仍存活,就会晋升到老年代。
  2. 老年代:老年代主要存放从新生代晋升过来的对象,以及一些大对象(可通过参数设置大对象直接进入老年代)。老年代垃圾回收频率相对较低,当老年代内存不足时,会触发 Major GC(也称为 Full GC),它会对整个堆进行垃圾回收,包括新生代和老年代。

五、优化 JVM 内存模型的方法

(一)合理设置堆内存大小

通过调整-Xms(初始堆大小)和-Xmx(最大堆大小)参数,可根据应用程序实际需求合理分配堆内存。若初始堆大小设置过小,可能导致频繁垃圾回收,影响性能;若最大堆大小设置过大,会浪费内存资源,且垃圾回收时间更长。例如,对于内存需求大的服务器端应用,可适当增大-Xmx的值,如-Xmx2g,表示最大堆大小为 2GB。

(二)选择合适的垃圾收集器

不同的垃圾收集器适用于不同应用场景:

  1. Serial 收集器:单线程垃圾收集器,适用于单 CPU 环境下的小型应用,垃圾回收时会暂停所有线程,但简单高效。可通过-XX:+UseSerialGC参数启用。
  2. Parallel 收集器:多线程垃圾收集器,追求高吞吐量,适用于后台运算且交互少的任务。可通过-XX:+UseParallelGC参数启用。
  3. CMS(Concurrent Mark Sweep)收集器:以获取最短回收停顿时间为目标,适用于对响应时间要求高的应用,如 Web 应用。可通过-XX:+UseConcMarkSweepGC参数启用。
  4. G1(Garbage - First)收集器:面向服务器的垃圾收集器,能兼顾吞吐量和低延迟,适用于大内存、多 CPU 的服务器环境。可通过-XX:+UseG1GC参数启用。

(三)优化对象创建和使用

  1. 减少不必要的对象创建:避免在循环中频繁创建对象,若对象可复用,尽量复用。比如在循环中创建大量String对象时,可考虑使用StringBuilder或StringBuffer,避免不必要的对象创建和内存开销。
  2. 及时释放对象引用:当对象不再使用时,及时将其引用设置为null,以便垃圾收集器及时回收对象占用的内存。

(四)监控和分析 JVM 内存使用情况

使用 JConsole、VisualVM 等工具,可实时监控 JVM 内存使用情况,包括堆内存、方法区等区域的使用情况,以及垃圾回收的频率和时间等。通过分析这些数据,能发现内存泄漏、频繁垃圾回收等问题,并针对性地优化。例如,通过 VisualVM 的可视化界面,能清晰看到堆内存的增长趋势、垃圾回收的次数和耗时等信息,帮助找出性能瓶颈。


六、垃圾收集器的深度剖析

(一)垃圾收集器的工作原理基础

垃圾收集器的核心工作是识别出内存中不再被使用的对象(即垃圾对象),并回收它们所占用的内存空间。为了实现这一目标,垃圾收集器通常采用两种主要的算法思想:引用计数法和可达性分析算法。

  1. 引用计数法:这种方法为每个对象添加一个引用计数器,每当有一个地方引用该对象时,计数器就加 1;当引用失效时,计数器就减 1。当计数器的值为 0 时,就认为该对象不再被使用,可以被回收。然而,引用计数法存在一个严重的问题,即无法解决循环引用的情况。例如,对象 A 和对象 B 互相引用,即使它们在程序中已经不再被其他地方使用,但由于它们之间的循环引用,它们的引用计数器永远不会为 0,从而导致内存泄漏。所以,在主流的 JVM 垃圾收集器中,很少单独使用引用计数法。
  2. 可达性分析算法:这是目前主流 JVM 垃圾收集器采用的算法。它通过一系列的 “GC Roots” 对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可达的,即可以被判定为垃圾对象。在 Java 中,能够作为 GC Roots 的对象包括虚拟机栈中局部变量表中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象以及本地方法栈中 JNI 引用的对象等。

(二)垃圾收集器的分类与特点

新生代垃圾收集器:

    • Serial 收集器:在新生代,Serial 收集器采用复制算法。它是单线程工作的,在进行垃圾回收时,会暂停所有用户线程。虽然工作方式简单,但在单 CPU 环境下,由于没有线程切换的开销,它能高效完成垃圾回收任务,且实现成本较低,对于内存较小的应用场景,性能表现尚可。以下是简单模拟其工作过程的代码示例(非实际 JVM 中的源代码,仅为示意):
// 假设这是一个简单的对象类
class SimpleObject {// 一些属性和方法省略
}public class SerialCollectorExample {public static void main(String[] args) {// 模拟创建一些对象SimpleObject obj1 = new SimpleObject();SimpleObject obj2 = new SimpleObject();// 假设这里有一个方法来模拟垃圾回收serialCollect();}private static void serialCollect() {// 这里简单模拟标记哪些对象是垃圾(实际更复杂)boolean isObj1Garbage = true;boolean isObj2Garbage = false;if (isObj1Garbage) {// 回收obj1占用的内存(实际JVM中是通过特定机制)obj1 = null;}if (isObj2Garbage) {obj2 = null;}}
}
  • ParNew 收集器:ParNew 收集器是 Serial 收集器在新生代的多线程版本,同样采用复制算法。它能充分利用多 CPU 的优势,在垃圾回收时多个线程同时工作,从而提高垃圾回收的效率。在多 CPU 环境下,其性能通常优于 Serial 收集器。并且,它是许多运行在 Server 模式下的虚拟机的首选新生代收集器,因为它可以与 CMS 收集器配合使用,满足一些对响应时间要求较高的应用场景。虽然 ParNew 收集器的核心代码涉及 JVM 底层实现,较为复杂,但可通过如下简化的多线程处理思路示例(非实际源代码)来理解其多线程工作方式:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;// 假设这是一个简单的对象类
class SimpleObject {// 一些属性和方法省略
}public class ParNewCollectorExample {public static void main(String[] args) {// 模拟创建一些对象SimpleObject[] objects = new SimpleObject[100];for (int i = 0; i < objects.length; i++) {objects[i] = new SimpleObject();}// 使用线程池模拟多线程垃圾回收ExecutorService executorService = Executors.newFixedThreadPool(4);for (int i = 0; i < objects.length; i++) {int finalI = i;executorService.submit(() -> {// 这里简单模拟判断对象是否为垃圾(实际更复杂)boolean isGarbage = Math.random() > 0.5;if (isGarbage) {objects[finalI] = null;}});}executorService.shutdown();}
}
  • Parallel Scavenge 收集器:Parallel Scavenge 是 Parallel Scavenge 收集器在新年代的版本,采用复制算法,同时该收集器也是针对新生代的多线程收集器。它的特点是关注系统的吞吐量,通过合理调整参数,如-XX:MaxGCPauseMillis(最大垃圾回收停顿时间)和-XX:GCTimeRatio(垃圾回收时间占总时间的比例),可以让系统在高吞吐量的情况下运行。下面是简单示例,展示如何通过调整参数影响其行为(实际中需在 JVM 启动参数中设置,这里只是概念示意):
// 假设这是一个简单的对象类
class SimpleObject {// 一些属性和方法省略
}public class ParallelScavengeCollectorExample {public static void main(String[] args) {// 模拟创建大量对象for (int i = 0; i < 1000000; i++) {SimpleObject obj = new SimpleObject();// 这里省略对象的使用和可能变为垃圾的过程}// 这里假设通过调整参数(实际在JVM启动时设置)// 如 -XX:MaxGCPauseMillis=100  -XX:GCTimeRatio=99// 来影响垃圾回收策略,以达到高吞吐量}
}

老年代垃圾收集器:

    • Serial Old 收集器:Serial Old 是 Serial 收集器在老年代的版本,采用标记 - 整理算法。由于老年代中的对象存活率较高,复制算法的成本会很高,所以采用标记 - 整理算法更为合适。它同样是单线程工作的,在垃圾回收时会暂停所有用户线程,适用于单 CPU 环境或者对应用停顿时间要求不高的场景。以下是简单模拟其标记 - 整理过程的代码示例(非实际 JVM 中的源代码,仅为示意):
// 假设这是一个简单的对象类
class SimpleObject {// 一些属性和方法省略
}public class SerialOldCollectorExample {public static void main(String[] args) {// 模拟创建一些对象SimpleObject[] objects = new SimpleObject[10];for (int i = 0; i < objects.length; i++) {objects[i] = new SimpleObject();}// 模拟标记 - 整理过程markAndSweep(objects);}private static void markAndSweep(SimpleObject[] objects) {// 简单模拟标记哪些对象是垃圾(实际更复杂)boolean[] isGarbage = new boolean[objects.length];for (int i = 0; i < objects.length; i++) {isGarbage[i] = Math.random() > 0.5;}// 模拟整理过程,将存活对象向一端移动int lastNonGarbageIndex = 0;for (int i = 0; i < objects.length; i++) {if (!isGarbage[i]) {objects[lastNonGarbageIndex++] = objects[i];}}// 清理端边界以外的内存(这里简单设置为null)for (int i = lastNonGarbageIndex; i < objects.length; i++) {objects[i] = null;}}
}
  • Parallel Old 收集器:Parallel Old 是 Parallel Scavenge 收集器在老年代的版本,采用标记 - 整理算法。它是多线程工作的,在多 CPU 环境下可以发挥出较高的性能,与 Parallel Scavenge 收集器配合使用,可以实现高吞吐量的垃圾回收,适用于注重吞吐量的应用场景。下面是简单的多线程标记 - 整理模拟示例(非实际源代码):
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;// 假设这是一个简单的对象类
class SimpleObject {// 一些属性和方法省略
}public class ParallelOldCollectorExample {public static void main(String[] args) {// 模拟创建大量对象SimpleObject[] objects = new SimpleObject[1000];for (int i = 0; i < objects.length; i++) {objects[i] = new SimpleObject();}// 使用线程池模拟多线程标记 - 整理ExecutorService executorService = Executors.newFixedThreadPool(4);boolean[] isGarbage = new boolean[objects.length];for (int i = 0; i < objects.length; i++) {int finalI = i;executorService.submit(() -> {// 简单模拟判断对象是否为垃圾(实际更复杂)isGarbage[finalI] = Math.random() > 0.5;});}executorService.shutdown();// 模拟整理过程,将存活对象向一端移动int lastNonGarbageIndex = 0;for (int i = 0; i < objects.length; i++) {if (!isGarbage[i]) {objects[lastNonGarbageIndex++] = objects[i];}}// 清理端边界以外的内存(这里简单设置为null)for (int i = lastNonGarbageIndex; i < objects.length; i++) {objects[i] = null;}}
}
  • CMS 收集器:CMS(Concurrent Mark Sweep)收集器主要作用于老年代,采用标记 - 清除算法。它的目标是尽量减少垃圾回收时的停顿时间。在垃圾回收过程中,它分为四个阶段:初始标记、并发标记、重新标记和并发清除。初始标记和重新标记阶段需要暂停用户线程,但是这两个阶段的时间相对较短;并发标记和并发清除阶段可以与用户线程并发执行,从而减少了垃圾回收对应用程序的影响。然而,由于它采用标记 - 清除算法,在垃圾回收后会产生内存碎片,当内存碎片过多时,可能会导致在分配大对象时找不到足够的连续内存空间,从而不得不提前触发 Full GC。下面是简单模拟其工作阶段的代码示例(非实际 JVM 中的源代码,仅为示意):
// 假设这是一个简单的对象类
class SimpleObject {// 一些属性和方法省略
}public class CMSCollectorExample {public static void main(String[] args) {// 模拟创建一些对象SimpleObject[] objects = new SimpleObject[100];for (int i = 0; i < objects.length; i++) {objects[i] = new SimpleObject();}// 模拟CMS收集器的工作阶段cmsCollect(objects);}private static void cmsCollect(SimpleObject[] objects) {// 初始标记(简单模拟)boolean[] isGarbage = new boolean[objects.length];for (int i = 0; i < 10; i++) {isGarbage[i] = true;}// 并发标记(这里简单模拟并发,实际是多线程操作)for (int i = 10; i < objects.length; i++) {isGarbage[i] = Math.random() > 0.5;}// 重新标记(简单模拟)for (int i = 0; i < objects.length; i++) {if (Math.random() > 0.9) {isGarbage[i] = true;}}// 并发清除(简单模拟)for (int i = 0; i < objects.length; i++) {if (isGarbage[i]) {objects[i] = null;}}}
}
  • G1 收集器:G1(Garbage - First)收集器较为特殊,它可以同时管理新生代和老年代的垃圾回收。它将堆内存划分为多个大小相等的 Region,每个 Region 可以根据需要扮演新生代或者老年代的角色。在垃圾回收时,G1 收集器会优先回收垃圾最多的 Region,采用复制算法和标记 - 整理算法相结合的方式进行垃圾回收。它可以在有限的时间内尽量获取最大的垃圾回收效率,同时也能较好地控制垃圾回收的停顿时间,适用于大内存、多 CPU 的服务器环境,并且对应用程序的性能影响较小。G1 收集器的实际代码非常复杂,涉及到 JVM 的底层内存管理和多线程调度等,以下是简化的概念示例(非实际源代码):
// 假设这是一个简单的对象类
class SimpleObject {// 一些属性和方法省略
}public class G1CollectorExample {public static void main(String[] args) {// 模拟堆内存划分为多个RegionSimpleObject[][][] regions = new SimpleObject[10][10][10];for (int i = 0; i < 10; i++) {for (int j = 0; j < 10; j++) {for (int k = 0; k < 10; k++) {regions[i][j][k] = new SimpleObject();}}}// 模拟G1收集器优先回收垃圾最多的Regionint maxGarbageRegionIndex = 0;int maxGarbageCount = 0;for (int i = 0; i < 10; i++) {int garbageCount = 0;for (int j = 0; j < 10; j++) {for (int k = 0; k < 10; k++) {if (Math.random() > 0.5) {garbageCount++;}}}if (garbageCount > maxGarbageCount) {maxGarbageCount = garbageCount;maxGarbageRegionIndex = i;}}// 模拟回收垃圾最多的Regionfor (int j = 0; j < 10; j++) {for (int k = 0; k < 10; k++) {regions[maxGarbageRegionIndex][j][k] = null;}}}
}

(三)垃圾收集器的选择与调优策略

  1. 选择合适的垃圾收集器:在选择垃圾收集器时,需要综合考虑应用程序的特点和需求。如果是单 CPU 环境下的小型应用,对响应时间要求不高,可以选择 Serial 收集器;如果是多 CPU 环境下的后台计算任务,追求高吞吐量,可以选择 Parallel 收集器(Parallel Scavenge 和 Parallel Old 的组合);如果是对响应时间要求较高的 Web 应用等,CMS 收集器或者 G1 收集器可能是更好的选择。此外,还需要考虑应用程序的内存使用情况、对象的生命周期等因素。
  2. 垃圾收集器的调优:一旦选择了合适的垃圾收集器,还可以通过调整相关参数来进一步优化其性能。例如,对于 Parallel Scavenge 收集器,可以通过调整-XX:MaxGCPauseMillis和-XX:GCTimeRatio来平衡垃圾回收的停顿时间和吞吐量;对于 G1 收集器,可以调整-XX:G1HeapRegionSize(设置 Region 的大小)、-XX:MaxGCPauseMillis(最大垃圾回收停顿时间)等参数来优化其性能。在进行调优时,需要不断地进行测试和观察,根据应用程序的实际运行情况来确定最佳的参数配置。

通过深入了解垃圾收集器的工作原理、分类特点以及选择调优策略,我们可以更好地优化 JVM 的内存管理,提高 Java 应用程序的性能和稳定性。

相关文章:

深入剖析 JVM 内存模型

前言&#xff1a; 下面分别介绍了新生代和老年代的不同收集器及其相关子类型&#xff0c;并附有示例代码和说明&#xff0c;感兴趣的朋友可以参考一下。 简介&#xff1a; 在 Java 虚拟机&#xff08;JVM&#xff09;的世界里&#xff0c;内存模型是其核心架构之一&#xff0…...

解决DeepSeek-R1模型在Cursor中使用报错的问题

在使用Cursor时&#xff0c;如果你尝试调用DeepSeek-R1模型&#xff0c;可能会遇到以下报错信息&#xff1a; {"error": {"message": "deepseek-reasoner does not support successive user or assistant messages (messages[1] and messages[2] in …...

ASP.NET Core 6.0 如何处理丢失的 Startup.cs 文件

介绍 .NET 6.0 已经发布&#xff0c;ASP.NET Core 6.0 也已发布。其中有不少变化让很多人感到困惑。例如&#xff0c;“谁动了我的奶酪”&#xff0c;它在哪里Startup.cs&#xff1f;在这篇文章中&#xff0c;我将深入研究这个问题&#xff0c;看看它移动到了哪里以及其他变化。…...

Java如何向http/https接口发出请求

用Java发送web请求所用到的包都在java.net下&#xff0c;在具体使用时可以用如下代码&#xff0c;你可以把它封装成一个工具类 import javax.net.ssl.*; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Outpu…...

数据分析 变异系数

目录 变异系数的应用场景包括&#xff1a; 特点&#xff1a; 注意事项&#xff1a; np.nanvar——方差&#xff0c;np.sanstd标准差 简单来讲就是平均值/标准差 变异系数&#xff08;Coefficient of Variation, CV&#xff09;是一种相对量的变异指标&#xff0c;常用于衡…...

利用免费GIS工具箱实现高斯泼溅切片,将 PLY 格式转换为 3dtiles

在地理信息系统&#xff08;GIS&#xff09;和三维数据处理领域&#xff0c;不同数据格式有其独特应用场景与优势。PLY&#xff08;Polygon File Format&#xff09;格式常用于存储多边形网格数据&#xff0c;而 3DTiles 格式在 Web 端三维场景展示等方面表现出色。将 PLY 格式…...

面试-二维数组

应用 快递业务有N个站点&#xff0c;1<N<10000&#xff1b;站点0、站点1可达&#xff0c;记作0-1&#xff1b;如果0-1、1-2&#xff0c;则站点0、站点2可达&#xff0c;记作0-2&#xff1b;s[i][j]1表示i-j可达&#xff0c;反之s[i][j]0表示i-j不可达&#xff1b;s[i][j…...

如何使用 findIndex() 方法查找数组中的第一个匹配元素的索引?

使用 findIndex() 方法查找数组中第一个匹配元素的索引 目录 简介findIndex() 方法概述如何使用 findIndex() 查找第一个匹配元素的索引 基本用法使用箭头函数和回调函数 实际项目中的代码示例 示例 1&#xff1a;查找第一个符合条件的用户索引示例 2&#xff1a;查找第一个符…...

5. 马科维茨资产组合模型+政策意图AI金融智能体(Qwen-Max)增强方案(理论+Python实战)

目录 0. 承前1. AI金融智能体1.1 What is AI金融智能体1.2 Why is AI金融智能体1.3 How to AI金融智能体 2. 数据要素&计算流程2.1 参数集设置2.2 数据获取&预处理2.3 收益率计算2.4 因子构建与预期收益率计算2.5 协方差矩阵计算2.6 投资组合优化2.7 持仓筛选2.8 AI金融…...

Centos类型服务器等保测评整/etc/pam.d/system-auth

修改服务器配置文件/etc/pam.d/system-auth&#xff0c;但是&#xff0c;把一下配置放在password的配置第一行才会生效 执行命令&#xff1a;配置口令要求&#xff1a;大小写字母、数字、特殊字符组合、至少8位&#xff0c;包括强制设置root口令&#xff01; sed -i 14a pas…...

从工厂到桌面:3D打印制造潮玩手办

传统潮玩手办的制造过程复杂且成本高昂。从设计到成品&#xff0c;需要经过多道工序&#xff0c;包括手工建模、模具制作、注塑成型等。这一过程不仅耗时耗力&#xff0c;而且难以满足消费者日益增长的个性化需求。此外&#xff0c;传统制造方式对于小批量生产或定制化产品的经…...

Java高频面试之SE-16

hello啊&#xff0c;各位观众姥爷们&#xff01;&#xff01;&#xff01;本牛马baby今天又来了&#xff01;哈哈哈哈哈嗝&#x1f436; Java中异常的处理方式有哪些&#xff1f; 在 Java 中&#xff0c;异常的处理方式主要有以下几种&#xff1a; 1. 使用 try-catch 语句 …...

三分钟简单了解一些HTML的标签和语法_01

1.图片建议建立一个文件夹如下图所示 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"keywords"><title>魔神羽落</title><style>.testone{background-color: #ff53e…...

缓存-Redis-数据结构-redis哪些数据结构是跳表实现的?

在 Redis 中&#xff0c;跳表&#xff08;Skip List&#xff09; 被用于实现 有序集合&#xff08;Sorted Set&#xff09; 数据结构。以下是对此实现的详细解释&#xff1a; Redis中的有序集合&#xff08;Sorted Set&#xff09; 有序集合&#xff08;Sorted Set&#xff0…...

Linux 系统错误处理简介

Linux 系统错误处理简介 1. errno&#xff1a;错误代码的载体2. strerror()&#xff1a;错误信息的翻译官3. perror()&#xff1a;便捷的错误信息输出4. 系统调用与库函数的区别5. 错误处理的最佳实践 在 C/C 程序开发中&#xff0c;我们经常需要处理各种错误情况 Linux 系统提…...

逐笔成交逐笔委托Level2高频数据下载和分析:20250122

逐笔委托逐笔成交下载 链接: https://pan.baidu.com/s/1WP6eGLip3gAbt7yFKg4XqA?pwd7qtx 提取码: 7qtx Level2逐笔成交逐笔委托数据分享下载 通过Level2逐笔成交和逐笔委托这种每一笔的毫秒级别的数据可以分析出很多有用的点&#xff0c;包括主力意图&#xff0c;虚假动作&…...

第18个项目:微信开发入门:获取access_token的Python源码

源码下载地址:https://download.csdn.net/download/mosquito_lover1/90301829 功能特点: 输入AppID和AppSecret,点击按钮后异步获取access_token 1、自动保存功能: 当用户输入或修改 AppID 和 AppSecret 时自动保存 获取到新的 access_token 时自动保存 所有数据都保存在…...

如何将自己本地项目开源到github上?

环境&#xff1a; LLMB项目 问题描述&#xff1a; 如何将自己本地项目开源到github上&#xff1f; 解决方案&#xff1a; 步骤 1: 准备本地项目 确保项目整洁 确认所有的文件都在合适的位置&#xff0c;并且项目的 README.md 文件已经完善。检查是否有敏感信息&#xff0…...

Windows远程连接Docker服务

问题背景 本地开发了一个SpringBoot项目&#xff0c;想通过Docker部署起来&#xff0c;我本地是Window11系统&#xff0c;由于某些原因不能虚拟化并且未安装Docker-Desktop&#xff0c;所以我在想有没有办法本地不需要虚拟化也不需要安装Docker-Desktop来实现支持Docker命令远…...

在Qt中实现点击一个界面上的按钮弹窗到另一个界面

文章目录 步骤 1&#xff1a;创建新窗口类步骤 2&#xff1a;设计窗口的 UI步骤 3&#xff1a;设计响应函数 以下是一个完整的示例&#xff0c;展示在Qt中如何实现在一个窗口中通过点击按钮弹出一个新窗口。 步骤 1&#xff1a;创建新窗口类 假设你要创建一个名为 WelcomeWidg…...

嵌入式知识点总结 ARM体系与架构 专题提升(一)-硬件基础

嵌入式知识点总结 ARM体系与架构 专题提升(一)-硬件基础 目录 1.NAND FLASH 和NOR FLASH异同 ? 2.CPU,MPU,MCU,SOC,SOPC联系与差别? 3.什么是交叉编译&#xff1f; 4.为什么要交叉编译&#xff1f; 5.描述一下嵌入式基于ROM的运行方式和基于RAM的运行方式有什么区别? 1…...

全氟醚橡胶发展前景:高性能密封材料的璀璨之星

在当今科技飞速发展的时代&#xff0c;各类高性能材料不断涌现&#xff0c;全氟醚橡胶便是其中一颗闪耀的明珠。它以其卓越的性能和广泛的应用领域&#xff0c;在众多关键行业中发挥着不可或缺的作用&#xff0c;展现出巨大的市场潜力和发展前景。 一、引言 全氟醚橡胶&#…...

Android程序中使用FFmpeg库

目录 前言 一、环境 二、创建APP 三. 添加FFmpeg库文件到app中 1. 复制ffmpeg头文件和so库到app中 2. 修改CMakeLists.txt文件内容. 3. 修改ffmpeglib.cpp 文件内容 4. 修改NativeLib.kt 文件添加方法和加载库 5. 调用 四. 增加解析视频文件信息功能 总结 前言 前面…...

Spring 依赖注入详解:创建 Bean 和注入依赖是一回事吗?

1. 什么是依赖注入&#xff08;Dependency Injection&#xff0c;DI&#xff09;&#xff1f; 依赖注入 是 Spring IoC&#xff08;控制反转&#xff09;容器的核心功能。它的目标是将对象的依赖&#xff08;如其他对象或配置&#xff09;从对象本身中剥离&#xff0c;由容器负…...

【动态规划】落花人独立,微雨燕双飞 - 8. 01背包问题

本篇博客给大家带来的是01背包问题之动态规划解法技巧. &#x1f40e;文章专栏: 动态规划 &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅&#x1f680; 要开心要快乐顺便…...

浅说树上差分——点差分

我们前面也学过差分&#xff0c;现在的话我们就把他放到树上来做。因为这是树&#xff0c;所以会有点和边之分&#xff0c;所以树上差分也会分为 点差分 和 边差分 。 引入 树上差分其实和线性差分没有什么区别&#xff0c;只不过是放到了树上的两点&#xff0c;而他们之间的…...

All in大模型!智能座舱语音交互决胜2025

大模型加速上车&#xff0c;AI智能座舱竞争更显白热化。 诚然&#xff0c;在语言大模型为核心的多模态能力加持下&#xff0c;智能语音助理能够理解复杂的语言指令&#xff0c;实现知识问答、文本生成等&#xff0c;以及根据上下文进行逻辑推理&#xff0c;提供更智能、准确的…...

windows git bash 使用zsh 并集成 oh my zsh

参考了 这篇文章 进行配置&#xff0c;记录了自己的踩坑过程&#xff0c;并增加了 zsh-autosuggestions 插件的集成。 主要步骤&#xff1a; 1. git bash 这个就不说了&#xff0c;自己去网上下&#xff0c;windows 使用git时候 命令行基本都有它。 主要也是用它不方便&…...

Git进阶笔记系列(01)Git核心架构原理 | 常用命令实战集合

读书笔记&#xff1a;卓越强迫症强大恐惧症&#xff0c;在亲子家庭、职场关系里尤其是纵向关系模型里&#xff0c;这两种状态很容易无缝衔接。尤其父母对子女、领导对下属&#xff0c;都有望子成龙、强将无弱兵的期望&#xff0c;然而在你的面前&#xff0c;他们才是永远强大的…...

IDEA导入Maven工程不识别pom.xml

0 现象 把阿里 sentinel 项目下载本地后&#xff0c;IDEA 中却没显示 maven 工具栏。 1 右键Maven Projects 点击IDEA右侧边栏的Maven Projects&#xff0c;再点击&#xff1a; 在出现的选择框中选择指定的未被识别的pom.xml即可&#xff1a; 2 Add as maven project 右键p…...