JVM面试知识点手册

第一部分:JVM 概述

1.1 JVM 简介

Java Virtual Machine(JVM) 是 Java 语言的核心组件,负责将 Java 程序编译后的字节码(bytecode)转换为机器指令,并在目标机器上执行。JVM 提供了硬件和操作系统的抽象,使得 Java 程序具有跨平台的特性,即“一次编写,随处运行”(Write Once, Run Anywhere)。

JVM 的核心作用:

  • 字节码执行:JVM 负责执行 Java 编译器生成的字节码文件(.class 文件)。
  • 内存管理:JVM 提供自动的内存管理机制,通过垃圾回收(Garbage Collection, GC)回收无用对象,避免了内存泄漏。
  • 线程管理:JVM 为 Java 提供了多线程支持,管理线程的生命周期。
  • 安全机制:JVM 提供了类加载器和安全管理器,确保执行环境的安全性。

JVM 的跨平台性:
Java 程序的跨平台性是通过 JVM 实现的。每种操作系统和硬件架构都对应不同的 JVM 实现,Java 源代码被编译成字节码后,由对应平台的 JVM 执行,确保程序无需修改就能在不同平台上运行。

1.2 JVM 运行原理简述

JVM 的工作流程大致分为以下几个步骤:

  1. 编译:Java 源代码(.java 文件)通过 Java 编译器(javac)编译为字节码文件(.class 文件)。
  2. 类加载:JVM 的类加载器将字节码文件加载到内存,并进行必要的验证和准备工作。
  3. 字节码执行:JVM 执行字节码文件,将其转换为对应平台的机器码,并通过解释器或即时编译器(JIT)执行。
  4. 内存管理和垃圾回收:JVM 在执行过程中自动管理内存分配,定期通过垃圾回收器回收不再使用的对象。
  5. 线程调度和并发控制:JVM 提供多线程支持,调度和管理 Java 线程的执行。
1.3 JVM 与 JRE、JDK 的关系

Java 开发环境中常常提到三个重要的组成部分:JDK、JRE 和 JVM。

  1. JVM(Java Virtual Machine):

    • JVM 是 Java 程序的运行环境,负责执行字节码文件。它是一种虚拟机,专门为 Java 设计。
  2. JRE(Java Runtime Environment):

    • JRE 是 Java 程序的运行时环境,它包含了 JVM 以及 Java 标准类库(如 Java 核心库、用户界面库等)。简单来说,JRE 是 Java 程序运行所必需的环境,但不包含开发工具。
  3. JDK(Java Development Kit):

    • JDK 是 Java 的开发工具包,包含了开发和调试 Java 程序所需要的工具(如编译器 javac、打包工具 jar 等)以及 JRE。因此,JDK 是开发者所使用的完整工具包,而 JRE 则是专用于运行 Java 程序的环境。

关系图

JDK├── JRE│    ├── JVM│    └── Java 核心类库└── 开发工具(javac、jar 等)

1.4 面试常见问题:
  1. 什么是 JVM,它的作用是什么?

    • JVM 是 Java 虚拟机,负责执行 Java 字节码文件、管理内存、处理线程调度等。
  2. JVM、JRE 和 JDK 之间的区别是什么?

    • JVM 是虚拟机,用于执行字节码;JRE 是包含 JVM 和标准类库的运行环境;JDK 是包含开发工具和 JRE 的完整开发包。
  3. JVM 如何实现跨平台?

    • Java 程序通过编译生成字节码,JVM 将字节码转换为对应平台的机器码,每个平台都有其对应的 JVM 实现,因此实现了跨平台性。
1.5 JVM 结构

JVM 的内部结构复杂且精妙,由多个模块组成,各模块协同工作,保证 Java 程序的高效执行。理解 JVM 的结构可以帮助我们在面试中更好地回答性能调优、类加载等相关问题。

JVM 的核心结构可以划分为以下几个模块:

  1. 类加载器(Class Loader)

    • 负责将字节码文件(.class 文件)加载到 JVM 内存中。
    • 类加载器使用了一种叫做 双亲委派模型 的机制来保证类的加载顺序(将在后续章节详细介绍)。
    • 类加载器的作用是将外部的类文件读取进内存,同时对类文件进行校验、解析、准备和初始化。
  2. 运行时数据区(Runtime Data Areas)

    • JVM 在执行 Java 程序时,会将数据存储在不同的内存区域。运行时数据区可以大致分为以下几个部分:
      • 方法区(Method Area):存储已加载的类信息、常量、静态变量、即时编译后的代码等,属于线程共享的内存区。
      • 堆(Heap):存储对象实例和数组,所有线程共享的内存区,堆是垃圾回收(GC)的重点区域。
      • 虚拟机栈(JVM Stack):每个线程都会创建一个虚拟机栈,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法在执行时都会创建一个栈帧(Stack Frame)。
      • 程序计数器(Program Counter Register):每个线程都有一个独立的程序计数器,记录当前线程执行的字节码指令的地址。
      • 本地方法栈(Native Method Stack):与 JVM Stack 类似,但用于存储本地方法调用时的相关信息。
  3. 执行引擎(Execution Engine)

    • JVM 的执行引擎负责解释并执行字节码文件。它分为两种执行模式:
      • 解释执行:逐行解释字节码并执行。
      • 即时编译执行(Just-In-Time, JIT):将热点代码编译为机器码,直接在 CPU 上执行以提高性能。
    • JVM 还会利用多种优化技术,如内联、逃逸分析等,来提升代码执行效率(将在后续章节详细讲解)。
  4. 本地方法接口(JNI,Java Native Interface)

    • JVM 通过 JNI 提供调用非 Java 代码的能力,例如调用 C/C++ 编写的底层代码或操作系统原生方法。
    • JNI 的作用是帮助 JVM 扩展与底层系统的交互功能,尤其是在需要调用特定硬件或者优化性能时。
  5. 垃圾回收器(Garbage Collector)

    • JVM 提供自动内存管理机制,垃圾回收器负责回收不再被引用的对象,防止内存泄漏。
    • 垃圾回收器通过不同的算法(如标记-清除、标记-整理、分代收集等)和回收器(如 Serial、CMS、G1 等)执行垃圾回收。
