JVM基础原理

JVM基础原理

jvm基础

jvm是什么?

jvm是运行在操作系统上的执行.class文件的虚拟机

jvm与操作系统的关系?

相同点都是可以执行对应的程序
不同点是jvm是可执行class文件与操作系统中间的一层(ps:某人说过计算机世界中的问题如果一个中间层不能解决就再加一个☺️)

jvm、jdk、jre之间的关系?

  • 包含关系:JDK ⊃ JRE ⊃ JVM(JDK 包含 JRE,JRE 包含 JVM)
  • JDK = JRE + 开发工具(javac、jar、javadoc 等)
  • JRE = JVM + 核心类库 + 运行时组件
  • JVM 运行在操作系统之上,不是包含关系

JjvW4I.png

java虚拟机规范与Java语言规范之间的关系?

java虚拟机规范是定义执行.class文件的虚拟机的规范;
java语言规范是定义java文件的规范

jvm中的内存划分

java对比C语言引以为豪的就是引入自动内存管理机制,不让使用者在关心内存的管理。

  • 内存划分

Jvwl01.png

JVM 的内存可以按线程共享线程私有分为两大维度、五大区域:

维度 区域 存储内容 异常场景
线程共享 堆 (Heap) 对象实例、数组 OutOfMemoryError: Java heap space
线程共享 元空间 (Metaspace) Class 信息、常量池、方法数据 OutOfMemoryError: Metaspace
线程私有 虚拟机栈 (VM Stack) 栈帧(局部变量表/操作数栈/动态连接/返回地址) StackOverflowErrorOOM
线程私有 本地方法栈 (Native Stack) native 方法的栈帧 同虚拟机栈
线程私有 程序计数器 (PC Register) 当前线程执行的字节码行号 无(唯一不报 OOM 的区域)

JDK8 的关键变化:用元空间 (Metaspace) 替代了 JDK7 的永久代 (PermGen)。元空间使用本地内存而非堆内存,因此不再受 -XX:MaxPermSize 限制,改为 -XX:MaxMetaspaceSize 控制,默认无上限。

为什么元空间移到本地内存? 永久代有固定大小上限,加载大量类(如动态代理、JSP)时容易 OOM: PermGen space。移到本地内存后,类元数据的存储空间更灵活,GC 压力也更小。

各区域异常速查

  • 堆 OOM → 对象太多/太大,或内存泄漏
  • 元空间 OOM → 动态生成大量类(CGLIB、动态代理、JSP)
  • 虚拟机栈 StackOverflow → 递归过深(常见于无心递归)
  • 虚拟机栈 OOM → 线程开太多(每个线程的栈默认 1MB)
  • 直接内存 OOM → NIO 使用 ByteBuffer.allocateDirect() 未释放

堆区

堆是一个由线程之间共享的内存区域。基本类型的对象会中栈上进行分配,而普通类型的对象会栈堆上进行分配。由于栈是线程独立的,因此就不存在并发问题

栈区

  • 虚拟机栈

Java虚拟机栈是基于线程级别的,每个线程都拥有独立的虚拟机栈帧。虚拟机栈的作用是存储栈帧;
在 JVM 中,栈帧(Stack Frame) 是用于支持虚拟机进行方法调用和方法执行的数据结构。它是线程私有的虚拟机栈(Java Virtual Machine Stack)的组成元素,每一个方法从调用开始到执行完毕,就对应着一个栈帧在虚拟机栈中的入栈到出栈的过程。

栈值中分为:

  • 局部变量表(用于存放方法参数和方法内定义的局部变量)
  • 操作数栈(存放方法运行期间的操作数或结果)
  • 动态连接(方法调用过程中的动态连接)
  • 方法返回地址

程序计数器

程序计数器是用来标记当前线程正在执行的程序运行到哪了一行。

元数据空间

元数据空间是JDK8中划分出来的,用于替代JDK7已经之前的Perm区(永久代)
元数据空间存储class信息常量池方法数据方法代码等等

