你好啊, 我是Alan。
今天这篇文章像你介绍一下关于JVM的类加载系统, 看完这篇文章你会弄清楚下面这几个问题
- 基本类型和引用类型都变成了什么?
- 类加载系统是什么流程
当我们使用javac命令编译完成一个java文件之后, 我们会得到一个.class, 而借助类加载器我们就将其加载到JVM的运行时数据区中。
如果从实现上的角度来看Java有两种数据类型, 一种是基本数据类型, 另外一种是引用数据类型, 基本类型也就是常见的int, long, float这种, 而引用数据类型分为类、接口、数组类和泛型类。
基本数据类型是由JVM预定好的, 其次数组类是由JVM根据代码实际情形进行生成的, 类class,接口interface是经过编译之后的.class文件。
说完了上面这些, 下面我们就来看看JVM对.class干了些什么? 主要分为三个部分, 分别是加载、链接和初始化。
一、加载
首先是加载, 在JVM中, 类加载是指把类的二进制数据加载到内存中的过程, 而去负责这个过程的东西叫做类加载器。
所有的类加载器都有一个祖先类加载器, 这个祖先类加载器叫做启动类加载器, 除了启动类加载器之外, 那就是其它加载器, 这些加载器都继承了java.lang.ClassLoader。
这些加载器本身就是Java写的, 需要先由启动类加载器加载进JVM中, 才能对其它类进行加载, 这种类型的加载器主要有扩展类加载器, 应用类加载器,
除了这些由Java负责提供的加载器之外, 我们还可以自定义加载器, 关于这点,
在加载的时候还有一个小小的规则, 那就是子类加载器在加载的时候, 会将加载的权力转交给父类加载器, 如果父类加载器没有找到, 才会让子类加载器去进行加载。
下面就来细说一下这些不同的加载类都加载了什么?
-
启动类加载器, 负责加载加载的是基础类, 比如JAVA_HOME目录下和lib目录下的类库。
-
扩展类加载器的父类加载器是启动类加载器, 可以加载JRE的lib下jar包中的类,
-
应用类加载器的父类加载器是扩展类加载器, 它负责加载系统变量java.class.path或者CLASSPATH所指定的路径
关于这些不同的加载器, 我画了一张图放在了下面,同时一个类的唯一性正是有加载该类加载器的实例和该类的名称所确定
我们也可以通过代码去验证一下:
public class Main{
public static void main(String[] args) {
ClassLoader classLoader = Main.class.getClassLoader();
System.out.println("Main.class的classLoader:" + classLoader);
System.out.println("Main.class loader的父:" + classLoader.getParent());
System.out.println("Main.class loader的爷:" + classLoader.getParent().getParent());
}
}
这段代码输出的结果是:
这里的PlatformclassLoader就是ExtLoader加载器, 爷加载器为null, 是因为启动类加载器是C++写的, 其实是启动类加载器。
通过这些不同的加载器完成查找功能, JVM下一步就是进行转换, JVM会将这些东西转换成为运行时数据区域中的数据。
二、链接
第二个大的步骤是链接, 在这阶段主要有三个小的步骤, 分别是验证、准备阶段和解析阶段。
首先是验证阶段, 这个阶段通常是确保我们加载进来的.class文件是否符合JVM的规范, 在安全领域有一种技术叫做字节码注入, 就是通过混淆.class文件来完成的。
在这个阶段JVM通过语义分析来确保字节码里的内容和Java语言的标准一致, 例如检查一个父类是否正确, 虚拟机也会深入的研究数据流和控制流, 也就是判断指令是不是有效的, 除此之外虚拟机还会检查字节码中的符号名称, 确保它们能够被正确的解析。
为了更加有效的完成类的加载, 可以采取一些措施, 比如使用-Xverifynone参数, 这样可以有效的减少虚拟机的类加载时间。
准备阶段的目的是给加载类的静态字段分配内存, 对于一个方法调用来说编译器会为它生成一个包含目标方法所在类的名字、目标方法的名字、接收类型以及返回值类型的符号引用, 在运行阶段这个符号引用会定位到对应的信息上去。
解析阶段主要负责将这些符号引用转换为对应的实体, 如果一个符号引用指向一个未被加载的类或者未被加载的字段或方法, 那么在解析阶段就会负责加载这个东西。
三、初始化
在变量让新手感到JavaScript的诡异这篇文章中, 我说到静态类型的编程语言在变量初始化阶段, 总会为它赋上一个对应类型的值, 比如int往往会赋上0, 而在JavaScript中只会为未初始化的变量赋上undefined值, 因为Java是静态类型的编程语言, 所以初始化都是按照变量类型来的。
类加载的最后一步, 初始化, 就是为了标记为常量值的字段赋值,以及执行方法的过程, JVM会通过加锁的方式来确保类的方法只被执行一次。
常量值是指被赋值的 final所修饰的 静态字段 并且类型为基本数据类型或者字符串, 有JVM来完成初始化。
final static int a = 1;
那么指的是什么呢? 是指静态字段被直接赋值的, 以及静态代码块中的代码,会被放在clint方法中,
static int a = 2;
static {int b = 1;}
当初始化完成的时候, 类才正式成为可以执行的状态。
说完静态字段的初始化, 那就是类的初始化了, 那类的初始化会发生在哪种情况之下呢? 主要是下面这些情况
- 遇到新建目标类实例的new指令, 调用静态方法的, 访问静态字段的指令, 这个时候会初始化它们所在的目标类。
- 子类的初始化, 会触发父类的初始化
- JVM启动的时候, 初始化用户所指定的类
- 接口定义了default方法, 那么直接实现或者简洁实现该接口类的初始化, 会触发该接口的初始化。
- 使用反射API对某个类进行反射调用的时候, 初始化这个类
- 当初次调用MethodHandle实例时候, 初始化MethodHandle指向方法所在的类。
下面我们来看一个例子:
public class Singleton {
private Singleton() {
}
private static class LazyHolder {
static final Singleton SINGLETON = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.SINGLETON;
}
}
当我们调用Singleton.getInstance的时候, 会调用静态字段, 这时候会触发类LazyHolder的初始化, 然后是new Singleton()创建实例, 同时类初始化是线程安全的, 并且仅仅执行一次, 所以只有一个实例。
好了, 到这里本篇文章就结束了, 本文主要介绍了关于Java类加载系统背后的相关知识, 如果你有更多的想法欢迎与我交流