1.6 面试常见问题:
  1. JVM 的核心组成部分有哪些?

    • JVM 包含类加载器、运行时数据区、执行引擎、本地方法接口、垃圾回收器等核心模块。
  2. JVM 的运行时数据区是如何划分的?

    • JVM 的内存模型分为方法区、堆、虚拟机栈、程序计数器和本地方法栈,每个区域有不同的用途和生命周期。
  3. 执行引擎中解释器与即时编译器(JIT)的区别是什么?

    • 解释器逐行解释字节码并执行,而 JIT 编译器将热点代码编译为机器码直接执行,以提高性能。
  4. 什么是 JNI(Java Native Interface),它的作用是什么?

    • JNI 是 Java 与其他编程语言(如 C/C++)交互的接口,允许 Java 程序调用本地代码和系统 API。

第二部分:JVM 内存模型

2.1 JVM 内存区域划分

JVM 在运行时将内存划分为多个区域,每个区域负责不同的任务,合理的内存划分帮助 JVM 高效地管理应用程序的资源。JVM 的内存模型大致可以分为以下几个部分:

  1. 方法区(Method Area)

    • 作用:方法区存储已加载的类信息、常量、静态变量、即时编译后的代码等。
    • 特点
      • 属于线程共享的区域,每个线程都可以访问方法区。
      • 方法区中还包含了运行时常量池(Runtime Constant Pool),用于存储编译期生成的各种字面量和符号引用。
      • 方法区在 JVM 规范中是逻辑上的概念,在不同的 JVM 实现中可能会有所差异。在 HotSpot 虚拟机中,方法区被称为“永久代(Permanent Generation)”,但在 Java 8 中,永久代被元空间(Metaspace)取代。
  2. 堆(Heap)

    • 作用:堆是 JVM 中用于存储对象实例的区域,几乎所有的对象实例和数组都存储在堆中。
    • 特点
      • 堆是所有线程共享的内存区域,是垃圾回收的重点区域。
      • Java 堆在逻辑上分为新生代(Young Generation)和老年代(Old Generation)。新生代进一步划分为 Eden 区和两个 Survivor 区(S0 和 S1),用于存储新创建的对象。
      • 堆内存大小可以通过 -Xms-Xmx 参数进行设置,-Xms 指定堆的初始大小,-Xmx 指定堆的最大大小。
  3. 虚拟机栈(JVM Stack)

    • 作用:每个线程在执行 Java 方法时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息,虚拟机栈是这些栈帧的集合。
    • 特点
      • 每个线程都有自己独立的虚拟机栈,线程执行时,栈帧按照方法调用顺序进栈、出栈。
      • 如果线程请求的栈深度大于虚拟机栈的最大深度,会抛出 StackOverflowError
      • 如果虚拟机栈无法申请到足够内存时,会抛出 OutOfMemoryError
  4. 程序计数器(Program Counter Register)

    • 作用:程序计数器是一个较小的内存区域,用于存储当前线程所执行的字节码指令的地址。
    • 特点
      • 每个线程都有独立的程序计数器,用于记录该线程下一条要执行的字节码指令位置。
      • 如果线程执行的是本地方法,程序计数器的值为空(Undefined)。
      • 程序计数器是 JVM 中唯一不会发生内存溢出的区域。
  5. 本地方法栈(Native Method Stack)

    • 作用:本地方法栈用于存储每个线程执行的本地方法的相关信息,类似于虚拟机栈,但它为本地方法(Native 方法)服务。
    • 特点
      • 本地方法栈为 Java 调用 C/C++ 等本地方法时提供了支持。
      • 与虚拟机栈类似,本地方法栈在某些情况下也可能抛出 StackOverflowErrorOutOfMemoryError
2.2 运行时常量池

运行时常量池(Runtime Constant Pool) 是方法区的一部分,用于存储编译期生成的常量,如字符串常量、数值常量等,以及类、方法的符号引用。

特点

  • 动态性:与 Class 文件中的常量池不同,运行时常量池支持动态添加常量。例如,运行时通过 String.intern() 方法将字符串放入常量池。
  • 内存溢出:当常量池无法申请到足够内存时,也会抛出 OutOfMemoryError
2.3 Java 内存模型(JMM)

Java 内存模型(Java Memory Model, JMM)定义了 Java 程序中多线程操作的内存可见性规则,确保不同线程之间对共享变量的读写操作有序可见。

  1. 可见性

    • 当一个线程对共享变量进行了修改,其他线程应该立即看到修改结果。
    • volatile 关键字可以确保变量的可见性,强制将修改后的变量值同步到主内存。
  2. 有序性

    • Java 程序中的操作可能会因为编译器优化或 CPU 的乱序执行而导致执行顺序与代码顺序不同。
    • synchronizedvolatile 关键字可以确保操作的有序性。
  3. 原子性

    • 原子性意味着一个操作是不可分割的,中间不会被打断。
    • Java 的基本数据类型赋值是原子操作,longdouble 类型的赋值在 32 位 JVM 中不是原子的。synchronizedLock 可以确保多线程情况下的原子性操作。
