JVM详细整理

JVM的体系结构

img

1. 线程私有(无 GC,生命周期同线程)

  • 程序计数器 (PC Register)
    • 作用:当前线程所执行字节码的行号指示器,用于线程上下文切换时的现场恢复。
    • 存储:执行 Java 方法时,存字节码指令地址;执行 Native 方法时,值为 Undefined
    • 特征:JVM 规范中唯一不抛出 OutOfMemoryError 的区域。
  • Java *虚拟机***栈 (**JVM Stack)
    • 作用:Java 方法执行的内存模型。方法执行对应栈帧(Stack Frame)的入栈与出栈。
    • 存储:栈帧(核心包含:局部变量****表操作数栈、动态链接、方法出口)。
    • 特征:请求深度超限抛 StackOverflowError;内存申请不足抛 OutOfMemoryError
  • 本地方法栈 (Native Method Stack)
    • 作用:为 JVM 调用 Native(C/C++)方法服务,机制与 JVM 栈类似。

2. 线程共享(GC 主阵地,生命周期同 JVM 进程)

  • 堆 (Heap)
    • 作用:JVM 内存最大的一块,对象分配与垃圾收集(GC)的核心区域。
    • 存储
      • 绝大多数的对象实例数组
      • 字符串****常量池 (String Table)(JDK 7 移入)。
      • 静态变量 (Static Variables)(JDK 7 移入,随 Class 对象存于堆中)。
    • 特征:内存分配失败且无法扩展时,抛出 OutOfMemoryError: Java heap space
  • 元空间 (Metaspace / 传统方法区的现代实现)
    • 作用:存储类的规范描述信息。核心特性:直接使用物理机的本地****内存 (Native Memory),剥离于 JVM 堆外。
    • 存储
      • 类元数据(类的类型信息、字段、方法字节码等)。
      • 运行时常量池(编译期生成的字面量和符号引用,不含字符串实体)。
      • JIT 代码缓存(即时编译器编译后的机器码)。
    • 特征:受物理内存限制,耗尽时抛出 OutOfMemoryError: Metaspace

JVM功能

  1. 解释执行

    • 核心动作: 把编译好的 .class 字节码,逐行翻译成底层操作系统能懂的机器码,然后立刻交由 CPU 执行。
    • 优缺点: 优点是启动极快(拿到字节码就能跑);缺点是执行效率低(像循环语句,执行100次就要老老实实翻译100次)。
  2. 即时编译

    • 核心动作: 弥补解释器的低效。JIT 会在程序运行时,找出那些被极其频繁调用的代码(即热点代码 Hot Spot),直接将其编译成机器码并缓存起来。
    • 混合模式: 再次遇到热点代码时,直接执行缓存的机器码,速度起飞。现代 JVM 采用的是**“解释器 + JIT”的混合模式**(刚启动时用解释器保证响应,运行久了靠 JIT 提升性能),这就是 Java 程序往往“越跑越快”的原因。
  3. 垃圾回收

    • 核心动作: 自动清理 Java 堆内存中那些“不再被使用的对象”,释放内存空间,让程序员告别手动 free() 和内存泄漏。
    • 两大核心机制:
      • 怎么找垃圾?(可达性分析): 从 GC Roots(如局部变量、静态变量等根节点)出发顺着引用链找,能连上的就是存活对象,连不上的就是可以回收的垃圾。
      • 怎么清垃圾?(分代收集): 把堆内存分为新生代(存放朝生夕死的短命对象,频繁发生轻量级的 Minor GC)和老年代(存放长期存活的老对象,偶尔发生重量级的 Full GC)。

