type
status
date
slug
summary
tags
category
password
icon
字节码结构
使⽤ hexdump 命令可以⽤来查看我们的 16 进制字节码。
如下图所示,字节码⽂件不使⽤分隔符:
实际案例
- magic: 文件类型,4字节,用于区分文件的类型,比如class,txt,zip等
- minor_version:次版本号,2字节
- major_version:主版本号,2字节
- constant_pool:常量池
- constant_pool_count:常量池中常量个数,2字节
- constant_pool[constant_pool_count-1]:实际存储常量的池,默认0位预留以后使用,所以长度少一位
常量池的每个常量是通过 Tag + [info…] 组成的,详情查看下面的常量表示,
例如:
07 表示 CONSTANT_Class,表示常量类名,其结构为
查到常量池 21 的 Tag 为 01 表示 CONSTANT_Utf8_info ,其结构为


- access_flags:可见行标识,2字节,public,final…
- this_class:当前class名,2字节,指向常量池中的位置
- super_class:父class名,2字节,指向常量池中的位置
- interfaces:接口信息
- interfaces_count:接口数量,2字节
- interfaces[interfaces_count]:存储接口的
- attributes:附加属性
- attributes_count:附加属性数量,两字节
- attributes[attributes_count]:存储附加属性的

反编译字节码
- 使⽤
javap
反编译可以看到⽂件基本结构
- 使⽤
javap -c
会得到每个⽅法的Code
信息
- 使⽤
javap -v
可以看到更详细的内容
- 在 Code 信息中可以看到我们的操作数栈的⼤⼩,本地变量表的⼤⼩和传⼊参数数量
- 虚拟机字节码指令表 在字节码中每个指令都⽤⼀个字节来表示
- 通过字节码来学习
- 内部类 内部类会持有外部类的原因是在内部类中⽤⼀个成员变量记录了外部类对象 在 Kotlin 中,如果在内部类中没有使⽤到外部类,那么不会持有外部类
- 泛型 Java 的泛型擦除并不是将所有泛型信息全部都擦除了,会将类上和⽅法上声明的泛型信息保存在字节码中的 Signature 属性中,这也是反射能够获取泛型的原因。但是在⽅法中的泛型 信息是完全擦除了
- synchronized 关键字
synchronized 会在⽅法调⽤前后通过 monitor 来进⼊和退出锁
JVM 运⾏时数据区

- 被所有线程共享的
- 堆
- ⽅法区
- 运⾏时常量池
- 每个线程私有的
- 本地⽅法栈(native method stacks)
- 局部变量表:要考虑参数中的局部变量,和类的成员方法中隐式的this引用
- 操作数栈:方法运行时,会将代码解析成对应的字节码指令,每个字节码指令可能需要从操作数栈中获取参数,获取返回参数到栈中。
- 动态连接:
- 返回地址:
- 程序计数器
虚拟机栈
虚拟机栈,每执行到一个方法,会把这个方法的栈帧(相当于每个方法都是一个栈帧)压入到栈中,执行完成后栈帧会出栈,栈帧中具体的内容如下:

堆(heap): new 出来的对象都存放在堆中,对象在堆中的结构为:
- 对象头
- 类型指针
- 数组⻓度(对象是数组时才有)
MarkWord
MarkWord 在32位虚拟机中的结构如下:
⽆锁状态中的 hashcode 是懒加载的,⼀个对象⼀旦计算过 hashcode 就被不会成为偏向锁。 ⽽⼀个偏向锁状态的对象,⼀旦计算过 hashcode (Object 默认的或者是 System 类提供的),那么会⽴即升级到重量级锁。 锁只能升级不能降级,⽆锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

- 具体数据
- 对⻬填充

一个类的实例化过程通常包括以下步骤:
- 类的加载(Loading): 在 Java 中,类的加载是由类加载器(ClassLoader)负责的。当程序第一次使用某个类时,类加载器会尝试将该类的类文件加载到内存中。类加载器通常会根据类的全限定名(包括包名)查找类文件,并将类的字节码加载到内存。
- 链接(Linking): 类加载完成后,进行链接,分为三个阶段:
- 验证(Verification): 验证确保加载的类符合 Java 语言规范,不会破坏虚拟机的状态。
- 准备(Preparation): 在这个阶段,为类的静态变量分配内存,并将其初始化为默认值。这里不包括在代码中显式赋值的初始化。
- 解析(Resolution): 将类、接口、字段和方法的符号引用转化为直接引用。解析阶段可以在运行期间完成也可以在编译期完成。
- 初始化(Initialization):
在类的初始化阶段,虚拟机会按照程序员指定的主动使用类的方式(例如通过
new
关键字创建类的实例,或者调用类的静态方法/字段)来执行类的初始化。初始化阶段包括对静态变量的赋值和执行静态代码块。
- 创建对象: 当类被加载、链接和初始化之后,就可以通过
new
关键字来创建该类的实例。new
操作符会在堆内存中为对象分配内存空间,并调用对象的构造方法进行初始化。
- 构造方法执行: 在对象创建后,会执行构造方法来完成对象的初始化工作。构造方法可以进行一些初始化操作,包括设置实例变量的初始值、执行其他方法等。
- 返回对象引用:
new
操作符执行完毕后,会返回该对象的引用,该引用可以被赋给一个变量,或者作为方法的返回值。
垃圾回收
主要发生在虚拟机的 heap(堆)区