2.4 面试常见问题:
  1. JVM 中堆与栈的区别是什么?

    • 堆是存储对象实例的区域,属于线程共享的内存;栈是线程私有的内存区域,用于存储局部变量、方法调用信息。
  2. 程序计数器的作用是什么?

    • 程序计数器记录当前线程正在执行的字节码指令的地址,确保线程切换后能正确恢复执行。
  3. JVM 内存划分的区域有哪些?

    • JVM 运行时内存划分为方法区、堆、虚拟机栈、程序计数器和本地方法栈,每个区域有不同的职责。
  4. 什么是 Java 内存模型(JMM),它解决了什么问题?

    • JMM 规定了 Java 程序中多线程操作共享变量时的可见性、有序性和原子性,确保线程安全。

第三部分:类加载机制

3.1 类加载过程

类加载机制是 JVM 的核心之一,它负责将类从字节码文件加载到内存并准备执行。类加载过程主要分为以下五个阶段:

  1. 加载(Loading)

    • 通过类的全限定名来获取该类的字节码内容,并将其转换成 JVM 可以识别的类对象(java.lang.Class)。
    • 在加载过程中,JVM 会根据类的全限定名找到对应的字节码文件(通常是 .class 文件),然后通过类加载器(ClassLoader)加载到内存中。
  2. 验证(Verification)

    • 确保字节码文件的正确性和安全性,防止恶意代码损害 JVM 的运行。
    • 验证的过程包括文件格式验证、元数据验证、字节码验证和符号引用验证。例如,它会确保类文件的格式正确、没有非法的操作码等。
  3. 准备(Preparation)

    • 为类的静态变量分配内存,并将其初始化为默认值(如 0null)。
    • 这一步不会给变量赋值实际的值,只会分配内存空间并初始化为默认值,真正的赋值操作会在初始化阶段完成。
  4. 解析(Resolution)

    • 将常量池中的符号引用(Symbolic Reference)替换为直接引用(Direct Reference)。
    • 解析的目标是将常量池中的符号引用转化为内存地址,如类、字段、方法等引用都会在解析阶段被转换为实际的内存地址。
  5. 初始化(Initialization)

    • 执行类的静态代码块(<clinit> 方法)和静态变量的初始化。
    • 在这个阶段,类的静态变量会被赋值为程序员指定的值,执行顺序依照代码中的静态代码块和静态变量声明顺序。
3.2 类加载器

JVM 使用类加载器(ClassLoader)来加载类的字节码文件。每个类在 JVM 中都有且只有一个类加载器负责加载它。Java 提供了多种类加载器,每种类加载器负责加载不同的类。

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

    • 作用:启动类加载器是 JVM 内置的,用于加载核心类库(如 java.lang.*java.util.* 等),这些类库存放在 JRE/lib 目录下。
    • 特点:启动类加载器是由本地代码实现的,它加载的是 JVM 启动时所需的核心类,并且不继承自 ClassLoader 类。
  2. 扩展类加载器(Extension ClassLoader)

    • 作用:扩展类加载器加载的是 JRE/lib/ext 目录中的类或通过 java.ext.dirs 系统变量指定的类库。
    • 特点:它是 ClassLoader 的子类,由 Java 编写,主要加载一些扩展类库。
  3. 应用程序类加载器(Application ClassLoader)

    • 作用:也称为系统类加载器,负责加载用户类路径(classpath)下的类,几乎所有应用程序中的类都是由它加载的。
    • 特点:它是 ClassLoader 类的实例,可以通过 ClassLoader.getSystemClassLoader() 方法获取。
  4. 自定义类加载器(Custom ClassLoader)

    • 作用:开发者可以通过继承 ClassLoader 类实现自己的类加载器,以满足特殊需求,比如动态加载类、网络加载类等。
    • 特点:自定义类加载器允许开发者通过覆盖 findClass() 方法来自定义类的加载方式。
3.3 双亲委派机制

双亲委派模型是 JVM 类加载机制中的一个重要原则,它规定当类加载器加载某个类时,首先会将请求委派给父类加载器,父类加载器继续向上委派,直到顶层的启动类加载器。如果父类加载器无法加载该类,才会由当前加载器尝试加载。

双亲委派机制的优点

  1. 避免重复加载:通过双亲委派机制,确保 Java 核心类库不会被重复加载。
  2. 安全性:防止自定义类加载器加载替代 Java 核心类的类,比如 java.lang.String 类,确保系统安全。

打破双亲委派模型

  • 在某些场景下,双亲委派模型需要被打破,例如通过自定义类加载器动态加载某些类。常见的例子是 Java 的 SPI 机制(Service Provider Interface),这需要使用 Thread.getContextClassLoader() 来加载自定义类。
3.4 面试常见问题:
  1. 类加载的五个阶段分别是什么?

    • 加载、验证、准备、解析、初始化。
  2. 双亲委派模型的作用是什么?

    • 双亲委派模型确保类的加载遵循先父后子的原则,避免核心类库被重复加载或篡改。
  3. 自定义类加载器有什么应用场景?

    • 自定义类加载器适用于动态加载类、模块化加载等场景,如 OSGi 模块化系统和热部署等。

