JVM内存模型 + GC系统 + 类加载

【Java面试题汇总】JVM篇(2026版) JVM内存模型、双亲委派模型、类加载机制、内存溢出、垃圾回收机制、内存泄漏、垃圾回收流程、垃圾回收器、G1、CMS、JVM调优 阅读详情

Java文件到类加载完成流程

Java源文件  经过  javac编译  变成    字节码 通过 类加载器 加载

‌1. 类加载器类型‌

加载器加载路径实现语言父加载器
Bootstrap$JAVA_HOME/lib/*.jarC++null(顶级)
ExtClassLoader$JAVA_HOME/lib/ext/*.jarJavaBootstrap
AppClassLoaderclasspath或模块路径JavaExtClassLoader
自定义类加载器用户指定路径JavaAppClassLoader

2. 工作流程(双亲委派模型)‌

‌1. 加载阶段流程图‌

| 自定义加载器 │ → │ AppClassLoader│ → │ ExtClassLoader│ → Bootstrap │

| 委托父类优先加载 │ 父类加载失败时尝试 │ 

都加载不到,会抛出‌ ClassNotFoundException ‌异常

‌2. 步骤分解

  1. ‌委托阶段‌

    • 类加载器收到请求后,优先委派父加载器处理
    • Bootstrap是最顶层父加载器(原生实现‌:由C/C++编写,而非Java语言实现,因此在Java代码中表现为null  ——》 实际‌为null的概念)
  2. ‌检查阶段‌

    • 父加载器检查是否已加载过该类(findLoadedClass())
    • 已加载则直接返回Class对象
  3. ‌加载阶段‌

    • 父加载器无法加载时,子加载器尝试从自己的路径加载
    • 自定义加载器需重写findClass()方法
  4. ‌定义阶段‌

    • 将字节码转换为Class<?>对象(defineClass())
    • 验证字节码合法性(如魔数0xCAFEBABE)

3. 关键原理‌

‌1. 双亲委派机制‌

  • ‌作用‌:避免重复加载 + 防止核心类被篡改
破坏双亲委派机制的实现方式

双亲委派机制是Java类加载器的核心机制,但在某些特殊场景下需要破坏这种机制。以下是几种常见的破坏方式及实现案例:

1. 自定义类加载器重写loadClass方法

这是最直接的破坏方式,通过覆盖默认的loadClass实现来绕过双亲委派。

java_自定义类加载器


import java.io.*;

public class CustomClassLoader extends ClassLoader {
    private String classPath;
    
    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }
    
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 先检查是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 直接自己加载,不委派给父加载器
                c = findClass(name);
            } catch (ClassNotFoundException e) {
                // 如果找不到,再委派给父加载器
                c = super.loadClass(name, resolve);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }
    
    private byte[] getClassData(String className) {
        String path = classPath + File.separatorChar + 
                     className.replace('.', File.separatorChar) + ".class";
        try (InputStream is = new FileInputStream(path);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = is.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            return null;
        }
    }
    
    public static void main(String[] args) throws Exception {
        CustomClassLoader loader = new CustomClassLoader("/path/to/classes");
        Class<?> clazz = loader.loadClass("com.example.TestClass");
        Object instance = clazz.newInstance();
        System.out.println("Loaded by: " + instance.getClass().getClassLoader());
    }
}
  1. 自定义类加载器继承ClassLoader

  2. 重写loadClass方法实现自己的加载逻辑

  3. 通过findClass方法从指定路径加载类字节码

  4. 示例展示了如何绕过双亲委派直接加载类

2. 线程上下文类加载器(Thread Context ClassLoader)

这是Java SPI机制常用的破坏双亲委派的方式。

java_线程上下文类加载器


public class ContextClassLoaderDemo {
    public static void main(String[] args) {
        // 获取当前线程的上下文类加载器
        ClassLoader original = Thread.currentThread().getContextClassLoader();
        
        try {
            // 设置自定义类加载器为上下文类加载器
            ClassLoader customLoader = new CustomClassLoader("/path/to/jars");
            Thread.currentThread().setContextClassLoader(customLoader);
            
            // 现在SPI实现类将通过自定义加载器加载
            ServiceLoader<SomeService> loader = ServiceLoader.load(SomeService.class);
            for (SomeService service : loader) {
                service.execute();
            }
        } finally {
            // 恢复原始类加载器
            Thread.currentThread().setContextClassLoader(original);
        }
    }
    
    interface SomeService {
        void execute();
    }
}
  1. 通过Thread.currentThread().setContextClassLoader设置自定义加载器

  2. ServiceLoader.load会使用当前线程的上下文类加载器

  3. 常用于JDBC、JNDI等SPI机制的实现

3. OSGi模块化系统的类加载机制

OSGi每个Bundle都有自己的类加载器,形成网状结构而非树状结构。

4. Tomcat的WebappClassLoader

Tomcat为每个Web应用创建独立的类加载器,优先加载自己WEB-INF/classes和WEB-INF/lib下的类。

破坏双亲委派的典型场景

  1. ‌热部署‌:如Tomcat、Jetty等Web容器

  2. ‌模块化系统‌:如OSGi、Jigsaw

  3. ‌SPI服务发现机制‌:如JDBC驱动加载

  4. ‌代码隔离‌:不同应用使用相同类库的不同版本

注意:破坏双亲委派机制需要谨慎使用,可能导致类冲突、内存泄漏等问题。

‌2. 类唯一性判定‌

  • ‌规则‌:相同类加载器 + 相同全限定名
  • 示例:

    // 不同加载器加载的String类被视为不同类 Class<?> cls1 = Class.forName("java.lang.String"); Class<?> cls2 = new CustomLoader().loadClass("java.lang.String");

  • System.out.println(cls1 == cls2); // false

‌3. 自定义加载器实现‌

public class MyClassLoader extends ClassLoader {

private String classPath;

@Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] data = loadClassData(name); // 从自定义路径读取字节码

return defineClass(name, data, 0, data.length);

}

}

4. 类加载过程‌

  1. ‌Loading‌

    • 通过类加载器查找并读取.class文件
    • 将字节码转换为JVM内部数据结构
    • 在方法区创建对应的Class对象

  2. ‌Linking‌

    • ‌验证‌:文件格式、元数据、字节码、符号引用等,符合jvm规范,确保不会危害jvm安全
    • ‌准备‌:为(类)静态变量分配内存并赋默认值(如int=0)
    • ‌解析‌:将符号引用转为直接引用,涉及常量池中的类、方法、字段等
  3. ‌Initialization‌

    • 执行<clinit>()方法(静态变量赋值+静态代码块)
    • 线程安全且仅执行一次

‌加载的东西最终去向哪里

JVM 运行时区域大致分为程序计数器、虚拟机栈、本地方法栈、堆、方法区(jkd1.8废弃)五个部分

  • 程序计数器(PC 寄存器、计数器)

    程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。

    在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都已自己的程序计数器。

    可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

    倘若执行的是 native 方法,则程序计数器中为空

  • Java 虚拟机栈(JVM Stacks)

    虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。

    不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

    每个栈帧主要包含的内容如下:

    • 局部变量表

      存储着 java 基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用

      注意:这里的基本数据类型指的是方法内的局部变量

      局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。

    • 操作数栈
    • 动态连接
    • 方法返回地址

    虚拟机栈可能会抛出两种异常:

    • 栈溢出(StackOverFlowError):

      若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常

    • 内存溢出(OutOfMemoryError):

      若虚拟机栈的容量允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OOM 异常

  • 本地方法栈(Native Method Stacks)

    本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。

    本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。

    本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

    虚拟机栈和本地方法栈的主要区别:

    • 虚拟机栈执行的是 java 方法
    • 本地方法栈执行的是 native 方法
  • Java 堆(Java Heap)

    Java 堆中是 JVM 管理的最大一块内存空间。主要存放对象实例。

    Java 堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。

    Java 堆的分区:

    • 在 jdk1.8 之前,分为新生代、老年代、永久代
    • 在 jdk1.8 及之后,只分为新生代、老年代

      永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代

    Java 堆内存大小:

    • 堆内存大小 = 新生代 + 老年代(新生代占堆空间的1/3、老年代占堆空间2/3)
    • 既可以是固定大小的,也可以是可扩展的(通过参数 -Xmx 和 -Xms 设定)
    • 如果堆无法扩展或者无法分配内存时报 OOM

    主要存储的内容是:

    • 对象实例
    • 类初始化生成的对象
    • 基本数据类型的数组也是对象实例
    • 字符串常量池

      字符串常量池原本存放在方法区,jdk8 开始放置于堆中

      字符串常量池存储的是 string 对象的直接引用,而不是直接存放的对象,是一张 string table

    • 静态变量

      • static 修饰的静态变量,jdk8 时从方法区迁移至堆中
      • 线程分配缓冲区(Thread Local Allocation Buffer)

        线程私有,但是不影响 java 堆的共性

        增加线程分配缓冲区是为了提升对象分配时的效率

    堆和栈的区别:

    • 管理方式,堆需要GC,栈自动释放
    • 大小不同,堆比栈大
    • 碎片相关:栈产生的碎片远小于堆,因为GC不是实时的
    • 分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配
    • 效率:栈的效率比堆高
  • 方法区(逻辑上)

    方法区是 JVM 的一个规范,所有虚拟机必须要遵守的。常见的 JVM 虚拟机有 Hotspot 、 JRockit(Oracle)、J9(IBM)

    方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区

    各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

    方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭 JVM 就会释放这个区域的内存。

    • Java8 以前是放在 JVM 内存中的,由堆空间中的永久代实现,受 JVM 内存大小参数限制
    • Java8 移除了永久代和方法区,引入了元空间

    拓展:

    JDK版本方法区的实现运行时常量池所在的位置
    JDK6PermGen space(永久代)PermGen space(永久代)
    JDK7PermGen space(永久代)Heap(堆)
    JDK8Metaspace(元空间)Heap(堆)
  • 元空间(元数据区、Metaspace)

    元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。

    元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受 JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM

    元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

    • 类元信息(Class)

      类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表

      常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中

    • 运行时常量池(Runtime Constant Pool)

      运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些

      运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法

  • 直接内存(Direct Memory)

    直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。

    常见于 NIO 操作时,用于数据缓冲区(比如 ByteBuffer 使用的就是直接内存)。

    分配、回收成本较高,但读写性能高。

    直接内存不受 JVM 内存回收管理(直接内存的分配和释放是 Java 会通过 UnSafe 对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。

Java 程序内存 = JVM 内存 + 本地内存

  • JVM 内存(JVM 虚拟机数据区)

    Java 虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机(JVM)内存。

    JVM 内存受虚拟机内存大小的参数控制,当大小超过参数设置的大小时会报 OOM

  • 本地内存(元空间 + 直接内存)

    对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。

    本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。

    虽然不受参数的限制,如果所占内存超过物理内存,仍然会报 OOM

堆外内存

  • 直接内存

    直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。

    可通过 -XX:MaxDirectMemorySize 调整大小,默认和 Java 堆最大值一样

    内存不足时抛出OutOf-MemoryError或 者OutOfMemoryError:Direct buffer memory;

  • 线程堆栈

    可通过 -Xss 调整大小

    内存不足时抛出

    • StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)
    • OutOfMemoryError(如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)
  • Socket 缓存区

    每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。

    如果无法分配,可能会抛出 IOException:Too many open files异常

  • JNI 代码

    如果代码中使用了 JNI 调用本地库,那本地库使用的内存也不在堆中,而是占用 Java 虚拟机的本地方法栈和本地内存

  • 虚拟机和垃圾收集器

    虚拟机、垃圾收集器的工作也是要消耗一定数量的内存

GC

新生代、MinorGC(Young GC)

  • 主要是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。
  • 新生代又分为 Eden、S0、S1(SurvivorFrom、SurvivorTo)三个区:

    • Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。

      当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

    • SurvivorFrom 区:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
    • SurvivorTo 区:保留了一次 MinorGC 过程中的幸存者。

    Eden 和 S0,S1 区的比例为 8 : 1 : 1

    幸存者 S0,S1 区:复制之后发生交换,谁是空的,谁就是 SurvivorTo 区

    JVM 每次只会使用 eden 和其中一块 survivor 来为对象服务,所以无论什么时候,都会有一块 survivor 是空的,因此新生代实际可用空间只有 90%

  • 当 JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁。

MinorGC

  • MinorGC 的过程(采用复制算法):

    1. 首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,一般是 15,则赋值到老年代区)
    2. 同时把这些对象的年龄 + 1(如果 ServicorTo 不够位置了就放到老年区)
    3. 然后,清空 Eden 和 ServicorFrom 中的对象;
    4. 最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。
  • Minor GC 触发机制:

    当年轻代满(指的是 Eden 满,Survivor 满不会引发 GC)时就会触发 Minor GC(通过复制算法回收垃圾)

  • 对象年龄(Age)计数器

    虚拟机给每个对象定义了一个对象年龄(Age)计数器。

    如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。

    对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。

    对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值) 来设置。

老年代、MajorGC(Old GC)

  • 老年代的对象比较稳定,所以 MajorGC 不会频繁执行。
  • 在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。

    当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

  • MajorGC 采用标记-清除算法:

    1. 首先扫描一次所有老年代,标记出存活的对象
    2. 然后回收没有标记的对象。

    MajorGC 的耗时比较长(速度一般会比 Minor GC 慢10倍以上,STW 的时间更长),因为要扫描再回收。

    MajorGC 会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。

  • 当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

永久代、元数据区(元空间)、常量池

  • 是 JDK7 及之前, HotSpot 虚拟机基于 JVM 规范对方法区的一个落地实现,其他虚拟机如 JRockit(Oracle)、J9(IBM) 有方法区 ,但是没有永久代。

    在 JDK1.8 已经被移除,取而代之的是元数据区(元空间)

  • 内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域。

    和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。

    所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

元数据区(元空间、Metaspace)

  • 元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。
  • 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

    默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

    • -XX:MetaspaceSize (初始空间大小):达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整

      如果释放了大量的空间,就适当降低该值;

      如果释放了很少的空间,那么在不超过 MaxMetaspaceSize时,适当提高该值。

    • -XX:MaxMetaspaceSize(最大空间)默认是没有限制的。

    除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

    • -XX:MinMetaspaceFreeRatio :在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;
    • -XX:MaxMetaspaceFreeRatio :在GC之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;

    类的元数据放入本地内存中,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由虚拟机的 MaxPermSize 控制,而由系统的实际可用空间来控制。

元空间替换永久代的原因分析:

  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 通常会使用 PermSize 和 MaxPermSize 设置永久代的大小就决定了永久代的上限,但是类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

    当使用元空间时,可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。

  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • Oracle 可能会将HotSpot 与 JRockit 合二为一。

类常量池、运行时常量池、字符串常量池

  • 类常量池

    在类编译过程中,会把类元信息存放到元空间(方法区),类元信息其中一部分便是类常量池

    主要存放字面量(字面量一部分便是文本字符)和符号引用

  • 运行时常量池

    在类加载时,会将字面量和符号引用解析为直接引用存储在运行时常量池

    (文本字符会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池)

    • 在 JDK6,运行时常量池 存在于 方法区
    • 在 JDK7,运行时常量池 存在于 Java 堆
  • 字符串常量池

    存储的是字符串对象的引用,而不是字符串本身

    字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中(JDK8 时,方法区就是元空间)

拓展

  • 字面量

    java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:

    int a=1; // 这个1便是字面量
    String b="iloveu"; // iloveu便是字面量
  • 符号引用

    由于在编译过程中并不知道每个类的地址,因为可能这个类还未加载,所以如果在一个类中引用了另一个类,被引用的类的全限定类名会作为符号引用,在类加载完后用这个符号引用去获取它的内存地址。

    比如:com.javabc.Solution 类中引用了 com.javabc.Quest,那么 com.javabc.Quest 作为符号引用就会存到类常量池,等类加载完后,就可以拿着这个引用去元空间找此类的内存地址

Minor GC、Major GC、Full GC 的区别

  • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC ):只是老年代的垃圾收集
  • 整堆收集(Full GC):收集整个 java 堆(young gen + old gen)和方法区的垃圾收集

Full GC 触发机制:

  • 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
  • 由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • 当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载

堆空间分成不同区的原因

  • 堆空间分为新生代和老年代的原因

    根据对象存活的时间,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。

  • 新生代分为了 eden、Survivor 区的原因

    为了更好的管理堆内存中的对象,方便GC算法(复制算法)来进行垃圾回收。

    如果没有 Survivor 区,那么 Eden 每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发 Full GC,而 Full GC 是非常耗时的。

    将 Eden 区满了的对象,添加到 Survivor 区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即 Survivor 相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。

  • 新生代的 Survivor 区又分为 s0 和 s1 区的原因:

    分两个区的好处就是解决内存碎片化。

    为什么一个 Survivor 区不行?

    假设现在只有一个survivor区,模拟一下流程:

    新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,此时进行 Minor GC,Eden和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

  • GC 优化的本质,也是为什么分代的原因:减少GC次数和GC时间,避免全区扫描。

堆不是对象存储的唯一选择(逃逸分析)

如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样无需在堆上分配内存。也无须进行垃圾回收了。

逃逸分析概述: 一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部引用,则认为没有发生逃逸
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。

System.gc()

  • GC(Garbage Collection)垃圾回收。

    System.gc() 是用 Java,C#和许多其他流行的高级编程语言提供的API。

    当它被调用时,它将尽最大努力从内存中清除垃圾(即未被引用的对象)。

  • 在默认情况下,通过 System.gc() 或者Runtime.getRuntime().gc() 的调用,会显式触发 Full GC(完整的 GC 事件),对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
  • 在 GC 完成之前,整个 JVM 将冻结(即正在运行的所有服务将被暂停),通常完整的 GC 需要很长时间才能完成。

    因此在不合适的时间运行 GC,将导致不良的用户体验,甚至是崩溃。

    JVM 具有复杂的算法,该算法始终在后台运行,进行所有计算以及有关何时触发 GC 的计算。当显式调用 System.gc() 调用时,所有这些计算都将被抛掉。

  • system.gc() 调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)
  • System.gc() 可以从应用程序堆栈的各个部分调用:

    • 开发的应用程序可以显式的调用 System.gc() 方法
    • System.gc() 也可以由第三方库,框架触发
    • 可以由外部工具(如 VisualVM)通过使用 JMX 触发
    • 如果应用程序使用了RMI,RMI会定期调用 System.gc()
  • GC 操作应该由 JVM 自行控制,在绝大部分的场景都不建议程序员手动写代码显式进行 System.gc() 操作。

    但是也不排除其中个别例外:

    在开发多个微服务时,每个服务都有多个备份节点。在非业务高峰时段,可以从微服务-负载均衡的节点池中取出其中一个 JVM 实例。然后通过该 JVM 上的 JMX 显式触发 System.gc() 调用,一旦 GC 事件完成并且从内存中清除了垃圾,将该 JVM 放回到微服务-负载均衡的节点池中。

    当然这个过程需要很好的微服务管理及服务发布机制配合,这样既能保证 JVM 垃圾内存的有效清理,又不影响业务的正常运行。

如何检测应用程序正在进行 System.gc()?

  • System.gc() 可以从多个渠道进行的调用,而不仅仅是从应用程序源代码进行的调用。因此,搜索应用程序代码System.gc() 字符串,不足以知道 GC 是否正在被调用。
  • 通过 GC 日志可以检测应用程序是否正在进行垃圾回收

    // java 8 启动 GC 日志:-XX:+PrintGCDetails -Xloggc:<gc-log-file-path>
    -XX:+PrintGCDetails -Xloggc:/opt/tmp/myapp-gc.log
    
    // java 9 启动 GC 日志:-Xlog:gc*:file=<gc-log-file-path>
    -Xlog:gc*:file=/opt/tmp/myapp-gc.log
  • 建议始终在所有生产服务器中始终启用 GC 日志,因为它有助于排除故障并优化应用程序性能。

    启用GC日志只会增加微不足道的开销。

    还可以将 GC 日志上传到垃圾收集日志分析器工具,例如GCeasy,HP JMeter等。这些工具将生成丰富的垃圾收集分析报告。

如何禁止GC显式调用或调整调用GC的频率?

如果就是想避免程序员显式调用GC,避免不成熟的程序员在不合适时间调用GC,避免人为造成的GC崩溃,可以通过如下方法:

  • 搜索和替换

    在代码库中搜索 System.gc() 和 Runtime.getRuntime().gc()

    如果看到匹配项,则将其删除。但是这种方法无法避免第三方库、框架或通过外部源进行调用。

  • 通过JVM参数强制禁止

    通过传递 JVM 参数 -XX:+DisableExplicitGC 来强制禁止显式调用。

    这种方式强制、有效,应用程序内的任何 GC 显式代码调用 System.gc() 都将被禁止生效。

    JVM 自身的 GC 策略不受此参数影响,只禁止人为的触发 GC。

  • RMI

    如果应用程序正在使用 RMI,则可以控制 GC 调用的频率 。启动应用程序时,可以使用以下JVM参数配置该频率:

    • -Dsun.rmi.dgc.server.gcInterval=n
    • -Dsun.rmi.dgc.client.gcInterval=n

    这些属性的默认值在

    • JDK 1.4.2 和 5.0 是 60000毫秒(即60秒)
    • JDK 6 和更高版本是 3600000毫秒(即60分钟)

    如果应用主机内存资源非常富余,可以将这些属性设置为很高的值,以便可以将GC带来的对应用程序的影响最小化。这也是应用程序性能优化的一种方式之一。

STW(Stop The World)事件

stop-the-world,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。

可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿。

  • 分析工作必须在一个能确保一致性的快照中进行
  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以需要减少 STW 的发生。

STW 事件和采用哪款 GC 无关,所有的 GC 都有这个事件。哪怕是 G1 也不能完全避免 Stop-the-world 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

开发中除非特殊情况,不要用 system.gc() 进行手动 GC,会导致 stop-the-world 的发生。

GC 常用算法

  • 分代收集算法(现在的虚拟机垃圾收集大多采用这种方式)

    它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。

    新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以使用的是复制算法。

    老年代里的对象存活率较高,没有额外的空间进行分配担保,所以使用的是标记-整理 或者 标记-清除。

  • 标记-清除算法

    每个对象都会存储一个标记位,记录对象的状态(活着或是死亡)。

    标记-清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

    优点是可以避免内存碎片。

  • 标记-压缩(标记-整理)算法

    标记-压缩法是标记-清除法的一个改进版,和标记清除算法基本相同。

    不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩(整理),然后把剩下的所有对象全部清除,这样就可以解决内存碎片问题。

  • 复制算法

    复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。

    当有效内存空间耗尽时,JVM 将暂停程序运行,开启复制算法 GC 线程。接下来 GC 线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC 线程将更新存活对象的内存引用地址指向新的内存地址。

    此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。

    复制算法不会产生内存碎片。

Snipaste_2023-02-02_15-35-32.jpg

直接内存(Direct Memory)详解

参考:JVM 直接内存

文件的读写过程

  • 传统 io 方式

    Java 本身不具备磁盘的读写能力,要想实现磁盘读写,必须调用操作系统提供的函数(即本地方法)。在这里 CPU 的状态改变从用户态(Java)切换到内核态(system)【调用系统提供的函数后】。

    内存这边也会有一些相关的操作,当切换到内核态以后,就可以由 CPU 的函数,去真正读取磁盘文件的内容,在内核状态时,读取内容后,会在操作系统内存中划出一块儿缓冲区,其称之为系统缓冲区,磁盘的内容先读入到系统缓冲区中(分次进行读取);系统的缓冲区是不能被 Java 代码直接操作的,所以 Java 会先在堆内存中分配一块儿 Java 的缓冲区,即代码中的 new byte[大小],Java 的代码要能访问到刚才读取的那个流中的数据,必须先从系统缓冲区的数据间接读入到 Java 缓冲区,然后 CPU 的状态又切换到用户态了,然后再去调用 Java 的那个输出流的写入操作,就这样反复进行读写读写,把整个文件复制到目标位置。

    可以发现,由于有两块儿内存,两块儿缓冲区,即系统内存和 Java 堆内存都有缓冲区,那读取的时候必然涉及到这数据存两份,第一次先读到系统缓冲区还不行,因为 Java 代码不能直接访问系统缓冲区,所以需要先把系统缓冲区数据读入到 Java 缓冲区中,这样就造成了一种不必要的数据的复制,效率因而不是很高。

  • directBuffer(直接缓存区)方式

    当 ByteBuffer 调用 allocateDirect 方法后,操作系统这边划出一块缓冲区,即 direct memory(直接内存),这段区域与之前不一样的地方在于这个操作系统划出来的内存可以被 Java 代码直接访问,即系统可以访问它,Java 代码也可以访问它,它是 java 代码和系统共享的一段内存区域,这就是直接内存。

    磁盘文件读到直接内存后,Java 代码直接访问直接内存,比传统 io 方式少了一次缓冲区里的复制操作,所以速度得到了成倍的提高。

    这也是直接内存带来的好处,适合做较大文件拷贝的这种 io 操作。

演示案例(运行并比较时间后可以发现,尤其是读写大文件时使用 ByteBuffer 的读写性能非常高):

// 演示ByteBuffer作用
public class Demo {
    static final String FORM = "D:\\asd\\asd.mp4"; // 选比较大的文件,比如200多兆
    static final String TO = "D:\\asd.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        // io 用时:3187.41008(大概用了3秒),多跑几遍,多比较,跑一次不算。
        io();
        // directBuffer 用时:951.114625(不到1秒)
        derectBuffer();
    }

    private static void deirectBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
            FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb); // 读写的缓冲区(分配一块儿直接内存)
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }

        }catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        print("directBuffer用时:" + (end - start) / 1000_000.0);
    }

    // 用传统的io方式做文件的读写
    private static void io() {
        long start = System.nanoTime();
        try ( // 网友1:写到try()括号里就不用手动close了
            FileInputStream from = new FileInputStream(FROM);
            FileOutPutStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];// byte数组缓冲区(与上面的读写缓冲区设置大小一致,比较时公平)
            while (true) {
                int len = from.read(buf);// 用输入流读
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);// 用输出流写
            }
        }catch(IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        print("io用时:" + (end - start) / 1000_000.0);
    }
}

直接内存的分配和回收

直接内存的分配和释放是 Java 通过 UnSafe 对象来管理的,并且回收需要主动调用 freeMemory() 方法,不直接受 JVM 内存回收管理。

ByteBuffer 底层分配和释放直接内存的大概情况:

  • ByteBuffer 对象被创建时,调用 Unsafe 对象的 allocateMemory(_1Gb) 方法分配直接内存,返回 long base,即内存地址
  • ByteBuffer 对象被销毁时,调用 unsafe 对象的 freeMemory(base) 方法释放直接内存。

    ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来检测 ByteBuffer 对象,一旦 ByteBuffer 对象被(Java)垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory() 方法来释放直接内存。

演示案例(演示直接内存溢出)

  • 运行后,输出 36

    即循环 36 次(一次 100 兆,循环 36 次也算 3 个 G 多了)后,爆出直接内存溢出异常:

    Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory

// 演示直接内存溢出
public class Demo {
    static int _100Mb = 1024 * 1024 * 100;
    
    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0; 
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);// 每次分配100兆内存
                list.add(byteBuffer);// 把这玩意放到List中,一直循环
                i++;
            }
        }finally {
            print(i);
        }
    }
}

使用 System.gc() 间接进行直接内存的回收可能存在的问题

  • 代码案例

    public class Demo {
        static int _1Gb = 1024 * 1024 * 1024;
        public static void main(String[] args) throws IOException {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
            print("分配完毕");
            print("开始释放");
            byteBuffer = null;
            System.gc(); // 显式的垃圾回收
        }
    }
  • System.gc() 触发的是一次 Full GC,是比较影响性能的垃圾回收 ,不光要回收新生代,还要回收老年代,所以它造成的程序暂停时间比较长。
  • 为了防止一些程序员不小心在代码里经常写 System.gc() 以触发显式的垃圾回收,做一些 JVM 调优时经常会加上 JVM 虚拟机参数 -XX:+DisableExplicitGC,禁用这种显式的垃圾回收,也就是让 System.gc() 代码无效。但是加上这个虚拟机参数后,可能会间接影响到直接内存的回收机制。

    • 没加虚拟机参数的话,由于 byteBuffer 被 null 了,显式触发 Java 垃圾回收,byteBuffer 的堆内存被回收时,会调用 unsafe 对象的 freeMemory(base) 方法释放直接内存,所以也导致了直接内存也被释放掉。
    • 加虚拟机参数之后,System.gc() 代码失效,虽然 byteBuffer 被 null 了,但如果内存比较充足,那么它还会暂时存活着,其创建的直接内存(ByteBuffer.allocateDirect(-1Gb))也会在 byteBuffer 的堆内存被 JVM 自动进行垃圾回收前一直存在着。

    所以禁用 System.gc() 之后,会发现别的代码不受太大影响,但直接内存会受到影响,因为不能用显式的方法回收掉Bytebuffer,所以 ByteBuffer 只能等到 JVM 自动进行垃圾回收时,才会被清理,从而它所对应的那块儿直接内存在此之前也会一直不会被释放掉,这就会造成直接内存可能占用较大,长时间得不到释放这样一个现象。

    所以使用直接内存的情况比较多,由程序员直接手动的管理直接内存时,推荐用 Unsafe 的相关方法,直接调用 Unsafe 对象的 freeMemory() 方法来释放直接内存。

JVM 的性能调优

调优参数

配置方式

  • java [options] MainClass [arguments]
  • options :JVM 启动参数。 配置多个参数的时候,参数之间使用空格分隔。
  • 参数命名: 常见为 -参数名
  • 参数赋值: 常见为 -参数名=参数值 或 -参数名:参数值

内存参数:

  • -Xms(s 为 strating):初始堆大小,JVM启动的时候,给定堆空间大小。

    可以设置与-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。

    示例:-Xms3550m :设置 JVM 初始内存为 3550M。

  • -Xmx(x 为 max):最大堆大小,JVM运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。

    示例:-Xmx3550m :设置 JVM 最大可用内存为 3550M。

  • -Xmn(n 为 new):新生代大小

    整个堆大小 = 新生代大小 + 老年代大小 + 持久代大小(jkd1.8废弃)

    持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。

    此值对系统性能影响较大,Sun官方推荐配置为整个堆的 3/8

    示例:-Xmn2g:设置年轻代大小为2G。

  • -Xss:设置每个线程的 Java 栈大小。

    根据应用的线程所需内存大小进行调整。

    在相同物理内存下,减小这个值能生成更多的线程。

    但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

    JDK5.0 以后每个线程 Java 栈大小为1M,以前每个线程堆栈大小为 256K。

    示例:-Xss128k :设置每个线程的堆栈大小为128k。

  • -XX:NewSize=n:设置年轻代大小
  • -XX:NewRatio=n:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值。

    示例:设置为 4 :年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5

  • -XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。

    注意 Survivor 区有两个。

    示例:设置为 3 :表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5。

  • -XX:MaxPermSize=n:设置永久代大小

    示例:-XX:MaxPermSize=16m:设置持久代大小为16m。

  • -XX:MaxTenuringThreshold=n:设置垃圾最大年龄

    如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。

    如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。

垃圾回收器参数

JVM给了三种选择:串行收集器、并行收集器、并发收集器。串行收集器只适用于小数据量的情况。

  • -XX:+UseSerialGC: 设置串行收集器。
  • -XX:+UseParallelGC: 设置并行收集器,表示年轻代使用并行收集器。
  • -XX:+UseParNewGC: 设置年轻代为并行收集。

    可与 CMS 收集同时使用。

    JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。

  • -XX:+UseParallelOldGC: 设置并行年老代收集器

    JDK6.0 支持对年老代并行收集。

  • -XX:+UseConcMarkSweepGC: 设置年老代并发收集器 CMS。
  • -XX:+UseG1GC: 设置G1收集器
  • -XX:ParallelGCThreads=n: 设置并行收集器收集时最大线程数使用的CPU数。并行收集线程数。
  • -XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间,单位毫秒。

    可以减少STW时间。

  • -XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。

    公式为 1/(1+n) 并发收集器设置

  • -XX:+CMSIncrementalMode: 设置为增量模式。

    适用于单 CPU 情况。

  • -XX:+UseAdaptiveSizePolicy: 设置此选项后,并行收集器会自动选择年轻代区大小和相应的 Survivor 区比例,以达到目标系统规定的最低相应时间或者收集频率等。

    此值建议使用并行收集器时,一直打开。

  • -XX:CMSFullGCsBeforeCompaction=n: 此值设置运行多少次 GC 以后对内存空间进行压缩、整理。

    因为并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。

  • -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。

    可能会影响性能,但是可以消除碎片。

元空间参数:

  • -XX:MetaspaceSize:初始化的 Metaspace 大小,该值越大触发 Metaspace GC 的时机就越晚。

    随着GC的到来,虚拟机会根据实际情况调控 Metaspace 的大小,而上下浮动主要由 -XX:MaxMetaspaceFreeRatio 和 -XX:MinMetaspaceFreeRatio 两个参数控制。

    在默认情况下,这个值大小根据不同的平台在 12M 到 20M 浮动。

    使用 java -XX:+PrintFlagsInitial 命令查看本机的初始化参数。

  • -XX:MinMetaspaceFreeRatio:

    当进行过 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增加 MetaspaceSize 的大小(为了避免过早引发一次垃圾回收)。

    默认值为40,也就是40%。

    设置该参数可以控制 Metaspace 的增长的速度,太小的值会导致 Metaspace 增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致 Metaspace 增长的过快,浪费内存。

  • -XX:MaxMetaspaceFreeRatio:当进行过 Metaspace GC 之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会减小 MetaspaceSize 的大小。

    默认值为70,也就是70%。

  • -XX:MaxMetaspaceExpansion :Metaspace 增长时的最大幅度。默认值大约为5MB。
  • -XX:MinMetaspaceExpansion :Metaspace 增长时的最小幅度。默认值大约330KB。
  • -XX:MaxMetaspaceSize:最大空间。默认是没有限制的。

    指定该值可以防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。

辅助参数

JVM提供了大量命令行参数,打印信息,供调试使用。商业项目上线的时候,不允许使用。一定使用 loggc。主要有以下一些:

  • -XX:+PrintGC

    输出形式:

    [GC 118250K->113543K(130112K), 0.0094143 secs]

    [Full GC 121376K->10414K(130112K), 0.0650971 secs]

  • -XX:+PrintGCDetails

    输出形式:

    [GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

    [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

  • -XX:+PrintGCTimeStamps -XX:+PrintGC:可与上面两个混合使用

    输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

  • -XX:+PrintGCApplicationConcurrentTime :打印每次垃圾回收前,程序未中断的执行时间。

    可与上面混合使用

    输出形式:Application time: 0.5291524 seconds

  • -XX:+PrintGCApplicationStoppedTime: 打印垃圾回收期间程序暂停的时间。

    可与上面混合使用

    输出形式:Total time for which application threads were stopped: 0.0468229 seconds

  • -XX:PrintHeapAtGC :打印GC前后的详细堆栈信息
  • -Xloggc:filename :与上面几个配合使用,把相关日志信息记录到文件以便分析。

调优建议

  • 年轻代大小选择

    • 响应时间优先的应用:

      尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。

      在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达老年代的对象。

    • 吞吐量优先的应用:

      尽可能的设置大,可能到达 Gbit 的程度。

      因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8 CPU 以上的应用。

  • 老年代大小选择

    • 响应时间优先的应用:

      老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。

      如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;

      如果堆大了,则需要较长的收集时间。

      最优化的方案,一般需要参考以下数据获得:

      • 并发垃圾收集信息
      • 持久代并发收集次数
      • 传统GC信息
      • 花在年轻代和年老代回收上的时间比例
      • 减少年轻代和老年代花费的时间,一般会提高应用的效率
    • 吞吐量优先的应用:

      一般吞吐量优先的应用都有一个很大的年轻代和一个较小的老年代。

      原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。

  • 较小堆引起的碎片问题

    因为老年代的并发收集器使用标记-清除算法,所以不会对堆进行压缩。

    当收集器回收时,它会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记-清除方式进行回收。

    如果出现“碎片”,可能需要进行如下配置:

    • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次 Full GC 后,对老年代进行压缩
riscv32-gcc工具链实战:从下载到编译的完整指南 本文提供了一份详尽的riscv32-gcc工具链实战指南,涵盖从官方渠道下载预编译版本、正确选择riscv32-glibc版本、配置环境变量到实战编译的全过程。通过清晰的步骤和避坑指南,帮助开发者快速搭建稳定的RISC-V交叉编译环境,并成功编译从Hello World到Linux内核的程序。 阅读详情

相关推荐

Qt Coordinate System

Qt 坐标系统Qt 坐标系统 数学基础 Qt坐标系统 视口 平移变换 缩放变换 setWindow 旋转 总结数学基础 矩阵与线性变换坐标系变换Qt坐标系统 视口和OpenGL一样,视口确定了画面在窗口(QWidget)中显示的区域,Qt的绘图坐标会完全映射到视口(View port)上,就像图示一样。默认情况下,窗口和视口是重合的,视口的大小覆盖了整个窗口。和OpenGL一样,Qt的QPa

unclerunning的博客 1151

全面解析 Java 虚拟机:架构原理、内存模型与 GC 调优指南

JVM是Java技术的核心,负责执行字节码并实现跨平台能力。其架构包括类加载子系统、运行时数据区和执行引擎,内存管理主要涉及堆的分区(新生代/老年代)和垃圾回收算法(标记-清除、复制等)。通过合理配置内存参数(如-Xmx)、选择合适的GC策略(G1/ZGC等)以及使用监控工具(jstat/VisualVM),可优化应用性能。深入理解JVM原理和调优技术是提升Java应用稳定性与效率的关键。

热爱代码的小小码农一只 822

头歌平台-人工智能导论实验(人脸识别)

答案仅供参考,自己动手丰衣足食!!!

qq_52119136的博客 3026

jvm之栈、堆

1. Java Virtual Machine ​ 人群当中,一位叫java的小伙子正向周围一众人群细数着自己取得的荣耀与辉煌。就在此时,c老头和c++老头缓步走来,看着被众人围住的java,c老头感叹地对着身旁的c++说道:“原以为你就可以挑起我的梁子一直走下去的。” ​ c++笑着回应道:“江山代有才人出,这世界以后总会是90后甚至00后的天下!” 察觉到c和c++的java连忙走出人群,说道:“两位前辈谦虚了,这世界可还离不开两位前辈,我只不过是站在了两位前辈的肩上罢了。” ​ “你这小子可是

码到成功 624

避开这5个坑!Tushare数据获取与清洗的常见问题解决方案

本文详细解析了使用Tushare进行金融数据分析时常见的5大问题,包括Token失效、数据缺失、频率限制等,并提供了Python实战解决方案。通过智能填充策略、队列化管理和分层存储设计,帮助开发者高效获取和清洗金融数据,提升量化分析效率。特别适合金融数据分析师和Python开发者参考。

f9g0h的博客 419

Unity2018连接MySQL数据库

当前在做的一个Unity项目需要连接到MySQL数据库,弄了一整天,排了很多错,终于在Unity上成功连接到MySQL数据库了,所以分享一下,避免有这个需求的朋友不用花那么多时间。 我使用的软件是 Unity20184.0f1 + mysql-8.0.24 导入 MySql.Data.dll 首先需要把 MySql.Data.dll 导入到Assets/Plugins 这个dll需要安装Connector/NET,在安装目录\Assemblies\v4.0中拿到,最好不要下载版本太高的,版本太高导入的时候

ZLX的博客 9449

JVM内存模型、参数配置

目录 1 概述和基本概念 1.1概述 1.2体系结构 1.2.1 类加载子系统 1.2.2运行时数据区 1.2.3执行引擎 2 堆,栈和方法区 2.1概念和联系 2.2辨清java堆 2.3java栈 2.4方法区 3 了解虚拟机参数 3.1虚拟机参数 3.2堆分配参数 3.3堆溢出处理 3.4栈配置 3.5方法区配置 3.6直接内存配置 4 垃圾回收概念...

业精于勤,荒于嬉 368

【Bug已解决】Codex CLI 报错 apply_patch 失败:无法应用补丁的解决方案

方案适用场景推荐指数统一换行符规范跨平台协作项目的标准配置⭐⭐⭐⭐⭐关闭自动格式化冲突IDE 与 Codex 并发编辑同一文件⭐⭐⭐⭐缩小任务改动范围长期有效的使用习惯优化⭐⭐⭐⭐⭐统一 UTF-8 编码涉及非 UTF-8 遗留文件的项目⭐⭐⭐提示重新读取文件单次失败后的快速恢复方式⭐⭐⭐⭐报错的本质是Codex 生成补丁时读取的文件内容,与实际应用补丁时磁盘上的文件内容出现了不一致,常见诱因包括换行符风格混用、文件被其他工具并发修改、编码问题等。

一键难忘的博客 7294

面试必问的JVM应该怎么学(面试题含答案)

java虚拟机的基本结构如图: 1)类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。 2)java堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中...

zhuguanghalo的博客 415

【MATLAB代码】二维平面的TOA定位,GDOP(几何精度因子)和CRLB(克拉美罗下界)计算与输出

本文介绍了一个MATLAB程序,用于计算二维定位场景中的几何精度因子(GDOP)和克拉美罗下界(CRLB)。该程序针对单目标和四个锚点配置,通过网格扫描法计算空间各点的GDOP分布,并以等高线图可视化展示。程序还可计算特定位置的理论定位精度下限(CRLB),输出X/Y方向标准差和GDOP值。核心算法通过构建几何矩阵和费舍尔信息矩阵实现,考虑了测量误差影响,并处理了特殊位置情况。程序提供了完整的源代码和参数修改说明,适用于定位系统精度分析。

985工科博士毕业,专攻定位、导航和滤波等算法研究。从业10年,主要使用MATLAB 803

【算能】TPU-MLIR 工具链之model_deploy

model_deploy.py 是算能 TPU-MLIR 工具链中实现模型高效部署的核心组件,通过灵活的量化配置与硬件优化,可在 ​精度损失 ≤1% 的前提下实现 ​3-5 倍加速。开发者需重点关注校准数据质量、混合精度策略及硬件兼容性

浩瀚之水的专栏 288

JVM整体结构

1.Java语言的跨平台特性 2.JVM整体结构及内存模型 JVM是由类装载子系统,运行时数据区,java执行引擎三部分组成的 这三部分是相辅相成共同工作了,不能单纯的说只有执行完某一个部分才由下一个部分来工作,从JVM运行开始,三个类加载器就要开始创建,并且类加载器的一些信息也要放在运行时数据区 1.类装载子系统 .class文件通过类装载系统进行加载,验证等一系列操作,在这个过程中将需要存储到内存的数据放到运行时数据区 类装载子系统也是之前的类加载机制介绍的 2.运行时数据区

weixin_56892092的博客 1083

[低代码 + AI] 明道云与 Dify 的三种融合实践方式详解

融合方式技术路径优点局限或注意事项UI 嵌入iframe 表单嵌入快速集成、用户体验一致无法自定义交互逻辑明道云调用 DifyAPI 请求节点工作流自动化、逻辑灵活有 timeout 限制、需优化 promptDify 调用明道云插件或 API 调用节点可查询实际业务数据、增强 AI 智能需理解明道云 API 结构、权限设置复杂明道云与 Dify 的结合为低代码平台注入了智能动力。

技术小甜甜:传播可落地技术,帮助个人和企业降低解决真实场景问题的成本 1186

你不知道的JVM原理

从GC的发展历史来看,目标是两个尽可能减少停顿时间,不影响业务。尽可能减少GC时间,让GC更快。基本上G1 GC是通用的,如果条件允许上ZGC或Shenandoah。JDK8的默认GC是Parallel GC,JDK9后一直都是G1 GC。从这里也能看出G1 GC性能更好。更通用。如果系统考虑吞吐优先,CPU资源用来处理业务,用Parallel GC如果系统考虑低延迟优先,每次GC时间尽量短,用CMS GC如果系统内存堆大,平均GC时间可控,使用G1 GC。

大头讲架构的博客 826

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

JVM内存模型解析

📘拥有多年一线研发和团队管理经验,研究过主流框架的底层源码(Spring、SpringBoot、SpringMVC、SpringCloud、Mybatis、Dubbo、Zookeeper),消息中间件底层架构原理(RabbitMQ、RocketMQ、Kafka)、Redis缓存、MySQL关系型数据库、 ElasticSearch全文搜索、MongoDB非关系型数据库、Apache ShardingSphere分库分表读写分离、设计模式、领域驱动DDD、Kubernetes容器编排等。

我是Java程序员廖志伟,感谢朋友们的支持!不定期贡献一篇高品质、过万文字、图文并茂且附有视频解说、满载代码示例注释的良心之作,坚决杜绝粗制乱造。 1696

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

JAVA内存模型与JVM内存结构

注意区分Java内存模型(Java Memory Model,简称JMM)与Jvm内存结构,前者与多线程相关,后者与JVM内部存储相关。本文会对两者进行简单介绍。

weixin_43681666的博客 3960

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

JVM实战:从内存模型到性能调优的全链路剖析

掌握 JVM,不只是为了面试,更是为了真正理解程序背后的执行机制。当你能读懂 GC 日志、分析堆快照、调节参数、优化性能——你就从“写 Java”跨越到了“理解 Java”。JVM 就像一台赛车的引擎,会开车的人很多,但能调教引擎的人不多。

dividividiv的博客 999

JVM-内存模型理解

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1d1vc3z2-1687074564948)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6fd1ed29-9cac-4641-97b6-cdde8a4ec154/Untitled.png)]​ 前几种算法的组合,将内存依据对象存活周期的不同将内存划分为几块,一般把内存分为新生代,老年代,永久代。其中新生代是朝生夕灭,存活时间较短;

weixin_46190208的博客 196

JVM核心技术深度解析:从类加载到GC调优的全栈指南

本文系统介绍了JVM的核心架构与调优实践。主要内容包括:1)JVM运行时数据区结构,重点解析堆、方法区、虚拟机栈等核心内存区域;2)类加载机制,从双亲委派原理到热加载实战;3)对象创建全流程与内存分配策略;4)JIT编译执行与分层优化;5)垃圾收集算法与主流收集器对比;6)亿级电商系统的JVM调优实战案例。文章通过原理分析、参数配置、代码示例等方式,帮助开发者深入理解JVM工作机制,掌握从内存分配到GC调优的完整技能体系,为构建高性能Java应用提供系统指导。

alonewolf_99的博客 670

JVM原理总结

本文全面解析Java虚拟机(JVM)的核心原理,包括:1)JVM的整体架构由类加载子系统、运行时数据区、执行引擎等模块组成;2)运行时数据区分为线程私有区域(程序计数器、虚拟机栈)和共享区域(堆、方法区);3)类加载机制采用双亲委派模型,经历加载、验证、准备等阶段;4)执行引擎通过解释器和JIT编译器执行字节码,JIT会对热点代码进行方法内联等优化。文章还详细介绍了堆内存划分、垃圾回收机制和性能调优方法,帮助开发者深入理解JVM工作原理并优化Java应用性能。

qq_52537313的博客 897

一文拿下JVM 深度剖析:内存模型、垃圾回收机制与性能优化

Java虚拟机(JVM)核心机制解析 摘要:本文系统介绍了JVM的核心组成与工作原理。首先阐述了JDK、JRE和JVM的层级关系:JDK包含JRE和开发工具,JRE包含JVM和核心类库。然后深入分析了JVM的体系结构,包括类加载机制(双亲委派模型)、运行时数据区(堆、栈、方法区等)以及执行引擎(解释器与JIT编译器)。重点讨论了内存管理机制,包括堆内存分代模型(新生代/老年代)和垃圾回收算法(标记-清除、复制、标记-整理等),并比较了不同GC收集器的特点。最后总结了JVM调优原则。

2202_75362996的博客 1675

深入理解JVM核心机制:内存管理、类加载与GC原理剖析

可以快速构建JVM监控原型,其内置的Java环境支持直接运行内存分析工具。实际测试发现,平台的一键部署功能特别适合演示GC日志可视化这类需要持续运行的服务,省去了本地配置环境的麻烦。对于想实践JVM调优的开发者,这种即开即用的体验非常友好。方法区(元空间)存储类元信息,JDK8后使用本地内存,需注意-XX:MaxMetaspaceSize设置。特殊场景如SPI服务加载会破坏双亲委派,需使用线程上下文类加载器。关注大对象分配(如缓存集合)和内存泄漏(未关闭的资源)垃圾回收核心算法演进。

GreenLeaf78的博客 291

【Java核心】JVM核心知识清单

核心架构:JVM五大模块(类加载+运行时数据区+执行引擎+JNI+GC)是基础,其中「运行时数据区」和「GC」是重中之重;核心机制:垃圾回收(算法/回收器/触发时机)、类加载(双亲委派)、OOM排查是面试/调优的核心;实战调优:先监控再调参,核心参数(-Xms/-Xmx/-Xmn)+ 回收器选择是小白入门的关键。

Coder_Boy_的博客 871

JVM从入门到入魔,这份JVM必知必会的完整版带你彻底玩懂JVM

在大厂摸爬滚打 10 多年的Java 高级技术专家全面梳理了系统化学习 JVM 的知识和经验,从入门到入魔,以及常见的面试问题深度剖析等高级的进阶方法与实战!既满足快速系统化学习和全面掌握知识的需求,又兼顾面试经验辅导。

m0_57042151的博客 226

在构建高可用、低延迟的 Java 应用(尤其是微服务、大数据处理、高频交易系统)时,JVM 内部机制是决定系统稳定性的“黑盒”。理解类加载、内存模型和垃圾回收(GC),不仅能帮助我们避免 OutOfM

关注点核心机制业务决策关键点避坑指南类加载双亲委派 / 自定义 Loader是否需要隔离(多租户/Tomcat)?是否需要SPI(插件化)?不要随意打破双亲委派,除非清楚后果;注意 Metaspace 泄漏。内存模型堆 (对象) / 栈 (帧) / 元空间 (类)大对象放哪?递归深度多少?动态类有多少?避免深递归 (StackOverflow);静态集合防泄漏;容器环境需感知内存限制。GC 算法复制 (新生代) / 整理 (老年代)对象存活率高还是低?新生代过小会导致频繁晋升;

qq_33240556的博客 643

深入理解JVM:从基础到实战全解析

本专栏系统讲解JVM核心原理与应用,涵盖基础架构、垃圾回收机制、性能调优和故障排查四大模块,并分析Java新特性在JVM中的实现。通过理论讲解、代码示例和实战案例,帮助1-3年经验的Java开发者深入理解JVM工作机制,掌握内存管理、线程同步、GC算法等关键技术,并能独立进行性能优化和问题排查。配套工具使用和调优策略,解决实际开发中的内存泄漏、线程死锁等常见问题,提升系统性能。

fykam的博客 642
上一篇: CompletableFuture 深度解析
下一篇: 线程、线程池、锁
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值