引用类型

  1. 强引用

    • 代码体现Object obj = new Object();
    • 生存态度“宁死不屈”。只要引用链还在,哪怕堆内存已经快炸了,JVM 也绝对不回收它,而是直接抛出 OutOfMemoryError 让程序崩溃。
    • 应用场景:我们日常手写的 99% 的代码都是强引用。
  2. 软引用

    • 代码体现SoftReference<Object> softObj = new SoftReference<>(new Object());
    • 生存态度“视死如归,但能活则活”。当 GC 发生时,如果当前内存还很充足,GC 就不管它;但如果内存马上就要满了(快要 OOM 之前),GC 就会把软引用指向的对象全部回收掉,以此来保命。
    • 应用场景:非常适合做内存****敏感的高速缓存(比如网页缓存、图片缓存)。内存够就留着加速,内存不够就清掉给核心业务让路。
  3. 弱引用

    • 代码体现WeakReference<Object> weakObj = new WeakReference<>(new Object());
    • 生存态度“听天由命”。不管当前内存够不够用,只要下一次 GC *发生,弱引用指向的对象都会被直接回收*。它的生命周期极其短暂。
    • 应用场景:常用于解决特定的内存泄漏问题。比如 Java 底层非常著名的 ThreadLocal 以及 WeakHashMap,都是用弱引用来保证哪怕忘记手动清理,系统也能自动把不再使用的 Key 回收掉。
  4. 虚引用

    • 这个最没存在感,它甚至都无法通过引用拿到真实的对象(get() 方法永远返回 null)。它唯一的用处就是在对象被回收时,给系统发送一个通知。平时业务代码里几乎绝对不会直接用到它。

内存泄漏与内存溢出

  1. 内存泄漏

  • 核心概念: 程序中某些对象明明已经不再被业务使用了,但因为它们还被其他对象引用着(GC Roots 还能顺藤摸瓜找到它们),导致垃圾回收器 (GC) 认为它们还是“活”的,从而无法回收这部分内存。
  • 场景:
    • 静态集合类滥用: 把对象不断塞进全局静态的 HashMapList 中,却从来不执行 remove() 清理。
    • 未关闭的底层资源: 数据库连接、网络连接、IO 流等,用完后没有在 finally 块中调用 close(),导致关联的内存一直被占用。
    • ThreadLocal 遗漏清理: 在线程池场景下,使用完 ThreadLocal 没有及时调用 remove() 方法,这也是高频考点。
  1. 内存****溢出

  • 核心概念: 程序在运行过程中申请内存时,JVM 发现可用内存不足,且经过 GC 之后依然无法腾出足够的连续空间,就会直接抛出 OutOfMemoryError (OOM) 异常。简单来说,就是系统真的没内存了
  • 常见分类:
    • 堆内存****溢出 (Java heap space): 最常见。比如一次性从数据库查出了几百万条数据塞进内存,或者内存泄漏积累到极限,把堆填满了。
    • 元空间溢出 (Metaspace): 运行时加载了太多的类(常见于 Spring/CGLIB 动态生成大量代理类的场景)。
    • 栈溢出 (StackOverflowError): 虽然不带 OOM 字眼,但常一起问。通常是因为方法无限递归调用,把线程栈给撑爆了。
  1. 两者关联:量变引起质变

  • 内存泄漏是一个慢性病,单次泄漏一点点内存,程序通常照样跑,你根本察觉不到。
  • 但如果不加以干预,内存泄漏不断积累,可用内存越来越少,最后连一个正常的新对象都放不下时,就会彻底爆发,导致内存溢出 (OOM)

类加载器

  1. 类加载器 的作用

  • 核心动作: 我们写的 Java 代码编译成 .class 字节码文件后,是安静地躺在硬盘上的。类加载器的作用,就是把这些字节码**“搬运”并读取到** JVM 内存****中,转化成 java.lang.Class 对象,这样程序在运行时才能真正实例化和使用它们。
  1. JVM 的三大核心加载器

JVM 中自带了三个核心的类加载器,它们分工极其明确,级别从高到低排列:

  • ① 启动类(根)加载器 (Bootstrap ClassLoader)
    • 管辖范围: 负责加载 JVM 最底层的核心类库(比如java.lang.Stringjava.util.ListObject 等,存放在 <JAVA_HOME>/lib 目录下)。
    • 注意: 它是用 C/C++ 语言实现的,所以在 Java 代码里获取它的引用时,只会返回 null
  • ② 拓展****类加载器 (Extension ClassLoader)
    • 管辖范围: 负责加载 Java 的一些扩展特性类库(存放在 <JAVA_HOME>/lib/ext 目录下)。
  • ③ 应用程序加载器 (Application/System ClassLoader)
    • 管辖范围: 负责加载当前应用 ClassPath 下的所有类。也就是说,你自己写的业务代码,以及通过 Maven 引入的各种第三方开源库,默认全都是由它来加载的。