第四部分:JVM 垃圾回收

4.1 垃圾回收概述

JVM 提供了自动内存管理机制,通过垃圾回收器(GC)来释放不再使用的对象,避免内存泄漏。垃圾回收的主要目标是回收堆内存中那些不可达的对象。

垃圾回收的必要性

  • 手动管理内存容易导致内存泄漏或内存溢出,而 JVM 自动管理对象生命周期,减少了程序员的负担。
  • 在没有垃圾回收的情况下,程序员需要手动释放内存,增加了开发复杂度和出错几率。

内存泄漏与内存溢出

  • 内存泄漏:指对象不会再被程序使用,但由于存在引用,导致它无法被垃圾回收器回收。
  • 内存溢出(OutOfMemoryError):指 JVM 在运行时无法分配足够的内存,通常是堆或方法区无法申请到足够内存空间。
4.2 垃圾回收算法

垃圾回收器使用不同的算法来识别和回收不再需要的对象,常见的垃圾回收算法有以下几种:

  1. 标记-清除算法(Mark-Sweep)

    • 过程:首先标记出所有需要回收的对象,然后直接清除它们。
    • 优点:实现简单,不需要额外的内存空间。
    • 缺点:标记和清除过程效率低,并且会产生大量内存碎片。
  2. 复制算法(Copying)

    • 过程:将存活的对象从当前内存区域复制到另一个区域,然后清空当前区域。
    • 优点:复制算法可以避免内存碎片问题,分配内存高效。
    • 缺点:需要额外的内存空间进行对象复制。
  3. 标记-整理算法(Mark-Compact)

    • 过程:首先标记出所有存活的对象,然后将存活对象压缩到内存的一端,最后清理掉未使用的内存空间。
    • 优点:避免了内存碎片问题,不需要额外的内存空间。
    • 缺点:移动存活对象的成本较高,适合老年代回收。
  4. 分代收集算法(Generational Garbage Collection)

    • 过程:将堆内存划分为新生代和老年代,不同代的对象使用不同的垃圾回收算法。
    • 优点:适应对象的生命周期特点,新生代回收频繁,老年代回收较少。
    • 缺点:新生代和老年代的垃圾回收算法不同,增加了系统的复杂度。
4.3 垃圾回收器

垃圾回收器是具体执行垃圾回收的组件,常见的垃圾回收器有:

  1. Serial 垃圾回收器

    • 单线程垃圾回收器,适用于单线程环境或内存较小的客户端应用。
  2. Parallel 垃圾回收器

    • 多线程垃圾回收器,适用于多核 CPU,可以在多个 CPU 上并行执行垃圾回收操作。
  3. CMS(Concurrent Mark-Sweep)垃圾回收器

    • 低停顿垃圾回收器,使用标记-清除算法,在应用运行过程中并发执行垃圾回收,适用于需要较短停顿时间的应用。
  4. G1(Garbage-First)垃圾回收器

    • 面向服务端应用的低停顿垃圾回收器,适用于大堆内存,能够同时处理新生代和老年代的垃圾回收,避免 Full GC。

第五部分:JVM 性能调优

5.1 常用 JVM 参数

JVM 提供了一系列参数,用于控制内存大小、垃圾回收行为、性能调优等。合理配置 JVM 参数能够显著提升 Java 应用的运行效率。以下是一些常用的 JVM 参数:

  1. 堆内存大小设置

    • -Xms:设置堆内存的初始大小。例如,-Xms512m 表示 JVM 启动时堆内存大小为 512MB。
    • -Xmx:设置堆内存的最大大小。例如,-Xmx1024m 表示 JVM 堆内存最大可达到 1024MB。
    • 调优建议:将 -Xms-Xmx 设置为相同的值,可以避免 JVM 在运行过程中频繁调整堆内存大小,从而减少性能波动。
  2. 栈内存大小设置

    • -Xss:设置每个线程的栈内存大小。例如,-Xss512k 表示每个线程的栈内存大小为 512KB。
    • 调优建议:对于多线程应用,适当增加栈内存大小可以避免栈溢出(StackOverflowError),但过大的栈内存会消耗更多的物理内存。
  3. 垃圾回收器选择

    • -XX:+UseSerialGC:使用 Serial 垃圾回收器,适用于单线程应用或资源受限的环境。
    • -XX:+UseParallelGC:使用 Parallel 垃圾回收器,适用于高吞吐量、多核 CPU 环境。
    • -XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器,适用于对低停顿时间有要求的场景。
    • -XX:+UseG1GC:使用 G1 垃圾回收器,适用于大堆内存的低延迟应用。
    • 调优建议:选择合适的垃圾回收器取决于应用的特点,CMS 和 G1 更适合低延迟应用,而 Parallel 更适合高吞吐量的服务端应用。
  4. 永久代/元空间设置

    • -XX:PermSize:设置永久代的初始大小(适用于 Java 7 及以下版本)。
    • -XX:MaxPermSize:设置永久代的最大大小(适用于 Java 7 及以下版本)。
    • -XX:MetaspaceSize:设置元空间的初始大小(适用于 Java 8 及以上版本)。
    • -XX:MaxMetaspaceSize:设置元空间的最大大小(适用于 Java 8 及以上版本)。
    • 调优建议:Java 8 及以上版本采用了元空间来替代永久代,适当设置元空间大小可以避免 OutOfMemoryError
5.2 性能调优工具

