《揭秘Java虚拟机:JVM设计原理与实现》学习笔记-第一章到第四章
一、Java虚拟机概述
知识点
- CS寄存器保存段地址,IP保存偏移地址。CS和IP这两个寄存器的值能够唯一确定内存中的一个地址
- 函数跳转的本质其实便是修改CS和IP这两个寄存器的内容,使其指向到目标函数所在内存的首地址,这样CPU便能执行目标函数了
- 中间语言由于其本身不能直接被CPU执行,为了能够被CPU执行,中间语言在完成同样一个功能时,需要准备更多便于自我管理的上下文环境,最后才能执行目标机器指令。准备上下文环境最终也是依靠机器码去实现,因此中间语言最终便生成了更多机器码,当然执行效率就降低了
- 通过编译器将Java语言翻译成中间语言,然后再交给虚拟机,其再将中间语言翻译成对应机器平台上的指令
- JVM 内存分为操作数栈、局部变量表、Java 堆、常量池、方法区
常见汇编指令:
jvm指令:
二、Java执行引擎工作原理:方法调用
计算机核心3大功能:
知识点
- 在Linux平台上,栈是向下增长的,从内存的高地址往低地址方向增长,因此每次调用一个新的函数时,需要为新的函数分配栈空间,新函数的栈顶相对于调用者函数的栈顶,内存地址一定是低位方向,因此新函数的栈顶总是通过对调用者函数的栈顶做减法而计算出来
- CPU不支持将数据从一个内存位置直接传送到另一个内存位置,若要想实现这个效果,必须使用寄存器进行中转
- 编译器会将一个方法内的局部变量分配在靠近栈底的位置,而将传递的参数分配在靠近栈顶的位置
- add()函数的方法栈是在调用方main()函数的方法栈空间基础上往下增长的,并且add()方法栈与main()方法栈连在一起
- 物理机器执行call函数调用时,机器会自动将eip入栈。
- 物理机器执行函数调用时,被调用方需要手动将ebp入栈。
- 对于压栈的入参,既可以从通过相对于调用者函数的栈顶的偏移量来相对定位,也可以通过相对于被调用者函数的栈底的偏移量来相对定
- 对于被调用者函数的方法栈内的数据,却不能以调用者函数为基准通过偏移量获取。因为此时被调用函数尚未分配方法栈空间,根本取不到数据,甚至会取到错误的数据。
- 函数返回的一般逻辑是,如果有返回值,就把返回值放在eax寄存器中,然后执行leave和ret指令。如果没有返回值,则直接执行leave和ret指令
方法栈示意图
物理机器调用函数过程:
小结
- 物理机器在执行程序时,将程序划分成若干函数,每个函数都对应有一段机器码。一段程序的机器码都放在一块连续的内存中,这块内存叫做代码段。物理机器为每一个函数分配一个方法栈,方法栈与代码段在地址上没有任何关系,并且只有当物理机器执行到某个函数时,才会为其分配方法栈,否则就不会分配。函数通过自身的机器指令遥控其对应的方法栈,可以往里面放入数值,也可以将数值移动到其他地方,也可以从里面读取数据,也可以从调用者的方法栈里取值。通过一条条指令和一个个栈,物理机器得以运行完一整个程序
知识点:
- 指针函数的返回类型是一个指针,而一般的函数声明所返回的则是普通变量类型。
- 函数指针声明的是一个指针,只不过这个指针与一般的指针不同,一般的指针指向一个变量的内存地址,而函数指针则指向一个函数的首地址。
- 函数指针作为C语言中的高级应用,是实现C语言动态扩展能力的关键技术之一,如同Java中的反射与类动态加载技术
三、Java数据结构与面向对象
知识点
- 一个class字节码文件主要由以下10部分组成:◎ MagicNumber◎ Version◎ Constant_pool◎ Access_flag◎ This_class◎ Super_class◎ Interfaces◎ Fields◎ Methods◎ Attributes
- 目前已发布的Version包括:1.1(45)、1.2(46)、1.3(47)、 1.4(48)、1.5(49)、 1.6(50)、1.7(51)
- 常量池里放的是字面常量和符号引用
- 字面常量主要包含文本串以及被声明为final的常量。符号引用包含类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符
- 常量池是Java字节码文件中比较重要的概念,是整个Java类的核心所在,因为常量池中记录了一个Java类的所有成员变量、成员方法和静态变量与静态方法、构造函数等全部信息,包括变量名、方法名、访问标识、类型信息等
class文件组成成份-10份
四、Java字节码实战
常量池数组组成结构
常量池元素一览表
编号 | 常量池元素名称 | tag位标识 | 含义 |
1 | CONSTRANT_Utf8_info | 1 | UTF-8编码的字符串 |
2 | CONSTRANT_Integer_info | 3 |
整型字面量 |
3 | CONSTRANT_Float_info | 4 | 浮点型字面量 |
4 | CONSTRANT_Long_info | 5 | 长整型字面量 |
5 | CONSTRANT_Double_info | 6 | 双精度字面量 |
6 | CONSTRANT_Class_info | 7 | 类或接口的符号引用 |
7 | CONSTRANT_String_info | 8 | 字符串类型的字面量 |
8 | CONSTRANT_Fieldref_info | 9 | 字段的符号引用 |
9 | CONSTRANT_Methodref_info | 10 | 类中方法的符号引用 |
10 | CONSTRANT_InterfaceMathodref_info | 11 | 接口中方法的符号引用 |
11 | CONSTRANT_NameAndType_info | 12 | 字段和方法的名称以及类型的符号引用 |
常量池元素结构
类的access_flags可选项
fields结构组成
变量的access_flags可选项
知识点
- 根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象全限定名表示。为了压缩字节码文件的体积(字节码文件最终也会占用服务器硬盘资源和内存资源),对于基本数据类型,JVM都仅使用一个大写字母来标识
- 对于数组类型,每一维将使用一个前置的“[”字符来描述,如“int[]”将被记录为“[I”, “String[][]”将被记录为“[[Ljava/lang/String;”
- 用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组“( )”之内,如方法“String getAll(int id,String name)”的描述符为“(I,Ljava/lang/String;)Ljava/lang/String;”
- 在编译期间,编译器会自动为一个类增加void <clinit>()这样一个方法,其方法名就是“<clinit>”,返回值为void。该方法的作用主要是执行类的初始化,源程序中的所有static类型的变量都会在这个方法法中完成初始化,全部被static{}所包围的程序都在这个方法中执行
- 编译器会自动为该类添加一个默认的构造函数
标识字符与基本数据类型对应表
methods结构组成
方法的access_flags
知识点2
- 为了能正确解析class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性
- 这9种表结构有一个共同的特点,即均由一个u2类型的属性名称开始,可以通过这个属性名称来判段属性的类型。该u2类型的属性名称指向常量池中对应的元素。
- 对大多数文件,类名和文件名是一致的,少数特殊类除外(如内部类),此时如果不生成SourceFile属性,当抛出异常时,堆栈中将不会显示出错误代码所属的文件名。
- 虽然JVM所支持的9大属性,其相互之间格式相差甚远,但是都会以一个u2类型的属性名称开始,JVM根据名称便可知道当前描述的到底是这9大属性中的哪一个属性
- attribute_name_index,指向常量池的索引,查找对应的属性表名称
9大属性及其介绍
Code属性结构表
类型 | 名称 | 数量 | 说明 |
u2 | attribute_name_index | 1 | |
u4 | attribute_length | 1 | |
u2 | max_stack | 1 | 操作数栈深度最大值 |
u2 | max_locals | 1 | 局部变量表所需存储空间,Slot |
u4 | code_length | 1 | 字节码长度,jvm限制一个方法不能超过65535条字节码指令,否则拒绝编译 |
u1 | code | code_length | 存放java源码编译后生成的字节码指令,字节流,每个指令都是u1单字节,一个字节能表示256种指令,jvm定义了约200个 |
u2 | exception_table_length | 1 | |
exception_info | exception_table | exception_table_length | |
u2 | attributes_count | 1 | |
attribute_info | attributes | attributes_count |
ConstantValue属性表
Exceptoins属性表
InnerClasses属性表
inner_classes_info表
LineNumberTable属性表
line_number_info表
LocalVariableTable属性表
local_variable_info表
SourceFile属性表
Synthetic和Deprecated属性表 - attribute_length = 0
知识点3
- 字节码文件中名字为<init>的方法表示的是类的构造函数,上文所描述的第一个方法名是<clinit>,这是类型的初始化方法。这两者的区别是:当JVM决定加载某个类型时,会调用<clinit>()方法,而当JVM决定实例化某个类型时,会调用<init>方法。一个是类型初始化,一个是类实例的初始化,两者有本质上的区别。并且<clinit>()一定先于<init>()方法被调用。