极客时间 深入拆解java虚拟机 一至三讲学习总结
为什么要学习java虚拟机
1、学习java虚拟机的本质,是了解java程序是如何被执行且优化的。这样一来,才可以从内部入手,达到高效编程的目的。与此同时,你也可以为学习更深层级、更为核心的java技术打好基础。
2、学习java虚拟机的好处
(一)可以针对自己的应用,最优化匹配运行参数。
(二)可以更好地规避虚拟机在使用中的bug,也可以更快地识别出java虚拟机中的错误。
(三)学习最前沿、最成熟的垃圾回收算法实现以及及时编译器的实现,对以后学习其他的代码托管技术很有帮助。
(四)虚拟机也可以运行其他语言,了解这些语言的通用体制,甚至让这些语言共享生态系统。
3、课程内容
(一)基本原理:剖析java虚拟机的运行机制,逐一介绍java虚拟机的设计决策以及工程实现。
(二)高效实现:探索java编译器,以及内嵌于java虚拟机中的及时编译器,更好理解java语言特性,继而写出简洁高效的代码。
(三)代码优化:介绍如何利用工具定位并解决代码中的问题,以及在已有工具不适用的情况下,如何打造专属轮子。
(四)虚拟机黑科技
第一节 Java代码是如何运行的?
为什么java要在虚拟机里运行?
语法复杂,抽象度高,在硬件上实现不现实。
转换思路:设计一个面向java语言特性的虚拟机,并通过编译器将java程序转换成该虚拟机所能识别的指令序列,也称java字节码(操作码都被固定为一个字节)。然后java虚拟机可以由硬件实现(一次编写,到处运行)
虚拟机附带托管环境(自动内存管理、垃圾回收、还提供诸如数组越界、动态类型、安全权限等动态检测)
java虚拟机具体是怎样运行java字节码的?
1、首先将编译成的class文件加载到java虚拟机中
2、在运行过程中,每当调用进入一个java方法,java虚拟机会在当前线程的java方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。、
3、当退出当前的执行方法时( 不管正常返回还是异常返回),java虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。
4、将字节码翻译成机器码。两种形式:(一)解释执行:逐条将字节码翻译成机器码并执行。(二)即时编译(JIT)将一个方法中包含的所有字节码编译成机器码后再执行。前者的优势在于无需等待编译,后者的优势在于实际运行速度更快。
对不常用的代码解释执行,对小部分热点代码及时编译。
第二节 java的基本类型
java虚拟机中的boolean类型
在java虚拟机规范中,boolean类型被映射成int类型。“true”被映射为整数1,“false”被映射为整数0。
public class Foo {
public static void main(String[] args) {
boolean 吃过饭没 = 2; // 直接编译的话 javac 会报错
if (吃过饭没) System.out.println(" 吃了 ");
if (true == 吃过饭没) System.out.println(" 真吃了 ");
}
}
# Foo.main 编译后的字节码
0: iconst_2 // 我们用 AsmTools 更改了这一指令
1: istore_1
2: iload_1
3: ifeq 14 // 第一个 if 语句,即操作数栈上数值为 0 时跳转
6: getstatic java.lang.System.out
9: ldc " 吃了 "
11: invokevirtual java.io.PrintStream.println
14: iload_1
15: iconst_1
16: if_icmpne 27 // 第二个 if 语句,即操作数栈上两个数值不相同时跳转
19: getstatic java.lang.System.out
22: ldc " 真吃了 "
24: invokevirtual java.io.PrintStream.println
27: return
上述程序运行只会打印“吃了”而不会打印“真吃了”,是因为对于java虚拟机来说,它看到的boolean类型,早已被映射为整数类型。因此,将原本声明为boolean类型的局部变量,赋值为出了0,1以外的整数值,在java虚拟机看来是“合法”的。
java基本类型
正无穷和负无穷分别是是0x7F800000和0xFF800000
0xF800001等对应NaN(Not-a-Number)
0x7FC00000为标准NaN,其他为不标准NaN。
NaN特性:除了“!=”始终返回true之外,所有其他比较结果都会返回false。
java类型的基本大小
栈帧有两个主要的组成部分
(一)局部变量区
在虚拟机规范中,局部变量区等价于一个数组,并且可以用正整数来所引。long、double值需要用两个数组单元来存储,boolean、byte、char、short和int在栈上占用的空间是一样的,引用类型也是一样的。(仅限于局部变量)
值读入字段或数组时相当于做了一次隐式掩码操作。(截取高位字节保证读入数据满足相应的字节要求)
boolean类型只读取最后一位读入字段或数组。
(二)字节码的操作数栈
java虚拟机的算术运算几乎全部依赖于操作栈。我们需要将堆中的boolean、byte、char以及short加载到操作栈上,而后将栈上的值当成int类型来运算。
对于boolean、char两个类型来说,加载伴随零扩展。加载时会被复制到int类型的低二字节,而高二字节会用0来填充。
对于byte、short这两个类型来说,加载伴随符号扩展。加载时会被复制到int类型的低二字节。然后根据正负高二字节用0或1填充。
java虚拟机是如何加载java类的
从class文件到内存中的类,按先后顺序需要经过加载、链接以及初始化三大步骤。
public class Foo {
static boolean boolValue;
public static void main(String[] args) {
boolValue = true; // 将这个 true 替换为 2 或者 3,再看看打印结果
if (boolValue) System.out.println("Hello, Java!");
if (boolValue == true) System.out.println("Hello, JVM!");
}
}
对于上述程序,当替换为2时候无输出,当替换为3时候打印HelloJava及HelloJVM。因为boolean的掩码处理是取低位最后一位,2取低位最后一位为0,3取低位最后一位为1。
java虚拟机是如何加载java类的?
加载
是指查找字节流,并且据此创建类的过程。对于数组类来说,是由java虚拟机直接生成。对于其他类来说,java虚拟机需要借助类加载器来完成查找字节流的过程。
启动类加载器没有java对象,在java中只能用null来指代。除了启动类加载器,其他的类加载器都是java.lang.ClassLoder的子类,因此有对应的java对象。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至java虚拟机中,放能执行类加载。
双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
另外两个重要的类加载器是扩展类加载器和应用类加载器,均由java核心类库提供。
三个重要类加载器功能
1、启动类加载器负责加载最为基础最为重要的类。
2、扩展类加载器的父类是启动类加载器,扩展类加载器负责加载相对次要、但又通用的类。
3、应用类加载器的父类是扩展类加载器,它负责加载应用程序路径下的类。
在java虚拟机中,类的唯一性是由类加载器实例以及类的全名一同决定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。
链接
是指将创建成的类合并至java虚拟机中,使之能够执行的过程。它可以分为验证、准备以及解析三个阶段。
(一)验证阶段
的目的,在于确保被加载类能够满足java虚拟机的约束条件。
(二)准备阶段
的目的是为被加载类的静态字段分配内存。部分java虚拟机还会构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定方法表。在class文件被加载至java虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己的方法、字段的地址。因此,每当需要引用这些成员时,java编译器会生成一个符号引用。
(三)解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载。
初始化
如果要初始化一个静态字段,可以在声明时直接赋值,也可以在静态代码块中对其赋值。如果直接赋值的静态字段被final修饰,并且它的类型是基本类型或字符串时,那么该字段便会被java编译器标记成常量值,起初始化直接由java虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被java编译器置于同一方法中,并把它命名为
类的初始化触发情况:
1、当虚拟机启动时,初始化用户指定的类。
2、当遇到用以新建目标类实例的new命令时,初始化new命令的目标类。
3、当遇到调用静态方法的指令时,初始化该静态方法所在的类。
4、党遇到访问静态字段的指令时,初始化·该静态字段所在的类。
5、子类的初始化会触发父类的初始化。
6、如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化。
7、使用反射API对某个类进行反射调用时,初始化这个类。
8、当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
public class Singleton {
private Singleton() {}
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
static {
System.out.println("LazyHolder.<clinit>");
}
}
public static Object getInstance(boolean flag) {
if (flag) return new LazyHolder[2];
return LazyHolder.INSTANCE;
}
public static void main(String[] args) {
getInstance(true);
System.out.println("----");
getInstance(false);
}
}
1、新建数组会导致LazyHolder的加载吗?会导致它的初始化吗?
答:会加载元素类LazyHolder,不会初始化元素类。
2、新建数组会导致LazyHolder的来链接吗?
答:不会链接元素类LazyHolder;在getinstance(false)时才真正链接和初始化。