小结

jvm中运行的参数是在上的信息,栈上保存的引用指向的是中的对象,类信息常量等信息则是放在元数据空间中。

对象创建的过程

  • 类加载检查
  • 分配内存空间
  • 设置对象的头部信息(对象的类型、hashcode、锁状态等)
  • 执行init方法(初始化)
  • 返回对象的引用

对象的有两种创建方式,一种是通过TLAB分配内存,另一种是通过CAS + 自旋锁分配内存。

TLAB (Thread Local Allocation Buffer) 的使用场景:

  • 适用场景:
    1. 小对象优先: TLAB 主要用于分配小对象。 这里的“小”是相对于整个堆空间而言的,具体大小由 JVM 的配置参数决定(例如,TLABSize)。通常,这个值设置为几 KB 到几十 KB。
    2. 线程频繁创建对象: TLAB 的优势在于避免了线程间的竞争,因此特别适合线程频繁创建对象的场景。例如,Web 服务器处理大量请求时,每个请求处理线程可能会创建很多临时对象。
    3. 内存分配速度敏感的应用: 由于 TLAB 分配不需要同步,速度非常快,因此对内存分配速度要求高的应用场景非常适用。

CAS + 自旋锁 的使用场景:

  • 适用场景:
    1. 大对象: 当对象的大小超过 TLAB 的限制时,必须直接在共享堆中分配内存。
    2. TLAB 分配失败: 当 TLAB 空间不足时,线程需要从共享堆中重新申请新的 TLAB 或直接分配对象。
    3. 高并发环境: 虽然 TLAB 减少了线程间的竞争,但在并发量非常高的场景下,仍然可能出现多个线程同时尝试在堆中分配内存的情况。
    4. 竞争不是特别激烈的场景: 自旋锁适用于竞争不是特别激烈的场景。如果竞争非常激烈,线程长时间自旋会消耗大量的 CPU 资源。

小结:

  • 优先使用 TLAB: JVM 会尽可能地使用 TLAB 来分配小对象,以提高分配速度和减少线程间的竞争。
  • TLAB 不足时使用 CAS + 自旋锁: 当 TLAB 空间不足或需要分配大对象时,JVM 会使用 CAS + 自旋锁 来在共享堆中分配内存。
  • 动态调整和优化: JVM 会根据具体的应用场景和硬件环境,动态调整 TLAB 的大小和自旋锁的策略,以达到最佳的性能和资源利用率。

更形象的比喻:

  • TLAB 就像每个线程的“私人储物柜”: 线程可以快速地将一些常用的物品(小对象)存放在自己的储物柜中,无需和其他线程竞争。
  • CAS + 自旋锁 就像“公共储物柜”: 如果物品太大或私人储物柜已满,线程需要到公共储物柜中寻找空间。如果公共储物柜正在被其他线程使用,则需要等待(自旋)直到可用。

对象加载的过程

① 类加载检查 → 检查这个类是否已被加载到元空间
② 分配内存 → 在堆上划一块空间(TLAB 或 CAS+自旋锁)
③ 初始化零值 → 所有字段设为默认值(int=0, 引用=null)
④ 设置对象头 → 写入 Mark Word(hashCode/GC年龄/锁状态)+ 类型指针
⑤ 执行 → 调用构造方法,按代码赋值
⑥ 返回引用 → 栈上的变量指向堆中的对象

类的加载机制

流程

加载 > 验证 > 准备 > 解析 > 初始化

执行时机早于,因为在装载到元数据区时就进行执行。

类加载器

  • Bootstrap ClassLoader

最底层的类加载器,用于加载java底层类的类加载器

  • Extention ClassLoader

扩展类的类加载器,用于加载lib/ext下的文件

  • App ClassLoader

应用加载器,用于加载classPath下的文件

  • Custom ClassLoader

自定义的类加载器