看完《深入理解Java虚拟机》后,稍微整理下知识点。
JVM内存区域
jvm在运行过程中会把它的内存分为若干个不同的数据区域,这些运行时数据区域包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。

程序计数器:
线程私有的,如果当前线程正在执行一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是本地Native方法,那这个计数器值为空。字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令。
Java虚拟机栈:

线程私有的,每执行一个方法的时候,都会创建一个栈帧,对于执行引擎来讲,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,被成为“当前栈帧”。
每一个栈帧包含了局部变量表、操作数栈、动态连接、方法返回地址等。
局部变量表:
存储变量值的,最小单位是变量槽,一个变量槽存放的32位以内的数据类型有boolean、byte、char、short、int、float这几种,reference表示对象实例的引用,它的长度和是32位还是64位虚拟机有关。而对于long、double这两种一般是64位长度,用高位对齐的方式,为它们分配两个连续的变量槽。
如果执行的是实例方法(也就是没有被static修饰的方法),那么局部变量表的第0位索引的变量槽默认存的是该方法所属的对象实例的引用,所以可以通过this来访问到实例的参数。其余的局部参数则按照参数表的顺序排列,从1开始。
局部变量表的变量槽是可以重用的,因为方法中的变量,其作用域并不一定会覆盖整个方法体,所以如果字节码PC计数器的值已经超过了某个变量的作用域了,那这个变量的变量槽就可以给其他变量使用。
操作数栈:
又叫操作栈,一个后入先出栈。方法的执行过程也就是各种字节码指令在操作数栈中入栈出栈的过程。操作数栈32位数据类型所占的栈容量为1,64位的为2,栈的最大深度在编译的时候就被写入到了class文件中Code属性的max_stacks数据项中,方法的执行过程中操作数栈的深度不会超过最大深度。
动态连接:

指向运行时常量池中该栈帧所属方法的引用。也就是将符号引用转为直接引用。
方法返回地址:
方法的退出过程相当于把当前栈帧出栈,所有可能的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,同时调整PC计数器的值来指向调用指令后面的一条指令(也就是执行调用该方法的地方的下一行代码)。
方法正常退出的时候,调用地方的PC计数器的值会做为返回地址,异常退出时,返回地址通过异常处理器表来确定。
本地方法栈:
和虚拟机栈类似,区别只是虚拟机栈执行的是Java方法,本地方法栈执行的是Native方法。
Java堆:
Java堆(Java Heap)是虚拟机内存中最大的一块。线程共享的。存放的是对象实例和数组。
又叫GC堆,因为它是垃圾收集器管理的内存区域。
对象内存布局
对象在堆内存中的存储布局划分为三个部:对象头、实例数据、对齐填充。
对象头
一部分存储对象本身的运行时数据:对象HashCode、GC分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳。
另一部分存储类型指针:对象指向它的类型元数据的指针。
如果对象是一个Java数组,还得有一块用于记录数组的长度。
实例数据
存储定义的各种类型的字段内容。
对齐填充
HotSpot虚拟机要求对象的大小必须是8字节的整数倍,对象头已经满足了,如果实例部分没有对齐,需要通过对齐来补全。
对象的访问定位
有两种,通过句柄访问和通过直接指针访问。
通过句柄访问对象

Java堆中可能会划分出一块内存来作为句柄池,reference存储的是句柄的地址,句柄中包含对象实例数据的地址和对象类型数据的地址。
优点:对象移动时只需要改变句柄中的实例数据指针。
缺点:访问需要两次指针定位。
通过直接指针访问对象

reference存储的直接就是对象示例数据的地址,而对象实例数据中可能要包含一个指向对象类型数据的指针。
优点:速度快,对象访问在Java中较为频繁,它节省了一次指针定位的时间,积少成多会节省大量时间。
方法区:
线程共享的。又叫非堆(Non-Heap),为了和Java堆区分开来。
jdk7之前:方法区在堆中,或者说使用堆中的永久代来实现方法区。
jdk7:将永久代中的字符串常量池、静态变量等放到Java堆中。
jdk8:移除永久代,将剩下的类型信息等放入元空间,元空间使用的是直接内存。
运行时常量池:
方法区的一部分。编译后得到的class文件,里面会存有常量池表,里面存放各种字面量和符号引用,这些会在类加载后存放到运行时常量池中。
类加载过程中的解析,就是将运行时常量池里的符号引用替换成直接引用的过程。
JVM类文件结构

所谓类文件也就是.class后缀的文件,我们知道jvm只认识class文件,这种class文件的结构也是很早就被确定下来,绝大部分在1997年第一版《Java虚拟机规范》中就已经定义好了。
class文件是一组以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件中,中间没有任何分隔符。如上图所示。
魔数与class版本
头四个字节是Java魔数,0xCAFEBABE。
接下来四个自己是版本号,第5、6字节是主版本号,第7、8字节是次版本号。
常量池
接下来的一大片没有固定长度的区域就是常量池,常量池中主要存放两大类变量:字面量和符号引用。
字面量:
文本字符串、被声明为final的常量值
符号引用:
被模块导出或者开放的包
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
方法句柄和方法类型
动态调用点和动态常量
访问标志
常量池结束后的2个字节代表访问标志,比如是类还是接口,是public吗,是final吗。
注意这个是类的访问标志。
类索引、父类索引与接口索引集合
字段表集合
依次为访问标志 、名称索引、描述符索引、属性表集合。
存的是类级变量和实例级变量。
方法表集合
和字段表一样依次为访问标志 、名称索引、描述符索引、属性表集合。
同理存的是方法的内容。
属性表集合
把属性表单独拿出来说,是因为在字段表和方法表都有。
Code属性:
code字段存的是代码经编译器处理后的字节码指令。
max_stack存的是操作数栈深度最大值。
max_locals存的是局部变量表的存储空间。
JVM类加载机制
Class文件并非只有存在于具体磁盘中的文件,它应当是一串二进制字符流,包括但不限于磁盘文件、网络、数据库、内存或者动态产生等。
类加载过程

一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段。
其中加载、验证、准备、初始化、卸载这五个阶段的开始的顺序是确定的,但是可能不一定是按部就班的完成,有可能这个阶段还没结束,下个阶段就开始了,所以他们是互相交叉地混合进行的。
加载
干了三件事:
1)通过一个类的全限定性名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这类的各种数据的访问入口。
干完这个后,外部的二进制字节流就按照虚拟机设定的格式存储在方法区中了。
验证
包括**文件格式验证、元数据验证、字节码验证、符号引用验证。**这个是一般程序设计的必要操作,JVM当然也有这一步,为的就是避免去运行了不符合规范的文件造成风险。
准备
给类中定义的变量(即static修饰的变量)分配内存并设置类变量初始值。
关键词static修饰的变量,注意这里如果加了final变成static final,那就是常量了,常量会直接赋值,从字段表的属性表中的ConstantValue获取。而不是常量的静态变量会给一个初始值,比如int的初始值是0,boolean的初始值是false等等。
| 数据类型 | 初始值 |
|---|---|
| byte | (byte) 0 |
| short | (short) 0 |
| int | 0 |
| long | 0L |
| char | ‘\u0000’ |
| float | 0.0f |
| double | 0.0d |
| boolean | false |
| 所有引用类型 | null |
解析
将常量池内符号引用替换为直接引用的过程。
包括类或接口的解析、字段解析、方法解析、接口方法解析。
初始化
初始化阶段就是执行类构造器<clinit>()方法的过程。
86万+

被折叠的 条评论
为什么被折叠?



