JVM的构成精讲

类加载器:把class文件从 磁盘/网络 拉进来放到内存,准备好

运行时数据区:内存的仓库,存放代码、变量等

执行引擎:把Java字节码转化成机器可执行的代码

本地方法接口:负责调用外部代码(eg.C++等)

运行时数据区

虚拟机栈

栈帧:每调用一个Java方法就压入一个栈帧;方法正常返回或者异常退出,栈帧都弹出

内存:固定大小,也可以动态扩展(eg-Xss:设置虚拟机栈大小(比如 -Xss256k))
👉 栈不是存对象的!对象在堆;栈里存的是对象引用。

局部变量表

1.存:方法参数、方法内定义的局部变量

2.是一组Slot(变量槽) 一个slot 32 bit

3.局部变量表在编译期就确定了长度,运行期间大小不变

4.GC可达性:

局部变量表里面的引用,就算代码走到后面不用了,只要引用还在 slot 里,GC 就不会回收对象

可以手动 a = null 辅助回收,或者用 javac -g 调试信息、JIT 优化擦除引用

操作数栈

1.是临时数据栈,基于栈式指令集,用来做计算、传参

2.也是编译期确定最大深度

3.对比:寄存器架构(x86/ARM)是用寄存器;

JVM 是栈式架构,指令简单、可移植,缺点指令多。

4.图 1 中的压 1 压 2 指的是压入【当前栈帧的操作数栈】

动态链接

Java 编译成 class 字节码的时候,不知道方法 / 字段在内存里的真实地址

只能写符号引用

动态链接就是在方法运行时,把栈帧里保存的符号引用,翻译成直接引用(内存地址)的过程

方法返回地址

方法执行完之后,回到调用者方法的位置,方法返回地址记录的就是这个位置

正常退出:执行 return 系列指令,返回值压到调用方操作数栈

异常退出:没有返回值,异常要往上抛,靠异常表处理

注意:异常不属于返回地址,栈帧里有独立的异常处理逻辑。

方法调用流程举例

本地方法栈

本地方法:用 native 关键字修饰的 Java 方法

方法体不是 Java 写的,是 C/C++ 等原生语言写的,放在外部动态库(.so/.dll/.dylib)里

这个方法只有声明,没有 {} 方法体

本地方法栈:专门给 JNI 本地方法(native 方法)服务的栈,和 Java 虚拟机栈是一对

1.存储:
java 复制代码
本地方法的栈帧(C/C++ 的栈帧,不是 Java 栈帧):

本地方法的局部变量
方法返回地址
函数调用上下文
JNI 调用相关信息

//⚠️ 注意:不是存 Java 对象,是原生代码的调用栈。
2.可能抛出的异常:
  • StackOverflowError:线程调用本地方法时,栈深度超过允许的最大栈深度
  • OutOfMemoryError:本地方法栈动态扩展,扩展时内存不够

很多虚拟机(HotSpot)里,本地方法栈和虚拟机栈共用一块内存,所以这两个栈的 OOM / StackOverflow 经常放一起讨论。

3.HotSpot

一句话:JVM 是接口,HotSpot 是这个接口最主流的实现类。

JVM 是一套规范(Specification),只定义运行时数据区、字节码、指令集等抽象规则

HotSpot 是 Oracle 官方默认的 JVM 实现(就是我们平时用的 JDK8、JDK17 默认的虚拟机)

我们日常说的 JVM 调优参数 -Xss、-Xmx、-Xms、-XX:+UseG1GC 全部都是 HotSpot 的参数

HotSpot 没有单独给本地方法栈做独立的内存区域:

  • 虚拟机栈 + 本地方法栈,合二为一
  • -Xss 参数同时控制这两个栈的大小
  • 所以你调 -Xss,既改 Java 方法栈大小,也影响本地方法栈

堆

堆是JVM 中最大的一块内存区域

唯一目的:存放对象实例 + 数组

