引言
众所周知,Java的slogan就是"Write once, run anywhere.",这也就意味着无论我们在什么平台的机器上用Java去做实现,都可以在任何支持Java的系统上直接运行,无需做任何额外操作。
Java是如何做到这些的呢?答案是JRE。
- 那么什么是JRE?为什么叫JRE?
- JRE(Java Runtime Environment),是一个Java代码的运行时环境,属于软件层,运行在操作系统软件之上,属于JDK的一部分。
- 什么是JDK?
- JDK(Java Development Kit),每一个JDK都包含了一个兼容的JRE和一个JVM,并且JDK包含了许多Java开发人员常用的工具以及类库,比如
javac
、java
、jar
、jmap
、jstat
、jstack
、jinfo
、rt.jar
等。- 什么是JVM?
- JVM(Java Virtual Machine),JVM可以理解为是一个运行在操作系统之上的虚拟电脑,当我们通过
javac
将*.java
编译成JVM可识别*.class
字节码文件后,再执行java
,此时JVM会将*.class
字节码文件解释成当前操作系统平台可识别的机器码去执行。这样的话就实现了"Write once, run anywhere."。- 整体流程如下所示
JVM类加载的过程
加载阶段:
- 通过类的全限定名来读取class字节码文件的二进制流
- 将字节流中的静态数据结构转化为方法区的运行时数据结构
- 在内存中生成代表这个类的
java.lang.Class
对象,作为方法区中这个类中的各种数据结构的访问入口
- 注意:
- 这只是类加载的其中一个阶段,不要和类加载混淆
- 加载阶段和链接阶段中的部分动作是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始
链接阶段(linking)
- 验证
- 文件格式验证:确保class文件的字节流中包含的信息符合虚拟机规范,并且不会危害虚拟机自身的安全
- 元数据验证:语义分析
- 是否有父类(除
java.lang.Object
外,所有的类都必须有父类) - 是否继承了不该继承的类
- 如果不是抽象类,是否实现了父类或接口中要求实现的所有方法
- 字段、方法是否与父类冲突
- 是否有父类(除
- 字节码验证:
- 确定语义合法、符合逻辑
- 类的方法不会做出危害虚拟机的事件
- 符号引用验证:发生将符号引用转化为直接引用的时候 -> 解析时
- 全限定名是否能找到对应的类
- 指定类中是否存在被引用的方法和字段
- 符号引用中的类、字段、方法的访问性是否可以被当前类访问(private、protected、public、default)
- 准备:
- 为类变量(被static修饰的)在方法区中分配内存,实例变量在对象实例化时分配在Java堆中
- 设置初始值,此时是赋零值,真正的值在初始化时完成赋值;finnal例外,直接赋值;
public static int value = 99; public static final int value = 99; 准备阶段结束后 public static int value = 0; public static final int value = 99;
- 解析:将常量池中的符号引用替换为直接引用
- 类或接口解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
- 这是类加载过程的最后一步,除了在加载阶段用户可以通过自定义类加载器参与,其他阶段皆由虚拟机主导和控制
- 在该阶段开始真正初始化类中定义的Java程序代码(或者说是字节码),调用
<clinit>()
在遇到以下几种情况时,触发初始化:- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射 API 对某个类进行反射调用时,初始化这个类;
- 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
至此,一个字节码文件便已经初始化完成。
本文总结
加载一个class类的过程总体分三个步骤,加载、链接、初始化
,其中链接阶段分为验证、准备、解析
三个阶段,加载阶段通过类的全限定名来读取class字节码文件的二进制流,并将字节码数据转化为方法区的运行时数据结构。链接阶段中的验证阶段对字节码文件进行格式和安全校验,准备阶段为类中的部分变量(被static修饰的变量)分配内存和初始值的赋值,解析阶段将常量池中的符号引用替换为直接引用。初始化阶段会将静态代码的赋值操作和静态代码块中的代码交给<clinit>()
方法进行初始化,完成变量的赋值已经资源的分配。