JVM 提供了多种性能调优工具,用于监控和分析 Java 应用的运行状态,帮助开发者定位性能瓶颈。常用工具包括:

  1. jstat

    • 作用:用于监控 JVM 运行时的内存和垃圾回收情况。
    • 常用命令
      • jstat -gc pid:显示 GC 相关信息。
      • jstat -gcutil pid:显示各代内存使用情况。
  2. jmap

    • 作用:用于生成 Java 堆的内存快照(heap dump),并可以分析堆中对象的占用情况。
    • 常用命令
      • jmap -heap pid:查看 JVM 堆的详细信息。
      • jmap -dump:format=b,file=heap_dump.hprof pid:生成堆快照文件。
  3. jstack

    • 作用:用于查看线程的堆栈信息,帮助分析线程死锁、线程阻塞等问题。
    • 常用命令
      • jstack pid:输出当前 JVM 进程的线程堆栈信息。
  4. jconsole

    • 作用:JDK 自带的图形化监控工具,用于监控 JVM 的内存、线程、CPU 使用情况。
    • 特点:直观易用,适合实时监控应用的运行状态。
  5. VisualVM

    • 作用:集成了多种分析功能,包括堆快照分析、GC 日志分析、CPU 和内存使用分析等。
    • 特点:支持实时监控和离线分析,适合分析性能问题和内存泄漏。
  6. MAT(Memory Analyzer Tool)

    • 作用:用于分析 Java 堆快照,帮助定位内存泄漏、分析大对象。
    • 特点:可以深入分析大对象及其引用关系,帮助开发者找到内存泄漏的根源。
5.3 常见性能优化策略
  1. 减少 Full GC 触发

    • 问题:Full GC 是垃圾回收中最耗时的一种操作,会暂停所有应用线程,影响应用性能。
    • 优化策略
      • 通过 -Xms-Xmx 设置合理的堆大小,避免频繁的内存分配和回收。
      • 使用 G1 或 CMS 垃圾回收器,这些回收器在执行 Full GC 时更高效。
      • 优化代码中对象的生命周期,避免短命对象大量创建和长时间存活。
  2. 内存泄漏排查

    • 问题:内存泄漏会导致应用的堆内存不断增长,最终触发 OutOfMemoryError
    • 优化策略
      • 使用 jmap 生成堆快照,并用 MAT 分析内存泄漏对象的引用链,找到泄漏源。
      • 避免全局静态变量持有大对象引用,及时清理不再使用的对象。
      • 使用 WeakReferenceSoftReference 代替强引用,减少对象不必要的长时间引用。
  3. 方法区溢出优化

    • 问题:方法区(Java 8 之前称为永久代)可能因类或方法过多而溢出,触发 OutOfMemoryError
    • 优化策略
      • 使用 -XX:MaxMetaspaceSize 设置合理的元空间大小,避免方法区溢出。
      • 对于动态生成类的应用,使用类卸载机制,及时卸载不再使用的类。
  4. 线程调优

    • 问题:线程过多或线程资源争用可能导致 CPU 利用率低或线程阻塞。
    • 优化策略
      • 使用 jstack 分析线程状态,定位死锁或线程饥饿问题。
      • 适当减少线程池中线程数量,避免频繁的上下文切换。
5.4 面试常见问题:
  1. 如何通过 JVM 参数来调优 Java 应用的性能?

    • 可以通过调整堆大小、选择合适的垃圾回收器、设置栈大小等参数来优化 JVM 性能。
  2. 如何定位和解决内存泄漏问题?

    • 使用 jmap 生成堆快照并结合 MAT 工具分析堆中的对象引用链,找到内存泄漏的来源。
  3. Full GC 是什么,它的触发原因有哪些?

    • Full GC 是对整个堆内存(包括新生代和老年代)进行的垃圾回收,通常由堆内存不足、方法区满等原因触发。

第六部分:字节码与执行引擎

6.1 字节码简介

字节码(Bytecode) 是一种面向虚拟机的中间语言,是 JVM 执行 Java 程序的基础。Java 源代码经过编译后生成 .class 文件,其中包含了 JVM 可以理解的字节码指令。

字节码的特点

  • 与硬件无关,跨平台性强。Java 编译器生成的字节码可以在任何 JVM 上运行。
  • 每个字节码指令对应特定的操作,如加载、存储、运算、控制跳转等。

查看字节码

  • 可以使用 JDK 自带的 javap 工具查看 .class 文件中的字节码。例如,javap -c MyClass 可以打印出 MyClass 的字节码指令。
6.2 解释器与即时编译器(JIT)

JVM 执行字节码的方式有两种:解释执行和即时编译(JIT)。

  1. 解释器
    • JVM 通过解释器逐条解释执行字节码。

每次遇到字节码指令时,解释器将其转换为机器码并执行。

  • 优点:启动速度快,因为无需等待字节码的编译。
  • 缺点:解释执行的效率较低,尤其在执行频繁的代码段时,性能会受到影响。
  1. 即时编译器(JIT)
    • JIT 编译器在运行时将热点代码(执行频率高的代码)编译为机器码,直接在 CPU 上执行。
    • 优点:通过将热点代码编译为机器码,JIT 提升了程序的执行效率。
    • 缺点:JIT 编译需要额外的时间和资源,可能在程序启动阶段增加延迟。

JVM 的 JIT 编译器通常分为两个阶段:

  • C1 编译器:进行简单优化,生成较快的机器码,适合应用启动阶段使用。
  • C2 编译器:进行复杂优化,生成更高效的机器码,适合长时间运行的热点代码。