双亲委派模型

  • 核心运行逻辑:
    • 当一个类加载器(比如应用程序加载器)收到加载类的请求时,它绝对不会自己先去加载,而是把这个请求“委派”给它的父类加载器(拓展类加载器)。
    • 父类加载器也是如此,继续向上委派,一直到最顶层的启动类加载器。
    • 只有当父级加载器表示:“这个类不在我的地盘,我找不到(加载失败)”时,子加载器才会迫不得已自己动手去尝试加载。
    • 通俗流程: 找爹加载 -> 爹再找爹 -> 最顶级的爹找不到 -> 逐级往下打回 -> 自己动手。

为什么要双亲委派?

  • 防止核心 API 被篡改(沙箱安全机制):** 假设有人恶作剧,自己写了一个名为 java.lang.String 的类,里面植入了恶意代码。因为双亲委派机制,加载请求会一直上报到“启动类加载器”。启动类一查,发现系统核心库里已经有一个绝对安全的 java.lang.String 了,它就会直接加载系统的版本,而**你伪造的那个类永远没有机会被加载。这就保护了 Java 核心基础排面的安全。
  • 避免类的重复加载: 父亲已经加载过的类,儿子就没必要再消耗资源加载一次了,保证了内存中同一个全限定类名对应的 Class 对象只有一个。

类加载的过程

JVM 的类加载机制是指将类的 .class 文件中的二进制数据读入内存,并最终转化为可以被 JVM 直接使用的 java.lang.Class 对象的过程。类的完整加载生命周期主要包含三个核心阶段:加载链接、初始化

  1. 加载

加载是类加载过程的第一个阶段。在此阶段,JVM 主要完成以下三件事情:

  • 获取**字节流:** 通过一个类的全限定名(Fully Qualified Name)来获取定义此类的二进制字节流。
  • 转化数据结构: 将这个字节流所代表的静态存储结构转化为方法区(Method Area)的运行时数据结构。
  • 生成 Class 对象: 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
  1. 链接

链接阶段负责将加载到 JVM 中的二进制数据合并到 JVM 的运行状态中。该阶段又被细分为三个具体的子步骤:

  • 验证
    • 作用: 确保 Class 文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证代码运行后不会危害虚拟机自身的安全。
    • 内容: 包括文件格式验证、元数据验证、字节码验证和符号引用验证等。
  • 准备:
    • 作用: 正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存,并设置其默认初始值。
    • 注意: 此阶段分配的仅是类变量的内存,且赋予的是系统底层数据类型的零值(例如 int 赋值为 0,对象引用赋值为 null),而非代码中显式赋予的值。
  • 解析:
    • 作用: JVM 将常量池内的符号引用替换为直接引用的过程。
    • 概念: 符号引用是以一组符号来描述所引用的目标(如类的全限定名);直接引用则是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
  1. 初始化

初始化是类加载过程的最后一步。直到这一阶段,JVM 才真正开始执行类中编写的 Java 程序代码(字节码)。

  • 核心动作: 执行类构造器 <clinit>() 方法。
  • 机制: <clinit>() 方法是由编译器自动收集类中的所有类变量(静态变量)的赋值动作静态语句块(****static{} **块)**中的语句合并产生的。在这一阶段,类变量才会被真正赋予程序中设定的初始值。

JVM 垃圾回收(GC)

一、 垃圾对象的判定算法

  1. 引用计数法

  • 核心原理: 为每个对象维护一个引用计数器。每当有一个地方引用该对象时,计数器加 1;引用失效时,计数器减 1。当计数器为 0 时,判定对象可被回收。
  • 致命缺陷: 无法解决对象之间循环引用的问题。如果两个对象互相引用,即使它们都不再被外部使用,其计数器也永远不为 0,导致内存泄漏。因此,主流的 JVM 均未采用此算法。
  1. 可达性分析算法

  • 核心原理: 目前 JVM 默认采用的判定算法。它以一系列被称为 GC Roots 的根对象作为起始点,向下搜索并构建引用链。如果一个对象到 GC Roots 没有任何引用链相连(即不可达),则判定该对象为可回收的垃圾。
  • 常见的 GC Roots 包括:
    • 虚拟机栈(栈帧中的局部变量表)中引用的对象。
    • 方法区中类的静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 JNI(Native 方法)引用的对象。
    • 所有被同步锁(synchronized)持有的对象。

