什么是字节码指令
- 字节码指令是包含在字节码中的指令,是jvm可执行的指令,可以说是jvm层面的汇编语言,或者说是java代码的最小单元。
字节码指令格式
java虚拟机的指令由一个操作码和零至多个操作数构成。
- 操作码:一个字节长度,代表某种特定操作含义的数字。(总数不超过256个)
- 操作数:代表此操作的参数。
字节码指令分类
- 加载与存储指令:用于将数据在栈帧中的局部变量和操作数栈中来回传输。
- 运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上算术指令可分为两种:对整形数据进行运算的指令和对浮点型数据进行运算的指令。
- 类型转换指令:可以将两种不同的数值类型进行相互转换。
- 对象创建与访问指令:用于创建和访问对象。
- 操作数栈管理指令:用于直接操作数栈。
- 控制转移指令:用于让java虚拟机有条件或无条件地从指定位置继续执行程序。
- 方法调用和返回指令:
方法调用指令:用于调用各种方法(实例方法,接口方法,实例初始化方法,私有方法,父类方法)。
返回指令:用于返回各种类型的返回值。 - 异常处理指令:java虚拟机处理异常采用异常表来完成,不采用字节码指令完成。
- 同步指令:java虚拟机支持方法级的同步和方法内部一段指令。
虚拟机类加载机制
java虚拟机把类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程称为虚拟机的类加载机制。
因为java里类型的加载,连接和初始化都是在程序运行期间完成的(动态加载,动态连接),这为java提供了极高的拓展性和灵活性。
类加载特性
- 类加载子系统负责从文件或网络加载class字节流
- 类加载子系统会读取字节码中的信息,运行时存储到JVM内存
- 任何Class要被加载,都要符合JVM字节码规范
类加载过程
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,会经历7个阶段。
其中,验证,准备,解析统称为连接
加载阶段
- 读取字节码二进制流
- 解析字节码二进制流的静态数据转换为运行时JVM方法区数据
- 在内存中生成一个代表这个类的java.lang.Class对象,放入堆中,作为方法区的访问入口
- 在加载类过程中,必然触发父类加载
Class实例何时被创建
- new实例化 A a = new A();
- 反射 Class clzA = Class.forName(“com.uestc.A”);
- 子类加载时父类同时加载
- JVM启动时,包含main方法的主类
- 1.7动态类型语言支持
连接阶段
验证
连接的第一步,目的是确保class文件的字节流中包含的信息符合规范。
准备
准备阶段正式为类中定义的变量(即静态变量,被static修饰的变量),分配内存并设置初始值,这些变量都在方法区中进行分配。
注意:这时候进行内存分配的只有类变量,不包括实例变量,实例变量会在对象实例化时随着对象一起分配在java堆中。
例如:public class A{public static int a = 100;}
准备阶段:a = 0
初始化阶段:a = 100
解析
解析是连接的核心阶段,是java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是将字节码中的静态字面关联转换JVM内存中的动态指针关联。
- 类解析
- 字段解析
- 方法解析
- 接口解析
初始化
加载、验证、准备、解析都是由虚拟机主导的,与代码无关。
初始化则是通过代码生成clint,完成类初始化操作,也就是说初始化阶段是执行类构造器方法< clint >()的过程。
- < clint >()不是在java代码中直接编写的方法,它是javac编译器的自动生成物。
- < clint >()方法用于完成类的初始化操作,对类(静态)变量赋值,执行static代码块。所以,如果没有类变量和静态代码块,就不会产生< clint > ()方法。
- 子类的< clint > 方法会优先执行父类的< clint >()方法。因此,java虚拟机中第一个被执行的< clint >()方法的类型是java.lang.Object。
- 接口和类一样,都会生成< clint >()方法(接口不能使用静态代码块,但是仍有变量初始化操作),但执行接口的< clint >()方法不需要先执行父接口的< clint >()方法,因为只有当父接口中定义的变量被使用时,父接口才会初始化。接口的实现类在初始化时也不会执行接口的< clint >()方法。
- 在多线程环境中,< clint >()方法默认会增加同步锁,只有第一个线程会执行< clint >(),其他线程处于阻塞等待状态,当第一个线程执行< clint >()后,代表该类加载完毕,后续线程不会再执行< clint >()。确保< clint >()方法只执行一次。
类加载器
三层类加载器
面试题:Class实例在JVM是全局唯一的吗?
不是,同一个Class被不同的类加载器加载后在JVM中产生的类对象是不同的。
推导:在同一个类加载器作用范围内Class实例加载时才会保持唯一性。
双亲委派模型
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。
因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
双亲委派的优点
- 保护了类不会被重复加载
- 禁止用户污染java开头的核心包,保证java程序的稳定
面试题:自定义类java.lang.Cusom运行时会有什么问题?
会报错,java.lang开头的包都是由启动类加载器加载,启动类加载器不会加载自定义类,所以自定义类包名不能是系统的核心包名。