6.3 逃逸分析与锁消除
  1. 逃逸分析

    • 逃逸分析是 JIT 编译器的优化技术,用于判断对象的作用范围。如果对象只在方法内部使用而不会逃逸到方法外部,则可以将其分配在栈上而不是堆上,从而减少堆内存分配和垃圾回收的压力。
  2. 锁消除

    • 锁消除是基于逃逸分析的一种优化技术。如果编译器通过逃逸分析发现加锁的对象不会被其他线程访问,那么就可以消除该锁,从而避免不必要的同步操作,提升性能。

第七部分:JVM 常见面试问题总结

7.1 JVM 高频面试问题

在 Java 面试中,JVM 是一个非常常见的考察点。以下是一些常见的 JVM 面试问题,涵盖 JVM 的内存模型、垃圾回收机制、类加载器等多个方面。这些问题不仅要求面试者对 JVM 的工作原理有深刻的理解,还需要有实际调优和问题排查的经验。

1. JVM 的内存结构是什么样的?
  • 回答思路:
    • JVM 内存划分为方法区(Java 8 后称为元空间)、堆、虚拟机栈、程序计数器、本地方法栈五个区域。
    • 堆内存用于存储对象实例,分为新生代和老年代。虚拟机栈保存每个线程的局部变量、操作数栈等。
    • 方法区存储类信息、常量、静态变量、即时编译代码。程序计数器记录当前线程的执行位置。
2. JVM 中堆和栈的区别是什么?
  • 回答思路:
    • 堆用于存储所有对象实例和数组,属于线程共享区域,垃圾回收器会在堆中回收不再使用的对象。
    • 栈用于存储线程的局部变量、方法调用链信息,每个线程都有自己的栈。栈内存较小,生命周期与线程一致。
3. 你对垃圾回收机制了解多少?可以介绍一下不同的垃圾回收器吗?
  • 回答思路:
    • 垃圾回收器通过追踪和清除不可达对象来释放内存,常用的垃圾回收算法包括标记-清除、复制、标记-整理等。
    • 常见的垃圾回收器有 Serial、Parallel、CMS 和 G1。Serial 单线程执行垃圾回收,Parallel 使用多线程执行,CMS 适用于低停顿的应用,G1 适用于大堆内存的服务端应用。
4. Full GC 发生的原因是什么?如何优化避免 Full GC?
  • 回答思路:
    • Full GC 是对整个堆内存(包括新生代和老年代)进行的垃圾回收操作,通常由老年代内存不足、方法区溢出等原因触发。
    • 优化方法包括:增大堆内存大小,减少对象的频繁创建和过长生命周期,调整垃圾回收器设置如 G1 或 CMS,合理设置元空间大小。
5. 双亲委派模型的作用是什么?有遇到过需要打破双亲委派模型的情况吗?
  • 回答思路:
    • 双亲委派模型规定类加载请求会优先委派给父类加载器,以保证核心类库不会被重复加载或篡改。
    • 常见的打破双亲委派模型的场景有 SPI 机制,它需要使用线程上下文类加载器加载自定义的服务实现。
6. 类加载过程有哪些步骤?
  • 回答思路:
    • 类加载分为加载、验证、准备、解析、初始化五个步骤。
    • 加载阶段通过类加载器将字节码加载到内存;验证阶段确保类的合法性;准备阶段为类的静态变量分配内存;解析阶段将符号引用替换为直接引用;初始化阶段执行静态代码块和静态变量赋值。
7. 如何排查 OutOfMemoryError
  • 回答思路:
    • OutOfMemoryError 可能发生在堆、方法区或虚拟机栈中。常见原因有对象过多、类加载过多或栈深度过大。
    • 使用 jmap 生成堆快照,通过 MAT 分析内存泄漏问题;通过 -XX:MaxMetaspaceSize 控制元空间大小;通过 -Xss 调整栈大小。
8. 什么是逃逸分析?它有什么用?
  • 回答思路:
    • 逃逸分析是 JIT 编译器的优化技术,用于判断对象是否逃逸出当前方法。如果对象未逃逸,JVM 可以将其分配在栈上,避免在堆中分配对象。
    • 优点是减少堆内存分配和垃圾回收开销,并且提高对象的访问速度。
9. 什么是内存屏障?在 JVM 中它有什么作用?
  • 回答思路:
    • 内存屏障是一种指令,用于禁止 CPU 的指令重排序。它确保在多线程环境下,某些操作(如读写共享变量)具有可见性和有序性。
    • 在 JVM 中,volatile 关键字可以通过内存屏障确保变量的可见性和有序性。
10. 什么是类卸载?它发生在什么情况下?
  • 回答思路:
    • 类卸载是指 JVM 从内存中移除不再使用的类,通常在不再需要加载的类被垃圾回收器回收时发生。
    • 类卸载主要发生在自定义类加载器加载的类上,当类加载器和其加载的类都没有被引用时,类可以被卸载。
11. Java 8 中永久代的变化?为什么 Java 8 使用元空间代替了永久代?
  • 回答思路:
    • 在 Java 8 中,永久代被移除,取而代之的是元空间。永久代用于存储类的元数据和静态变量等,但容易导致内存溢出。
    • 元空间不使用堆内存,而是直接使用本地内存,从而可以动态调整大小,避免永久代内存溢出的情况。