二、 基础垃圾回收算法

  1. 标记-清除算法

  • 执行过程: 分为“标记”和“清除”两个阶段。首先通过可达性分析标记出所有需要回收的垃圾对象,随后统一清除被标记的对象。
  • 主要缺点:
    • 效率问题: 标记和清除的过程效率都不算高。
    • 空间碎片问题: 清除后会产生大量不连续的内存碎片,可能导致后续分配大对象时因找不到足够连续的内存而提前触发另一次 GC。
  1. 标记-复制算法

  • 执行过程: 将可用内存按容量划分为大小相等的两块。每次只使用其中的一块。当这一块内存用完时,将存活着的对象集中复制到另一块上,然后把已使用过的那块内存空间一次性清理掉。
  • 优缺点: 解决了内存碎片问题,实现简单,运行高效。但代价是内存空间利用率下降了一半。适合存活率较低的区域(如新生代)。
  1. 标记-整理算法

  • 执行过程: 标记过程与“标记-清除”一致。但后续步骤不是直接清理,而是让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。
  • 优缺点: 解决了空间碎片问题,且不会损失可用内存空间。但移动存活对象需要暂停用户线程(Stop The World),导致回收效率相对较低。适合存活率较高的区域(如老年代)。
  1. 分代收集理论

严格来说这不是一种具体算法,而是一种内存****管理策略。JVM 根据对象存活周期的不同,将 Java 堆划分为新生代老年代,并针对不同区域的特点采用最合适的上述算法:

  • 新生代: 绝大多数对象“朝生夕死”,存活率低。因此采用标记-复制算法(通常划分为 Eden 区和两个 Survivor 区)。
  • 老年代: 对象存活时间长、存活率高。因此通常采用标记-清除算法标记-整理算法

三、 GC 的分类与触发场景

根据回收发生的内存区域,GC 主要分为以下三种类型。其中,减少 Full GC 的发生频率是 JVM 调优的核心目标之一。

  1. Minor GC (新生代 GC)

  • 作用范围: 仅针对新生代(包含 Eden 区和 Survivor 区)进行内存回收。
  • 触发条件: 当新生代的 Eden 区空间不足,无法为新对象分配内存时触发。
  • 执行特点: 发生极其频繁,回收速度极快。存活的对象会被移入 Survivor 区,满足晋升年龄条件(默认 15)的对象将被移入老年代。
  1. Major GC (老年代 GC)

  • 作用范围: 仅针对老年代进行内存回收(注意:在部分语境下,Major GC 常与 Full GC 混用,但严格语义上指代老年代专属的回收,如 CMS 收集器的并发清理阶段)。
  • 触发条件: 老年代空间不足,或者新生代对象晋升到老年代时的空间预估失败。
  • 执行特点: 发生频率较低,耗时通常比 Minor GC 长得多。
  1. Full GC (全堆 GC)

  • 作用范围: 对整个 Java 堆(新生代 + 老年代)以及方法区(元空间)进行全面的垃圾回收。
  • 核心影响: Full GC 是极其昂贵的操作。它会引发全局的 Stop The World (STW) 现象,暂停所有用户线程,直至垃圾回收完成,严重影响系统响应时间。
  • 主要触发场景:
    • 老年代空间不足: 无论是直接创建大对象导致老年代塞满,还是 Minor GC 时的晋升对象大小超过老年代剩余空间(空间分配担保失败)。
    • 元空间 (Metaspace) 耗尽: 加载的类信息过多,达到设定的元空间大小阈值。
    • 显式调用 System.gc() 代码中主动调用此方法会建议 JVM 执行 Full GC(但 JVM 不保证立即执行)。