- JVM 堆的结构划分
- 新⽣代
- eden (80%):当一个对象被 new 出来时,会被放入伊甸区。
- from (10%):当每次伊甸区满了,会把幸存下来的对象放到 from 区,
- To (10%):当伊甸区和 from 区都满了,触发 minor GC 会把幸存下来的对象,移动到 to 区,并交换 from 和 to 区,保证 to 区,每次都是空的。
- ⽼年代
- 对象会在新⽣代的 eden 区域中创建(⼤对象会直接进⼊⽼年代)
- 在 age 达到⼀定值时会移动到⽼年代(具体值可以在 JVM 中配置)
- 在 minor GC 时,存活对象数量⼤于 to 区的容量,那么会直接进⼊⽼年代
每次 GC 幸存下来的对象的年龄都会加 1
第⼀次 eden 区满了以后进⾏ minor GC 将存活对象 age + 1,然后放⼊ from 区域,将 Eden 区域内存清空。以后每次 minor GC 都将 eden 和 from 中的存活对象 age + 1 ,然后放⼊ to 区域,然后将 to 区 域和from 区域互相调换。
对象什么时候会被放入老年代?
- 垃圾回收算法
- 标记-清除算法 (会产⽣空闲内存碎⽚)
- 标记-整理算法(防⽌产⽣内存碎⽚)
- 复制算法(效率最⾼,但是内存利⽤率低)
JVM 中新生代使用复制算法,老年代使用标记整理算法
- 关于跨代引⽤ 为了防⽌不能确定新⽣代的对象是否被⽼年代的对象引⽤⽽需要进⾏ full GC 。 通过 card table 将⽼年代分成若⼲个区域,如果老年代的某些对象引用新生代的对象,或者新生代的对象引用了老年代的对象,那么这些老年代的对象会被放在一个单独的区域,并通过 card table 进行标记,所以在 minor GC 时只需要对表中记录的⽼年代区域进⾏扫描就可以了。

从字节码的角度上简单解释一下为什么 Java 的内部类会引用到外部类,Kotlin 会有这种情况吗?
字节码中,内部类会有一个外部类对象的属性,并通过内部类的构造方法传入了外部类的引用,然后赋值给定义的外部类对象。
如果代码中 Kotlin 的内部类没有引用到外部类的对象,则编译的 class 文件,内部类不会有外部类的对象引用。
简单说下 MarkWord 的结构,为什么偏向锁不在 Markword 里面记录 hashcode?
无锁状态,前 25 位表示该对象的 hashcode,接着 4 位表示对象的 GC 年龄,后面是 0 01。
偏向锁,前 23 位记录线程的信息,接着两位表示时间,接着 4 位表示对象的 GC 年龄,后面是 1 01。
轻量级锁,前 30 位指向栈中锁的指针,后两位 00。
重量级锁,前 30 位指向重量级锁的指针,后两位 10。
记录不下,要记录线程
简单说下 JVM 堆的结构划分
分为新生代,老年代,永久代。
新生代中有包含 eden 区,和两个 surviver 区(from 、to) 。
简单说下 JVM 各个区域使用的垃圾回收算法,如何防止跨代引用
新生代 使用的复制算法,当 eden 区满了后,会把存活的对象复制到 from 区,会根据情况复制到 to 区,以及老年代。
老年代 使用标记整理算法。
JVM 中会有一个 CardTable,CardTable 类似一个数组,包含多个区域,并对老年代进行划分相同多个区域,CardTable 中每个区域会记录对应的老年代对应的区域是否有跨代引用。
JVM对象创建流程
1. 类加载
- 当 JVM 遇到
new
关键字时,首先会检查该类是否已经加载到内存中。如果没有加载,JVM 会通过类加载器(ClassLoader)加载该类。
- 类加载过程包括加载、验证、准备、解析和初始化等步骤。
2. 内存分配
- 一旦类加载完成,JVM 会在堆内存中为该对象分配内存空间。内存的大小在类加载时就已经确定(根据类的成员变量等)。
3. 初始化成员变量
- 分配内存后,JVM 会将对象的成员变量初始化为默认值:
- 数值类型(如
int
,float
等)初始化为0
或0.0
。 - 布尔类型初始化为
false
。 - 引用类型初始化为
null
。
4. 执行构造代码块和初始化代码
- 如果类中有实例初始化块(非静态代码块),JVM 会执行这些代码块。
- 然后,JVM 会执行类的构造函数。构造函数可以显式地初始化成员变量或执行其他操作。
5. 返回对象引用
- 构造函数执行完毕后,JVM 会返回该对象的引用。此时,对象创建完成,程序可以通过该引用操作对象。
示例代码
执行流程
- 类加载:
MyClass
类被加载到内存中。
- 内存分配:为
MyClass
对象分配内存。
- 初始化成员变量:
x
初始化为0
,name
初始化为null
。
- 执行实例初始化块:输出
"实例初始化块执行"
。
- 执行构造函数:输出
"构造函数执行"
,并将x
和name
初始化为10
和"Java"
。
- 返回对象引用:
obj
指向新创建的对象。
注意事项
- 如果类有父类,会先执行父类的构造函数(通过
super()
调用)。
- 静态代码块和静态变量的初始化在类加载时完成,而不是对象创建时。
- 对象创建过程中可能会抛出异常(如内存不足或构造函数中的异常),需要适当处理。
这就是 Java 中对象创建的基本流程。
- 作者:shuouyang
- 链接:https://notion-tree.vercel.app/article/b850a9db-44be-497d-8f90-bfe6bcd9760d
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。