12. 什么是 JVM 调优?你有实际 JVM 调优的经验吗?
  • 回答思路:
    • JVM 调优是通过调整 JVM 参数和配置来优化 Java 应用的性能。常见调优包括堆内存大小的调整、垃圾回收器的选择、Full GC 频率的控制、线程调度优化等。
    • 实际调优经验可以包括通过 GC 日志分析性能问题、使用工具(如 jmapjstack、VisualVM)来排查内存和线程问题,以及如何根据业务需求配置合适的 JVM 参数。
7.2 综合性 JVM 问题场景分析
  1. 场景一:大型电商系统的 JVM 调优实践

    • 问题:系统在高并发情况下频繁发生 Full GC,导致响应时间延迟。
    • 分析:通过查看 GC 日志,发现堆内存不足导致频繁的 Full GC。通过增加堆大小(-Xms-Xmx),并使用 G1 垃圾回收器替代 CMS,减少了 Full GC 的次数。此外,减少了长生命周期对象的使用,降低了老年代的占用。
  2. 场景二:内存泄漏问题排查

    • 问题:某线上服务随着运行时间增长,内存不断增加,最终抛出 OutOfMemoryError
    • 分析:使用 jmap 生成堆快照,通过 MAT 工具分析堆内存,发现某个静态集合类没有清理不再使用的对象,导致了内存泄漏。解决方案是在代码中定期清理该集合,释放不再需要的对象。
  3. 场景三:多线程应用的 JVM 栈溢出

    • 问题:在处理复杂业务逻辑时,系统抛出 StackOverflowError
    • 分析:由于业务逻辑存在深度递归调用,导致栈深度超出默认值。通过调整 JVM 栈大小参数(-Xss),增加每个线程的栈空间,解决了栈溢出问题。
7.3 常见陷阱与误区
  1. 误区一:JVM 参数设置越大越好

    • 解释:并不是堆内存、栈内存设置得越大越好。过大的堆会导致垃圾回收耗时较长,栈内存设置过大则可能浪费系统资源,甚至引发系统崩溃。应根据应用实际需求来设置合理的 JVM 参数。
  2. 误区二:Full GC 触发时 JVM 会立即回收所有对象

    • 解释:Full GC 是回收整个堆内存,但并不能保证所有对象都被立即回收。如果对象之间存在复杂的引用链,或者引用关系没有正确处理,可能导致对象仍然存活。
  3. 误区三:永久代溢出等同于堆内存溢出

    • 解释:永久代溢出和堆内存溢出是两

种不同的错误。永久代溢出与类的元数据、静态变量等有关,堆内存溢出则是由于对象实例数量过多导致堆空间不足。在 Java 8 之后,永久代已被元空间替代。
在这里插入图片描述

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

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

相关文章

Java 集合详解

目录 一. 概述 二. Collection接口实现类 三. Map接口实现类 四. 线程安全集合 五. List接口下集合实现原理 1. ArrayList实现原理 1.1. 基于动态数组 1.2. 随机访问 1.3. 添加元素 1.4. 删除元素 1.5. 迭代器 1.6. 克隆和序列化 1.7. ArrayList简单使用 2. Link…

重磅发布:OpenAI o1全新推理模型系列

2024年9月12日&#xff0c;OpenAI正式推出全新的推理模型系列——OpenAI o1。这款全新AI模型系列专为解决复杂问题而设计&#xff0c;能够在响应前花费更多时间进行思考&#xff0c;并通过深入推理应对比以往模型更具挑战性的科学、编程和数学问题。 1. 开发背景与首发版本 今…

安装Kali Linux后8件需要马上安排的事

目录 一、更新升级 二、 编辑器 三、用户与权限 四、 下载TOR 五、下载终端 一、更新升级 sudo apt update -y && sudo apt upgrade -y && sudo apt autoremove 二、 编辑器 VScode或者vim&#xff1b;点击.deb就会下载了 一般都会下载到Downloads文件夹中…

读论文-使用潜在扩散模型进行高分辨率图像合成

论文名称&#xff1a;High-Resolution Image Synthesis with Latent Diffusion Models 论文地址&#xff1a;arxiv.org/pdf/2112.10752v2 项目地址&#xff1a;GitHub - CompVis/stable-diffusion: A latent text-to-image diffusion model 潜在扩散模型&#xff08;LDMs&…

Mac使用技巧-来自苹果专人在线辅导服务2

好记性不如烂笔头&#xff01; 其实高效的学习途径还是尽量跟着苹果工作人员在线进行学习&#xff0c;这样一对一&#xff0c;有来有往&#xff0c;学习有反馈&#xff0c;并且很高效&#xff0c;很多东西演示一遍就学会了&#xff0c;自己看还是会花更长的时间。 苹果专人在线…

AI测试|利用OpenAI的文本生成模型,自动生成测试用例的几个场景示例

将人工智能 (AI) 融入软件测试将彻底改变游戏规则&#xff0c;可以显著提高效率和有效性。本文利用 OpenAI 的文本生成模型&#xff08;text generation model&#xff09;&#xff0c;特别是 GPT-3.5-turbo 和 GPT-4-turbo-preview&#xff0c;在 Google Colab 中构建文本生成…

102.SAPUI5 sap.ndc.BarcodeScannerButton调用摄像头时,localhost访问正常,使用IP访问失败

目录 原因 解决办法 1.修改谷歌浏览器的setting 2.在tomcat中配置https访问 参考 使用SAPUI5的sap.ndc.BarcodeScannerButton调用摄像头时&#xff0c;localhost访问正常&#xff0c;使用IP访问时&#xff0c;一直打不开摄像头&#xff0c;提示getUserMedia()问题。 原因…

