首页 > 系统相关 >深入剖析 JVM 内存模型

深入剖析 JVM 内存模型

时间:2025-01-21 15:57:12浏览次数:3  
标签:收集器 int 内存 剖析 objects 垃圾 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) {
        // 模拟堆内存划分为多个Region
        SimpleObject[][][] 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收集器优先回收垃圾最多的Region
        int 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;
            }
        }
        // 模拟回收垃圾最多的Region
        for (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 应用程序的性能和稳定性。

标签:收集器,int,内存,剖析,objects,垃圾,JVM,加载
From: https://blog.csdn.net/xxiaobaibaibai/article/details/145284941

相关文章

  • 我们开发了一个强大的 shell 脚本用于收集系统内存信息
    在Linux系统管理中,监控内存使用情况至关重要。我们开发了一个强大的shell脚本用于收集系统内存信息。该脚本以/bin/bash为解释器,首先创建/var/log/meminfo_collector.log日志文件,若创建失败则记录错误并终止,以方便后续追踪调试。接着根据当前日期在/root/kylin_s......
  • 【K8S系列】K8s 领域深度剖析:年度技术、工具与实战总结 (思维导图-java架构)
    创建一个关于Kubernetes(简称K8s)领域的深度剖析年度总结的思维导图,特别是针对Java架构师的需求,可以帮助梳理和理解过去一年中重要的技术进展、工具以及实战经验。下面是一个基于文本的思维导图结构建议,你可以根据这个结构使用任何思维导图软件来创建你的图形化版本。Kuberne......
  • PM部分成员模型:深度剖析角球预测的方案
    在角球的世界里,预测比赛结果和球员表现一直是球迷、教练和分析师们关注的焦点。传统的分析方法往往存在一定的局限性,而新的统计模型——部分成员模型(PartialMembershipModel,简称PM)为角球预测带来了新的曙光。今天,就让我们深入探讨一下这个模型是如何在角球领域发挥作用的。P......
  • 内存字符串有关问题
    问题一:#include<iostream>#include<cstdint>#include<cstring>usingnamespacestd;typedefstructdata{charhwid[4];charsip[4];charrev[4];}Data;intmain(){Datastdata;memset(&stdata,0,sizeof(stdata));......
  • Kubernetes 中 JVM 监控实战:Prometheus + JMX Exporter 全解析
    背景skywalking采集的jvm要自己在页面选择endpoint来查看,不合符开发者使用习惯前置知识prometheus-operator的四个CRD作用Prometheus:由Operator依据一个自定义资源kind:Prometheus类型中,所描述的内容而部署的PrometheusServer集群,可以将这个自定义资源看作是一......
  • 前端人必知必会:Node.js进程深度剖析
    文章目录一、Node.js进程初相识二、Node.js进程核心概念2.1进程的基本定义2.2与线程的爱恨情仇2.3进程在Node.js架构中的角色三、Node.js进程相关模块3.1process模块:进程掌控者3.2child_process模块:子进程创建利器3.3cluster模块:多核CPU的完美搭档四、......
  • Java类加载机制与JVM运行时数据区各逻辑内存区域与JDK的版本相关差异浅谈
    Java类加载机制与JVM运行时数据区各逻辑内存区域与JDK的版本相关差异浅谈 【摘要】JVM(JavaVirtualMachine)作为Java研发人员工作的每天都会接触到的虚拟机,其运行机制与底层原理想必大家都略知一二,今天我将从初学者的角度出发,结合甲骨文官方的技术文档,对部分Java虚拟机的相关......
  • 02内存结构篇(D2_剖析运行数据区)
    目录学习前言一、程序计数器1.作用2.存储的数据3.异常三、Java虚拟机栈1.栈帧1.1.局部变量表存储内容存储容量其他1.2.操作数栈作用存储内容存储容量1.3.动态连接1.4.方法返回1.5.附加信息2.栈异常四、本地方法栈1.本地方法1.1.什么是本地......
  • 02内存结构篇(D1_自动内存管理)
    目录一、内存管理1.C/C++程序员2.Java程序员二、运行时数据区1.程序计数器2.Java虚拟机栈3.本地方法栈4.Java堆5.方法区运行时常量池三、Hotspot运行时数据区四、分配JVM内存空间分配堆的大小分配方法区的大小分配线程空间的大小一、内存管理1.C/C......
  • Java几种常见的内存溢出及其解决方法
    java.lang.OutOfMemoryError:Javaheapspacejava.lang.OutOfMemoryError:GCoverheadlimitexceededjava.lang.OutOfMemoryError:Unabletocreatenewnativethreadjava.lang.StackOverflowError微信扫码查看:JAVA基础之内存机制.pptx......