JVM
JVM总览
在AI的加持下,似乎整理输出一些查重率几乎100%的文章已经没有什么意义了,毕竟stack overflow的流量都断崖式下跌(终于不用从CSDN里面抛吃的了🫠),从对外部来讲没什么意义不过从对昨天的自己的来讲还是有那么一点点意义,至少在敲下文字的这段时间,没有被空虚迷惘无意义等打扰。
对于传统的物理机,指令集(Instruction-Set Architecture)作为连接硬件与软件的中间层,指令集下面硬件负责实现指令集的语义,指令集上面则是这种抽象的高级语言。
而对于java虚拟机,也可以理解为字节码作为这个中间层,字节码下面是JVM,上面则是java/kotlin/Scala等高级语言。对于c语言,我们从ascii字符开始编译链接成elf文件,然后加载进入进程的地址空间,然后修改pc寄存器,完成状态机的初始化,进而CPU开始无情的执行指令。那么对于jvm呢?我们编译成的class文件,如何被加载进入内存?在jvm中内存布局又是怎样?指令如何执行的呢?
三个概念
specification 规范
implementation 实现
runtime instance 运行实例,每一次java hello 就创建了一个jvm的运行实例
一张图
简单介绍:
java文件被编译成class文件。
class文件就是一个数据结构。
然后将class文件输入给加载器,加载器输出方法区类信息+heap中Class< T >对象。
加载完毕后,执行引擎开始解释执行java字节码,同时对高频代码通过JIT(Java Intime Compiler)编译成汇编代码。
最后在整个过程中垃圾回收,一直默默提供支持。
主要参考资料:https://www.jyt0532.com
Class
现在开始讲解class文件解析。class文件就是一个数据结构,跟elf文件类似。
class文件的15个部分:
1.Magic Number 魔數
2.Version 版本號
3.Constant Pool Count 常量池數量 2个字节,16位故常量最多2^16^ =65,536 个
4.Constant Pool 常量池
5.Access Flags 訪問標誌
6.Class Name 類別名稱
7.Super Class Name 父類別名稱
8.Interfaces Count 接口的數量 = 0001:1个接口
9.Interfaces 接口 = 0005:常量池第5个索引
10.Fields Count 字段的數量 = 0001:1个字段
11.Fields 字段
12.Methods Count 方法的數量
13.Methods 方法
方法与字段的结构:
其中descriptor_index描述了参数类型与返回类型
14.Attributes Count 屬性的數量
15.Attributes 屬性
终极杀器:javap -verbose a.class
https://www.jyt0532.com/2020/03/01/class-file-structure/
public class demo {
int a = 1;
static int b = 2;
static final int c = 3;
final int d = 4;
String e = "ee";
public static void main(String[] args) {
System.out.println(b);
}
}
liaozk@mac-air jvm % javap -verbose demo
Classfile /Users/liaozk/Documents/Foundation/jvm/out/production/jvm/demo.class
Last modified 2025年5月6日; size 739 bytes
SHA-256 checksum 401ff86cf6fc738b07c81d15def8b323ae0c425787457ea2d5637a84319ae113
Compiled from "demo.java"
public class demo
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // demo
super_class: #2 // java/lang/Object
interfaces: 0, fields: 5, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // demo.a:I
#8 = Class #10 // demo
#9 = NameAndType #11:#12 // a:I
#10 = Utf8 demo
#11 = Utf8 a
#12 = Utf8 I
#13 = Fieldref #8.#14 // demo.d:I
#14 = NameAndType #15:#12 // d:I
#15 = Utf8 d
#16 = String #17 // ee
#17 = Utf8 ee
#18 = Fieldref #8.#19 // demo.e:Ljava/lang/String;
#19 = NameAndType #20:#21 // e:Ljava/lang/String;
#20 = Utf8 e
#21 = Utf8 Ljava/lang/String;
#22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#23 = Class #25 // java/lang/System
#24 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Fieldref #8.#29 // demo.b:I
#29 = NameAndType #30:#12 // b:I
#30 = Utf8 b
#31 = Methodref #32.#33 // java/io/PrintStream.println:(I)V
#32 = Class #34 // java/io/PrintStream
#33 = NameAndType #35:#36 // println:(I)V
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (I)V
#37 = Utf8 c
#38 = Utf8 ConstantValue
#39 = Integer 3
#40 = Integer 4
#41 = Utf8 Code
#42 = Utf8 LineNumberTable
#43 = Utf8 LocalVariableTable
#44 = Utf8 this
#45 = Utf8 Ldemo;
#46 = Utf8 main
#47 = Utf8 ([Ljava/lang/String;)V
#48 = Utf8 args
#49 = Utf8 [Ljava/lang/String;
#50 = Utf8 <clinit>
#51 = Utf8 SourceFile
#52 = Utf8 demo.java
{
int a;
descriptor: I
flags: (0x0000)
static int b;
descriptor: I
flags: (0x0008) ACC_STATIC
static final int c;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
ConstantValue: int 3
final int d;
descriptor: I
flags: (0x0010) ACC_FINAL
ConstantValue: int 4
java.lang.String e;
descriptor: Ljava/lang/String;
flags: (0x0000)
public demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #7 // Field a:I
9: aload_0
10: iconst_4
11: putfield #13 // Field d:I
14: aload_0
15: ldc #16 // String ee
17: putfield #18 // Field e:Ljava/lang/String;
20: return
LineNumberTable:
line 1: 0
line 3: 4
line 9: 9
line 11: 14
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this Ldemo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #28 // Field b:I
6: invokevirtual #31 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 14: 0
line 15: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_2
1: putstatic #28 // Field b:I
4: return
LineNumberTable:
line 5: 0
}
SourceFile: "demo.java"
liaozk@mac-air jvm %
总之,理解为java的class数据结构就是属于JVM自己的ELF文件。
Class Loader
前面学习了class文件,现在就是class文件的使用:类加载。
作为一名低级的开发,调包侠是非常合适的一个标签,既然是调包,必然涉及在当前文件中引入其他的类文件。c语言在使用gcc编译时,使用了外部文件需要使用-L选项指定库搜索路径,java这是需要在启动jvm时,通过classpath选项指定搜索路径(当然也可以设置classpath环境变量)。实际看下idea启动时的命令即可发现classpath制定了所有的依赖jar包
输入=类文件(如Hello.class)
输出=有两个:
- 类对象(
Class<Hello>
对象)存在堆里(反射的入口+实例化的模板) - 类的各种信息如字段、方法等存在方法区
过程:
- 加载阶段:读取.class文件并转换为字节码数据
- 内存映射:
- heap 创建Class对象作为运行时访问类的入口
- 方法区(元空间)存储类结构信息,供jvm执行方法解析字段
- 初始化:静态变量赋值,静态代码块
类加载的时机
动态加载,并不是一开始将所有类都加载,而是用到时再加载。
主动使用:完整的加载流程
被动使用:会被加载,但不会被初始化
主动使用-类:
1.用了new 關鍵字 代表新物件被創建
2.static method被呼叫
3.static variable被存取(但compile-time 常數例外)
4.如果此類別的子類被初始化時 此類別還沒被初始化 則會先初始化此類別
5.從命令列被執行時 用戶指定的主類(就是public static void main(String[] args) 所在的類)
6.任何的反射調用
主动使用-接口:
1.static method被呼叫
2.static variable被存取(但compile-time 常數例外)
3.從命令列被執行時 用戶指定的主類(就是public static void main() 所在的類)
4.任何的反射調用
接口与类相比,区别在于:
1.接口没有new
2.子类被初始化时,父类也会被加载,接口则不一定,接口子类被初始化时并不强制要求初始化接口父类
类加载详解
更详细的加载周期:
加载:去类路径下面找到文件的byte流,载入后生成instance of java.lang.Class
验证:
准备:为static variable 分配内存,并给予预设值(int类型默认为0),static final 则赋予给定的具体值
解析:属于java的连接,将class中的全限定名称引用转变为内存地址/偏移量
初始化:将static 非final的变量初始化为各自对应的真正初始值
验证准备解析:又可统称为链接
解析也可以放在初始化之后
加载
在這個階段 有三件事情必須要完成
1.用其中一個加載器 透過一個類別的完全限定名找到.class檔案(字節流)
2.將這個字節流中的類別資訊存進方法區
3.在Heap中 生成一個這個類別的類物件
1.類加載器先去看這個類物件是不是已經在heap裡了
2.如果已經在heap裡了 就從heap回傳class object
3.沒有在heap的話 代表這個type第一次被存取 那類加載器就會去尋找這個class
4.如果找不到 就拋出ClassNotFoundException
5.找到的話 就生成類物件 並且存進heap
加载器
启动类加载器:负责加载$JAVA_HOME/lib下能被jvm识别的类库,无法被java程序引用
扩展类加载器:负责加载$JAVA_HOME/lib/ext/*.jar及系统变量java.ext.dirs 指定的目录下的文件,java程序可以直接用扩展类加载器
应用程序加载器:负责加载用户路径中的文件
Parent Delegation Model
双亲委派模型,更应该叫父亲委派模型:收到加载请求时委派给父类,父类无法加载才会自己去尝试加载
子加载器收到加载java.lang.String
的请求时,会逐级委派至Bootstrap ClassLoader(核心类由其加载
注意,这里不是一种继承关系!
Class Object
类物件,class对象,所有的type都有
Class
Interface
Primitives(byte, short, int, long, float, double, boolean, char)
Void
Arrays
验证
略
Preparation
分配空间&默认值
Data Type | Default Value (for fields) |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | ‘u0000’ |
String (or any object) | null |
boolean | FALSE |
Resolution
想起了c语言的GTO 与PTL 了
oop-klass模型,暂时不了解
大致就是将全限定符号解析为地址引用,有点类似于c的动态链接
Initialization
执行静态代码块
类加载顺序
- 所有的静态初始块会被合并,包括静态变量的声明
- 所有的实例初始化也会被合并
- 实例初始化优先于构造器
Jvm 新增选项 -verbose:class
Runtime Data Areas
大头,重要的部分,状态机的状态。
1.jvm中使用 操作数stack 进行传参,类似汇编中的寄存器
2.每个栈帧都有一个指向运行时常量池中这个栈帧所属方法的指针
3.本地方法有独立的stack
方法区
方法区是所有线程共享,前面提到加载器输入byte array,输出Class< T > 到heap + 字段方法等到方法区
方法区中,每个类有8种matedata
1.Runtime Constant Pool
运行时常量池是每个类有各自的,区别于字符串常量池(heap中,所有类共享jvm只有一个)。
class文件中的常量池+other信息
2.Type Information
类的:全限定名称+接口/类+父类/父接口的全限定名称+访问修饰(public,private,abstract)
3.Field Information
對於一個類別的所有字段 都必須存在方法區
1.字段名稱
2.字段類別
3.字段修飾符(public, private, static, final, transient, volatile)
注意:类static的变量存在于方法区Class variable(下图的f2 与 e)
所以 列出所有排列組合給你
原始類型(primitive type) | 引用類型(reference) | |
---|---|---|
靜態變量(static variable) | 值:方法區 | 引用: 方法區 值: 堆 |
實例變量(instance variable) | 值:堆 | 引用: 堆 值: 堆 |
局部變量(local variable) | 值:棧 | 引用: 棧 值: 堆 |
上範例程式
public class Demo {
public static void main(String args[]){
int a = 1;
Integer b = 2;
Hello c = new Hello();
}
}
public class Hello {
int d = 3;
static int e = 4;
Foo f1 = new Foo();
static Foo f2 = new Foo();
}
public class Foo {
}
4.Method Information
方法:名称+返回类型+参数+修饰符+…
5.Class variable
非 final 的 static 变量
final的static 存在与常量池
6.Method table
简单理解为保存了实例方法的字节码,提供一个entry
7.Reference to ClassLoader table
是那个加载器加载的类
8.Reference to Class object
指向对应的Class对象
堆
存放了所有实例对象,所有线程共享
堆里有一个全局共享的字符串常量池:
及其繁琐的细节就不探究了
注意下面:
String s = "abc";
//可以简单等价为下面,区别:除了在常量池中多了abc 还会多一个new String("abc)
String s = new String("abc").intern();
总结
stack:引用+基本类型
heap:实例+Class
方法区:类的相关信息
Hello hello1 = new Hello();
Hello hello2 = new Hello();
World world = new World();
Execution Engine
加载完成后,运行时数据区描述了状态机的所有初始状态,现在就剩下执行了
类加载完后,字节码存放在方法区,然后由执行引擎一个指令一个指令去执行
解释执行+JIT(Just-in-time compiler)
基本代码都是在interpreter中执行,部分经常调用的会通过JIT编译成机器码
Garbage Collector
每一个javaer 第一次开始写c代码时就会感受到jvm一直以来的默默付出。
where:heap ;方法区中的信息也有可能被回收,但条件非常严格。
why:厨房的垃圾桶,永远都不够大
what:无法被引用的对象
when:
how:
What如何找到那些无法被引用的对象
1.引用计数 - 无法解决循环引用
2.可达性分析:从gc root出发,无法到达的对象就应该被回收
GCroot
略:从各类引用出发
那什麼是GC Root呢 下列的人選都是我們的GC Root
1.JVM棧楨裡的局部變量 指到的對象: 理所當然 棧楨還在 那局部變量指到的都不應該被回收
2.方法區裡的static變數 指到的對象: 理所當然 類別等級的變量 即使沒有任何的實例 只要方法區的類別沒有被回收 那方法區中跟這個類別相關的靜態變數都不該被清理 因為那些靜態變數已經被加載了 代表隨時可能會用到
3.系統類別(System Class): 也就是被啟動類加載器加載的類別 比如說rt.jar
(Runtime enviroment) 或是java.util.*
等類別
4.本地方法棧中的本地方法所引用的對象: 也是理所當然 人家棧裡面的東西都不該清掉 包含了本地方法的局部變數跟全局變量
5.線程(Thread): 所有正在跑的線程以及這些線程所指到的物件
6.在finalizer queue裡面 還沒跑finalizer方法的物件
被這些指到的 都是菁英中心裡面的人 沒被指到的就是垃圾
引用
强
软:即将溢出时,这类引用也会被清理掉
弱:下一次gc会被清理
虚:跟没引用一样
Reference Queue
how 回收
1.Mark-Sweep标记并清除
碎片多
2.Mark-Compact标记并整理
整理耗时,适合垃圾少
3.Mark-Copy
需要额外空间,适合垃圾多
杂糅:
再來就剩下一些細節
1.新大區又稱為伊甸園區(Eden) 新小1稱為Survivor1 新小2稱為Survivor2
2.伊甸區跟Survivor區的比例可以在跑程式的時候自己給定 -Xx:SurvivorRatio
3.在Survivor區存活幾次才進老年代的次數也可以自己給定 -XX:MaxTenuringThreshold
4.如果有個對象太大根本無法在伊甸區分配空間 那就直接在老年代分配
5.如果有對象太大 無法在Minor GC之後放進Survivor區 那也是直接進老年代
override 了 finalize 会给对象一次缓行,第一次GC时,发现有finalize 且未被执行,会缓行,下次GC再回首。
元空间(方法区)的回收,略过。
Object in heap
为什么要再特别讲一下heap中的一个对象存储呢?我想起了21年刚转行入职在b公司,当时将数据库从oracle迁移到mysql,有许多慢sql需要优化(必须承认oracle是如此的强大),当时实在没法优化了,最后搬到内存中来处理连接,当时主管问了一个问题:几十万个对象,会不会传输很久?会不会将内存爆了?那会儿刚自学转行,完全不知道一个对象需要大致占用多少字节。
java在heap中的每个实例,除了自身的实例数据与对其填充外,额外有一个对象头。
Object Header:
Mark Word:标记字段,记录了锁标志位,GC分代,hash等,一般8字节
Klass Pointer:指针,指向方法区类的元数据
数组对象多了长度信息:4字节
不开启压缩指针
class A{
int a = 1;//4字节
boolean b = false;//1字节
classC c ;//指针8字节
}
对象头:8+8 = 16字节
对象实例:4+1+8 = 13字节
对齐:32-29 = 3字节
总计占用内存:32字节。
//jol可以查看对象布局
public static void main(String[] args) {
User user=new User();
//查看对象的内存布局
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
对象头的5种状态:一共有64个bit
更多的关于锁与对象,此处暂略。