JVM运行机制
基本概念
计算机是没有办法直接运行java代码的,也没有办法直接运行java编译的字节码。在实际的运行过程中,需要将编译好的字节码交给JVM去执行。JVM会把字节码转化为实际的机器码
栈
栈(stack)又称为堆栈活堆叠,栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进后出的原则存储数据。
JVM运行方式
虚拟机常用的实现方式有两种:基于栈的和基于寄存器的。我们的JVM是基于栈的。
这两种虚拟机的区别在于,基于寄存器的虚拟机,执行速度会更快,但是可移植性差。
基于栈的虚拟机,虽然指令比寄存器的多,但是可移植性好,实现更简单。
类字节码
我们都知道,java是需要编译的。当.java命令被编译后称为.class文件。这个.class文件就是类字节码文件。
类字节码结构
类字节码由十个部分组成
- 魔数(Magic Number)
- 版本号(Minor&Major Version)
- 常量池(Constant Pool)
- 类访问标记(Access Flags)
- 类索引(This Class)
- 超类索引(Super Class)
- 接⼝表索引(Interfaces)
- 字段表(Fields)
- ⽅法表(Methods)
- 属性表(Attributes)
魔数
java的魔数其实就是标识它是一个java文件的标志。当我们用010editor打开.class文件时,发现它的前四个字节是CAFEBABE(咖啡宝贝)
常量池
对于JVM字节码来说,如果操作数非常小或者很常用的数字0之类的,这些操作数是内嵌到字节码中的。如果是字符串常量和较大的整数等,class文件是把这些操作数存储在一个叫做常量池(Constant Pool)的地方,当使用这些操作数时,使用的是常量池数据中的索引位置。常量池的作用有点类似于C语言中的符号表,但是比符号表要强大得多。
常量池的基本机构如下:
1 | { |
字段表与方法表
字段表:类中定义的字段会被存储到这个集合中,包括类中定义的静态和非静态字段,不包括方法内部定义的变量。
1 | { |
方法表:类中定义的方法会被存储在这里,与字段表很类似,方法表也是一个变长结构。
1 | { |
属性表中的Code
Code属性包含了所有方法的字节码,它的结构如下:
字节码指令的基本结构
java虚拟机的指令由一个字节长度的操作码和紧随其后的操作数构成。”字节码”这个名字的由来也是因为操作码的长度用一个字节表示。
大部分字节码指令包含了所要操作的类型信息。比如ireturn 用于返回一个int类型的数据 return 用于返回一个double类型的数据。
后面的操作数一一在常量池中有对应。比如#15其实就是printStream
中的println
方法。而invokervirtual就是指调用实例方法。
常见指令:
- ldc -> 将int,float或String型常量值从常量池中推送⾄栈顶(宽索引)
- xload -> 将指定类型的本地变量推送⾄栈顶 (x代表任意字符串)
- xstore -> 将指定类型的栈顶值存⼊本地变量中 (x代表任意字符串)
- pop -> 将栈顶数值弹出(数值不能是long或double类型的)
- dup -> 复制栈顶数值并将复制值压⼊栈顶
- xadd -> 将栈顶两指定型数值相加并将结果压⼊栈顶 (x代表任意字符串)
- goto -> ⽆条件跳转 • xreturn -> 从当前⽅法返回指定类型 (x代表任意字符串)
- invokevirtual -> 调⽤实例⽅法
- invokespecial -> 调⽤超类构建⽅法, 实例初始化⽅法, 私有⽅法
- invokestatic -> 调⽤静态⽅法
- invokeinterface -> 调⽤接⼝⽅法 • invokedynamic -> 调⽤动态⽅法
JAVA类加载
java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到jvm中。其中类装载器的作用其实就是类的加载。
类加载的具体过程可以拆分为三个步骤,分别是加载\链接以及初始化。
这些工作实际都在jvm当中完成。(在java中它们被封装成native函数)
类加载种类
Boostratp ClassLoader(启动类加载器)这个类加载器负责将一些核心的,被JVM识别的类加载进来,用c++实现,与JVM是一体的。
Extension Classloader(扩展类加载器)这个类加载器用来加载java的扩展库 *Application Classloader(App类加载器/系统类加载器)用于加载我们自己定义编写的类。
User ClassLoader(用户自己实现的类加载器)当实际需要自己掌控类加载过程时才会用到,一般没有用到。
因此除了Bootstrap ClassLoader之外,其他的ClassLoader都是可以在java代码中找到的
类加载的方法
- loadClass(加载指定的Java类)
- findClass(查找指定的java类)
- findLoaderClass(查找JVM已经加载过的类)
- defineClass(定义一个Java类)
- resolveClass(链接指定的java类)
类加载的两种方式
Java类加载方式分为显示和隐式,显示即我们通常使用java反射或者ClassLoader来动态加载一个类对象。而隐式指的是类名.方法名()或new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载去加载任意的类。
1 | //通过反射加载类 |
通过反射去加载实际还是使⽤了的ClassLoader,这两者在本质上并⽆区别
什么是双亲委派机制
定义:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类时),子加载器才会尝试自己去加载。这个机制就叫做双亲委派机制。
实现:
- 检查请求的类是否已经被加载过了。
- 未加载,则请求父类加载器去加载对应路径下的类。
- 如果加载不到,才由下面的子类依次去加载。