一、定义
![](https://img.haomeiwen.com/i4807654/54d6a00806dd3a53.png)
从 class 文件到内存中的类,按先后顺序需要经过加载、链接以及初始化三大步骤。
二、Java 语言的类型
1.基本类型(primitive types)
Java 的基本类型,是由 Java 虚拟机预先定义好的。
类型 | 值域 | 默认值 | 虚拟机内部符号 |
---|---|---|---|
boolean | {false,true} | false | Z |
byte | [-128,127] | 0 | B |
short | [-32768,32767] | 0 | S |
char | [0,65535] | '\u0000' | C |
int | [-231,231-1] | 0 | I |
long | [-263,263-1] | 0L | J |
float | ~[-3.4E38,3.4E38] | +0.0F | F |
double | ~[-1.8E308,1.8E308] | +0.0D | D |
Java 的基本类型都有对应的值域和默认值。可以看到,byte、short、int、long、float 以及 double 的值域依次扩大,而且前面的值域被后面的值域所包含。因此,从前面的基本类型转换至后面的基本类型,无需强制转换。
他们的默认值看起来不一样,但在内存中都是 0。
2.引用类型(reference types)
- 类:有对应的字节流
- 接口:有对应的字节流
- 数组类:由 Java 虚拟机直接生成
- 泛型参数
无论是直接生成的数组类,还是加载的类,Java 虚拟机都需要对其进行链接和初始化。
由于泛型参数会在编译过程中被擦除,因此 Java 虚拟机实际上只有前三种。
三、加载
1.定义
查找字节流,并且据此创建类的过程
对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。
对于其他的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。
2.类加载器
![](https://img.haomeiwen.com/i4807654/281acb46803fda61.png)
- 启动类加载器(bootstrap class loader):由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)
- 扩展类加载器(extension class loader):Java 核心类库提供。负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)
- 应用类加载器(application class loader):Java 核心类库提供。负责加载应用程序路径下的类。(这里的应用程序路径,是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
- 自定义类加载器:除了由 Java 核心类库提供的类加载器外,可以加入自定义的类加载器,来实现特殊的加载方式。比如可以对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。
除了加载功能之外,类加载器还提供了命名空间的作用。
在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,往往借助这一特性,来运行同一个类的不同版本。
启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。
3.双亲委派模型
一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。
好处:双亲委派模可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。
打破双亲委派:重写loadClass方法
四、链接
链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
1.验证
确保被加载类能够满足 Java 虚拟机的约束条件。
Java 编译器生成的类文件必然满足 Java 虚拟机的约束条件。
2.准备
为被加载类的静态字段分配内存。
部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。
在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。
举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。
3.解析
将第二阶段的符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。
五、初始化
如果要初始化一个静态字段,可以在声明时直接赋值,也可以在静态代码块中对其赋值。
如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。
类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。
只有当初始化完成之后,类才正式成为可执行的状态。
类的初始化触发时机
- 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();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
只有当调用 Singleton.getInstance 时,程序才会访问 LazyHolder.INSTANCE,才会触发对 LazyHolder 的初始化(对应第 4 种情况),继而新建一个 Singleton 的实例。
由于类初始化是线程安全的,并且仅被执行一次,因此程序可以确保多线程环境下有且仅有一个 Singleton 实例。
极客时间《深入拆解 Java 虚拟机》 学习笔记 Day07 - http://gk.link/a/11WCN
网友评论