前言
最近读周志明的《深入理解Java虚拟机》以及半栈工程师的Java虚拟机文章,对JVM又重新复习了一遍,每次看后收获都不一样(因为没有debug能力,还是很懵懂),担心会忘记将自己读后总结记录下来
总结内容
java代码怎样被操作系统执行
-
java先编译成二进制形式的java字节码放在Class文件中
-
通过jclasslib bytecode viewer插件查看class文件基本格式
2.1 Class类文件的结构(参考《Java虚拟机规范》)
(1)魔数与Class文件的版本
(2)常量池
(3)访问标志
(4)类索引、父类索引与接口索引集合 // 继承关系
(5)字段表集合
(6)方法表集合
(7)属性表集合 -
当运行过程中需要这个类时进行类加载
3.1 配置-XX:+TraceClassLoading JVM参数监控类的加载
3.2 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
3.3 利用ClassFileStream加载class文件转成文件流
3.4 调用ClassFileParser解析文件流生成JVM的数据结构InstanceKlass
(1)在Metaspace(元空间)为 InstanceKlass 分配内存
(2)分析Class文件,填充 InstanceKlass 内存区域
3.5 Dictionary保存ClassLoader加载过的类信息 -
调用主类的main方法
4.1 分配方法对应的Method,将解析方法时读取到信息填充到Method中
4.2 在调用类的static方法时初始化InstanceKlass,调用 link_class()对类进行链接
4.3 最终是通过StubRoutines::call_stub()的返回值来调用java方法的
4.4 通过method找到对应的entry_point例程,并传递给call_stub例程
(1)当方法链接时,会去设置方法的entry_point
(2)例程可以理解为用汇编写好的一个方法
4.5 call_stub准备好堆栈后,就开始前往entry_point处
4.6 entry_point例程就会开始执行传递给它的Java方法了
方法
运行时栈帧结构
Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法 调用和方法执行背后的数据结构
-
局部变量表:用于存放方法参数和方法内部定义的局部变量
1.1 在方法的Code属性的max_locals数据项中确定了该方 法所需分配的局部变量表的最大容量
1.2 reference类型表示对一个对象实例的引用
(1)根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引
(2)根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息
1.3 returnAddress类型已经全部改为采用异常表来代替了
1.4 如果执行的是实例方法,那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用 -
操作数栈
2.1 操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中
2.2 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配
-
动态链接
3.1 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用
3.2 字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数
基于栈的解释器执行过程
- 根据下面代码来描述执行过程中操作数栈和局部变量表的变化情况
- 首先执行偏移地址为0的Bipush指令:将整型推入操作数栈顶
- 执行偏移地址为2的istore_1指令:将操作数栈顶的整型值出栈并存放到第一个局部变量槽中
- iload_1和iload_2指令分别将局部变量表第1个和第二个变量槽中的整型值复制到操作数栈顶,iadd指令将操作数栈中头两个栈顶元素出栈,做整型加法, 然后把结果重新入栈
对象
HotSpot虚拟机对象探秘总结
- Java堆是否规整又由所采用 的垃圾收集器是否带有空间压缩整理(Compact)的能力决定选择空闲列表和指针碰撞分配内存
- 保证内存分配原子性方式
2.1 CAS配上失败重试的方式保证更新操作的原子性
2.2 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):每个线程在Java堆中预先分配一小块内存 - 对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
3.1 对象头包含两类信息
(1)Mark Word: 用于存储对象自身的运行时数据
(2)类型指针:对象指向它的类型元数据的指针 - Java程序会通过栈上的reference数据来操作堆上的具体对象(对象的访问定位)
4.1 句柄访问:reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
4.2 直接指针:reference中存储的直接就是对象地址
new指令源码分析总结
- 获取创建对象所属类地址(InstanceKlass),并将其入栈
- 尝试在TLAB区为对象分配内存,如果分配失败,会直接在Eden区进行分配
- 对象的初始化
3.1 先初始化对象实例数据
3.2 进行对象头的初始化
计算对象大小
- lucene提供的专门用于计算堆内存占用大小的工具类:RamUsageEstimator
<dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId><version>4.0.0</version> </dependency>
- RamUsageEstimator就是根据java对象在堆内存中的存储格式,通过计算Java对象头、实例数据、引用等的大小,相加而得
- 常用方法如下:
//计算指定对象及其引用树上的所有对象的综合大小,单位字节 long RamUsageEstimator.sizeOf(Object obj)//计算指定对象本身在堆空间的大小,单位字节 long RamUsageEstimator.shallowSizeOf(Object obj)//计算指定对象及其引用树上的所有对象的综合大小,返回可读的结果,如:2KB String RamUsageEstimator.humanSizeOf(Object obj)
束语
虽然说没有debug Java虚拟机的能力,但在看相关书籍和文章前对自己提出相关问题,带着目的性去阅读还是能解答很多自己的内心的疑惑,后面继续沉淀吧