一.JVM是什么
JVM是Java Virtual Machine的缩写,即JAVA虚拟机。JVM 是运行Java 程序的跨系统平台。JVM针对不同操作系统,实现了不同版本JVM,为JAVA程序运行屏蔽了底层操作系统差异,使得JAVA程序具备跨平台特性。
随着JVM 发展, 已经支持运行其它语言,例如 groovy 脚本语言, Scala函数式语言,Kotlin、JRuby 等。
二. JVM 架构模型
JVM 是基于栈的指令集架构设计的, 实现跨平台特性。
目前主流有两种架构指令集:
基于栈式架构:
1.设计实现简单,适用资源受限的系统。
2.避开寄存器分配难。使用零地址指令方式分配。
3.由于指令都是零地址,指令运行依赖于操作栈,指令集小,编译容易实现。
4.不需要硬件支持,可移植性好,更容易实现跨平台。
基于寄存器架构:
1.指令集架构完全依赖硬件,可移植性差。
2.指令执行高效,性能强。
3.花费更少的指令执行操作。
4.大部分基于寄存器的指令,都是基于 地址 指令 设计。
典型的 X86二进制指令,以及 Android 的 Davlik 虚拟机指令。
二.JVM内存结构
JVM运行时,内存结构由程序运行相关的 程序计数器, 虚拟机栈, 本地方法栈 和 方法区,堆 组成。
程序计数寄存器(Program Counter Register):
寄存器用来存储下一条指令的地址,让执行引擎读取下一条指令。每个线程栈对应一个寄存器。线程中断或恢复时,通过寄存器保存执行指令地址,可以快速恢复执行。
JAVA虚拟机栈(JVM Stacks):
每个线程在创建时,都会对应创建一个虚拟机栈。虚拟机栈内部由一个个栈帧(Stack Frame)组成,一个栈帧对应一个方法的调用。栈帧由局部变量表,操作栈,动态链接,方法返回地址组成。
1.当虚拟机栈设置为固定大小时,可能抛出 StackOverflowError 异常。(-Xss 设置虚拟机栈最大可用空间)
2.当虚拟机栈设置为动态扩展时,可能抛出 OutofMemoryError 异常。
JVM对虚拟机栈的栈帧进行压栈 和出栈操作。遵循 先进后出 / 后进先出 原则。在运行线程中,具体某个时间点,只有一个活动栈帧,即栈顶栈帧是有效的。这个栈帧称为当前栈帧(Current Frame),
对应的方法称为当前方法(Current Method),定义当前方法的类,称为当前类(Current Class)。执行引擎只针对当前栈帧操作。
JAVA 有两种退出当前方法的方式,一种是方法正常返回,使用 return 指令(void 返回值默认在方法最后面添加return 指令);另一种是抛出异常。这两种方式,最终会导致栈帧被弹出。
局部变量表(Local Variables):是一个数字数组,主要用于存储方法的参数(形参)和在方法内定义的局部变量。数据类型包括 基本数据类型,对象引用 以及 returnAddress 类型。在方法内定义局部变量是线程私有,不存在数据安全问题。即不会出现被其它线程修改,导致出现并发问题。但由形参传递过来的引用类型则看具体使用情况。比如:String类型是final修饰的类,不可变类,虽然是引用类型,但也不存在并发问题。 局部变量表的大小在编译期间就确定了,保存在方法code属性 maximum local variables 的数据项中。运行期不会改变大小。方法调用结束后,随着方法栈帧一起销毁。
局部变量表 的基本存储单元是变量槽(Slot),32bit(4字节)长度。数据类型中,用32bit(4字节)来表示的类型(int,char),占用一个变量槽(包括 returnAddress 类型),64bit(8字节)(long,double)的占用两个变量槽。
byte,short, char 在存储前都会转换为 int类型。boolean 也会转换成int类型, 0-表示false,非0表示true。
当前方法是实例方法时,该对象引用this将会保存在局部变量第一个变量槽。即index = 0。其它变量依次按参数定义顺序排列。
局部变量表的槽位是可以重复使用的。
局部变量表的变量(除this变量以外)必须手动赋值,否做无法使用。
局部变量表的变量是垃圾回收的跟节点,只要被局部变量表直接或间接引用的变量,都不会被回收。
操作数栈(Operand Stack)或表达式栈:在方法执行过程中,根据字节指令,往栈中写入数据或提取数据,即入栈(push)和出栈(pop)。
操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈深度在编译期间确定好,具体值保存在 code属性中max_stack的值。
操作数栈中可以保存任何数据类型。32bit(4字节)占一个栈单位深度,64bit(8字节)占两个栈单位深度。
操作数栈通过标准入栈(push)或出栈(pop)操作来完成一次数据访问。非访问索引式访问。
解释引擎是基于栈的执行引擎,而操作数栈就是栈的具体实现。
栈顶缓存技术(Top-of-Stack): 操作数存储在内存中,频繁的读取会影响执行速度。HotSpot JVM的设计者提出栈顶缓存,将栈顶元素全部缓存在物理CPU寄存器上,降低对内存的读写次数,提高执行引擎的执行效率。
动态链接(Dynamic Linking): 每个栈帧内部都有一个引用,记录该方法在运行时常量池中的位置。这个引用是为了支持当前方法的代码实现动态链接(Dynamic Linking)。invokeDynamic 指令。Java源文件编译到字节码文件中时,变量和方法引用都作为符合引用(Symbolic Reference)保存在class文件的常量池中。动态链接将这些符号引用转换成调用方法的直接引用。
方法返回地址(Return Address):调用该方法的PC寄存器的值。
附加信息: 与JVM实现相关的一些附加信息。对程序调试提供支持的信息。
本地方法栈(Native Method Stacks): Java调用非Java代码的接口。本地方法,在Java中用native修饰的方法,在C/C++中通过JNI方式实现。通过本地方法,Java可以直接和操作系统交互。
堆区(Heap): 在JVM启动时,即创建堆。堆是JVM管理的最大一块内存空间。Java 所有线程共享堆空间,并且线程还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)。对象实例和数组默认都分配在堆上。方法结束后,堆中对象并不会马上被回收,而是在垃圾清理时进行。GC(Garbage Collection,垃圾收集器) 执行垃圾回收主要就是关注堆回收的效率。
-Xms: 表示堆的起始内存大小。等价于 -XX:InitialHeapSize。默认情况,物理电脑内存 / 64。
-Xmx: 表示堆的最大内存空间。等级于 -XX:MaxHeapSize。 默认情况,物理电脑内存 / 4 。
当堆内存超过 mx 配置大小,抛出 OutofMemoryError 异常。
生产环境,一般将 -Xms 和 -Xmx 设置为相同的值,避免垃圾回收重新分配堆大小,提高性能。
堆中的对象分为两类:
1.生命周期较短的瞬时对象。
2.生命周期非常长的对象。
配置堆中 新生代 和 老年代的结构占比:
默认 -XX:NewRatio=2,表示新生代占1,老年代占2,即新生代占整个堆的1/3。
Eden 和 两个Survivor 空间 默认配置 -XX:SurvivorRatio=8,即默认占比例 8:1:1。
对象的内存布局:
在HotSpot虚拟机中,对象在内存中分成三个部分: 对象头(Header), 实例数据(Instance Data), 对齐填充(Padding)。
32位中:
MarkWord:4字节;
class对象指针(Klass Pointer): 4字节。
实例数据(Instance Data): 实际数据大小。
对齐填充(Padding): 按8 字节对齐。
64位中:
MarkWord:8字节;
class对象指针(Klass Pointer): 8字节。开启压缩指针则4字节。jdk7之后默认开启。
实例数据(Instance Data): 实际数据大小。
对齐填充(Padding): 按8 字节对齐。
- 对象头(Header):
- Mark Word(标记字段):关于锁的信息。对象的Mark Word部分占4个字节/8个字节,表示对象的锁状态(比如轻量级锁的标记位,偏向锁标记位),另外还可以用来配合GC分代年龄、存放该对象的hashCode等。
- Klass Pointer(Class对象指针):Class对象指针的大小也是4个字节/8个字节,其指向的位置是对象类型对应的Class信息实例(其对应的元数据对象)的内存地址。
- 数组长度:如果对象是数组类型,占用4个字节/8个字节,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
- Instance Data(对象实例数据):这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节。
- padding data(对齐):如果上面的数据所占用的空间不能被8整除,padding则占用空间凑齐使之能被8整除。被8整除在读取数据的时候会比较快。
实例分析:
java.lang.String 类:
对象 | 字节数 | 对齐填充字节 | 实际内存大小 | 备注 |
---|---|---|---|---|
Byte | 13 | 3 | 16 |
|
Short | 14 | 2 | 16 | |
Integer | 16 | 0 | 16 | Integer 对象的字段其实只有value 一个,其他都是类变量。
实际内存大小 = 对象头 + value属性 + 填充对齐字节 = 12 + 4 + 0 = 16字节 |
Long | 20 | 4 | 24 | |
Float | 16 | 0 | 16 | |
Double | 20 | 4 | 24 | |
Boolean | 13 | 3 | 16 |
|
Character | 14 | 2 | 16 | |
String | 20 | 4 | 24 |
Mark Word(标记字段) 8 字节, Klass Pointer(Class对象指针) 4字节(开启压缩指针), 实例字段 hash 是int 类型 4 字节,实例字段 value char数组,对象引用, 4字节,补齐 4字节。合计24字节。实际内存大小 = 对象头 + hash 字段 + value字段 + 填充对齐字节 = 12 + 4 + 4 + 4 = 24字节 |
Object | 12 | 4 | 16 |
实际内存大小 = 对象头 + 填充对齐字节 = 12 + 4 = 16字节 |
|
24 | 0 | 24 |
实际内存大小 = 对象头 + modCount字段 + size字段 + elementData字段 + 填充对齐字节 = 12 + 4 + 4 + 4 + 0 = 24字节 |
|
44 | 4 | 48 |
实际内存大小 = 对象头 + 字段 + 填充对齐字节 = 12 + 8 * 4 + 4 = 48字节 |
对象分配过程概述:
1.当程序创建一个对象时(大部分是通过new关键字),优先保存到伊甸园(Eden)。
2.当伊甸园空间满时,这时还需要创建对象,则GC对伊甸园进行垃圾回收(Minor GC), 将伊甸园中不再被其它对象所引用的对象进行销毁。再将需要创建的对象放到伊甸园区。
3.然后将伊甸园中剩余的对象移动到幸存者0区(Survivor 0)。
4.再次GC后,将伊甸园和幸存者区0 没有回收的对象放到 复制到 幸存者1区(Survivor 1)。
5.之后垃圾回收,重新 3 和 4 步骤。
6.当经历了多次Minor GC垃圾回收,对象还没被回收,则将对象保存到老年代(Old)。 一般默认是15次,不同JDK版本会有差别。具体可以通过参数 -XX:MaxTenuringThreshold=<N> 设置。
7.当老年代满时,触发 Major GC,进行老年代垃圾清理。
8.若老年代清理后,依然无法保存对象,则抛出内存溢出异常。 java.lang.OutofMemoryError: Java heap space
对象分配过程 - TLAB:
JVM在堆区为每个线程分配了一个私有缓存区域。每个线程私有,避免一些线程安全问题,同时提高内存分配吞吐量。因此也叫快速分配策略。
逃逸分析:
1.栈上分配。将堆分配转换成栈分配。
2.同步省略。一个对象被发现只有一个线程被访问到,则对这个对象操作不考虑同步。锁消除。
3.分离对象 或标量替换。
标量(Scalar)是指一个无法再分解成更小的数据的数据。Java 中原始数据类型就是标量。
聚合量(Aggregate)是可以分解的数据。Java的对象就是一个聚合量,可以再分解成 聚合量 和 标量。
方法区:
方法区在JDK7之前方法区叫永久代(Permanent generation),JDK8之后使用元空间(Metaspace)取代方法区。元空间不在虚拟机设置的内存中,而是使用本地内存。
方法区具体存储被JVM加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等。
CodeCache(JIT编译优化):
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通