是 GC 垃圾回收的主战场

线程共享,启动时分配

可通过 -Xms(初始堆)、-Xmx(最大堆)控制大小

整体结构
java 复制代码
Heap
├─ Young 新生代  //存放刚创建、生命周期短的对象,GC 频率很高,这里的 GC 叫 Minor GC(YGC)
│  ├─ Eden 伊甸区
│  ├─ S0  From Survivor
│  └─ S1  To Survivor
└─ Old 老年代   //存放长期存活、大对象:Major GC / Full GC,开销远大于 YGC,速度慢很多。


Full GC 之后还是放不下 → OOM(`java.lang.OutOfMemoryError: Java heap space`)

//比例默认:Eden : S0 : S1 = 8 : 1 : 1(可以用 -XX:SurvivorRatio修改)
//新生代占整个堆默认 1/3,老年代 2/3(-XX:NewRatio 调整)

JDK8 移除了永久代,改用元空间(Metaspace,直接在本地内存,不在堆里)

永久代(PermGen)= JDK7 及之前放类信息的地方,在堆里面

元空间(Metaspace)= JDK8 之后替代永久代,不在堆里,用操作系统本地内存

具体下面方法区部分有涉及

新生代分配规则

对象优先分配在Eden

=>Eden满了=>YGC=>存活的对象复制到 To Survivor

=>...=>再次触发YGC=>Eden + From Survivor里存活的对象复制到To

=> 互换角色(S0 <=> S1来回切换)

1.✅ 一句话理解互换: From 是存存活对象的那个,To 是空的目的地;每次 YGC 完,两者身份交换。

2.对象在 Survivor 熬过一定次数 GC(默认 15 次,-XX:MaxTenuringThreshold),晋升到老年代

方法区

注意:方法区是《Java 虚拟机规范》里定义的逻辑区域,不是物理内存;

不同虚拟机实现不一样,最常用的 HotSpot 有两个大版本变化:

  • JDK1.7 及以前:永久代(PermGen),属于堆的一部分
  • JDK1.8+:元空间(Metaspace),直接使用本地内存(Native Memory),不再在堆里
存什么

1.类型信息

  • 类名、父类、接口列表、访问修饰符
  • 字段信息(字段名、类型、修饰符)
  • 方法信息(方法名、返回值、参数、修饰符、方法字节码)
  • 常量池(运行时常量池,重点!)

2.运行时常量池

  • 编译期生成的字面量(字符串、数字)、符号引用,class 文件里的常量池加载后放到这里
  • 支持动态:String.intern()、动态生成类、反射动态生成常量

3.静态变量

JDK1.7 之前静态变量在永久代;JDK1.7 静态变量移到堆,1.8 继续在堆

4.静态常量

5.方法表

6.注解信息

相关推荐
今天的砖头有点烫手啊1 天前
Java 对象内存布局:压缩指针、对齐填充与 JOL 实测
java·jvm
凯凯姜1 天前
JVM 内存持续增长与 Full GC 频繁
jvm
驭渊的小故事1 天前
从 JVM 底层看泛型:泛型擦除、Signature 属性与 Super Type Token 模式
java·开发语言·jvm
香蕉大香蕉(06敬畏市场)2 天前
高频八股 · Java 并发编程(10):异步、ThreadLocal 与虚拟线程(收官)
java·开发语言·网络·jvm·python·ai·实习
wuminyu2 天前
JVM规范学习总结-Class文件结构
java·linux·c语言·jvm·c++
酌沧3 天前
MemT让记忆成为模型内生能力
jvm·人工智能·算法
wuminyu3 天前
JVM规范学习总结-Java源码经过javac编译后产物
java·linux·c语言·jvm·c++
学到老活到老4 天前
类已经加载了,为什么静态代码还没执行?
java·jvm
学到老活到老4 天前
写了 new,就一定会在堆上分配对象吗?
java·jvm