有关JS下隐藏的敏感信息

免责声明&#xff1a;本文仅做分享&#xff01; 目录 JavaScript 介绍 核心组成 工具 FindSomething ** 浏览器检查 ** LinkFinder URLfinder ** SuperSearchPlus ** ffuf ParasCollector waymore Packer Fuzzer JS逆向 应用&#xff1a; 小结&#xff1a; Ja…

简明linux系统编程--互斥锁--TCP--UDP初识

目录 1.互斥锁 2.信号 2.1介绍 2.2信号的内核机制 3.linux网络编程概述 3.1一览七层协议 3.2一览数据传输过程 3.3四层网络模型 3.4服务端和客户端的数据交互 4.TCP服务端编程 5.TCP客户端编程 6.UDP服务端编程 7.UDP客户端编程 1.互斥锁 互斥锁也是和信号量一样&a…

【C++】——优先级队列和容器适配器

文章目录 优先级队列容器适配器 优先级队列 优先级队列是一种特殊的队列&#xff0c;他的元素出队列顺序并不按照先进先出原则&#xff0c;而是根据元素的优先级来。优先级高的先出&#xff0c;优先级低的后出。(类似于堆) 优先级队列常用成员函数&#xff1a; empty()&#x…

6.C++程序中的基本数据类型

数据类型是指在C中用于声明不同类型变量或函数的一个系统或抽象或者是一个分类&#xff0c;它决定了变量存储占用的内存空间以及解析存储的位模式。其实数据类型可以理解为固定内存大小的别名&#xff0c;是创建变量的模具&#xff0c;具体使用哪种模具&#xff08;包括自定义&…

ai写作软件排行榜前十名,5个软件帮助你快速使用ai写作

ai写作软件排行榜前十名&#xff0c;5个软件帮助你快速使用ai写作 AI写作软件已经成为许多人工作和创作中的重要工具&#xff0c;尤其是在快速生成内容、提高写作效率以及优化文本方面。以下是五款优秀的AI写作软件&#xff0c;它们能够帮助你轻松完成各种写作任务&#xff0c…

芯片级配件产品研发的小众企业生存之路

在半导体行业中&#xff0c;芯片级配件产品的研发一直是一个充满挑战的领域&#xff0c;尤其是对于小众企业而言&#xff0c;如何在技术壁垒高、资金需求大的市场中生存并发展&#xff0c;成为了业界普遍关注的问题。芯片级配件产品涉及到晶圆制造、封装、测试等多个复杂工艺环…

计算机人工智能前沿进展-大语言模型方向-2024-09-20

计算机人工智能前沿进展-大语言模型方向-2024-09-20 1. Multimodal Fusion with LLMs for Engagement Prediction in Natural Conversation Authors: Cheng Charles Ma, Kevin Hyekang Joo, Alexandria K. Vail, Sunreeta Bhattacharya, Alvaro Fern’andez Garc’ia, Kailan…

码头童话,“丈量”行业数智化转型

作者 | 曾响铃 文 | 响铃说 一箱车厘子从地球正对的另一边远渡重洋来到中国&#xff0c;而一旦到达&#xff0c;5个小时内它就能变成北京、天津、河北、河南等区域老百姓果盘里的美味。 这一幕&#xff0c;来自央视联合华为制作发布的《新智中国说-谈智一会间》第一期“码头…

win10下使用docker、k8s部署java应用

在上一篇文章 Windows10上Docker和Kubernetes的安装 中&#xff0c;已经介绍了在 Windows10上安装Docker和Kubernetes &#xff0c;有了这个环境基础之后&#xff0c;就可以用来部署服务了 在项目目录下新建Dockfile文件&#xff0c;内容如下&#xff08;请根据实际情况调整&am…

鸿蒙开发之ArkUI 界面篇 十五 交叉轴对其方式

鸿蒙界面有两个容器一个是Colum、一个是Row&#xff0c;Colum主轴是垂直方向&#xff0c;交叉轴是水平方向&#xff0c;Row的主轴是水平方向&#xff0c;交叉轴是垂直方向&#xff0c;对应方向调整子控件的话&#xff0c;justifyContent调整的是主轴方向的子控件距离&#xff0…

Java发送Outlook邮件:从设置到发送攻略!

Java发送Outlook邮件详细步骤&#xff01;如何使用Java发邮件&#xff1f; Java作为一种广泛使用的编程语言&#xff0c;提供了强大的功能来实现自动化邮件发送。AokSend将详细介绍如何使用Java发送Outlook邮件&#xff0c;从基本的设置到最终的发送过程。 Java发送Outlook邮…

美元降息,对普通人有哪些影响?

美元降息&#xff0c;对普通人有哪些影响&#xff1f; 美元降息了。很多朋友都说我又不炒股&#xff0c;我手里又没有美金&#xff0c;美元跟我有啥关系啊&#xff1f;那我们就来聊聊美元降息&#xff0c;对我们国内经济到底有哪些影响&#xff1f;你再来看看跟你有没有关系&a…

短视频矩阵系统开发|技术源代码部署

产品功能亮点&#xff1a; 1. 支持多账号多平台一键 授权管理 2.支持矩阵视频批量剪辑&#xff0c;批量发布 3. 多平台关键词布局&#xff0c;提升企业及产品曝光 4. 评论区关键词自动回复&#xff0c;意向线索智能挖掘 5. 多账号投放数据统计&#xff0c;省时省力 6. 留资…