JVM 的类加载机制和 Java 的类加载机制相似,但 JVM 的类加载过程稍有些复杂。
JVM 经过加载 .class 文件,可以将其中的字节码解析成操做系统机器码。那这些文件是怎么加载进来的呢?又有哪些约定?接下来咱们就详细介绍 JVM 的类加载机制,同时介绍三个实际的应用场景。java
现实中并非说,我把一个文件修改为 .class 后缀,就可以被 JVM 识别。类的加载过程很是复杂,主要有这几个过程:加载、验证、准备、解析、初始化。
如图所示。大多数状况下,类会按照图中给出的顺序进行加载。下面咱们就来分别介绍下这个过程。mysql
加载的主要做用是将外部的 .class 文件,加载到 Java 的方法区内。加载阶段主要是找到并加载类的二进制数据,好比从 jar 包里或者 war 包里找到它们。程序员
确定不能任何 .class 文件都能加载,那样太不安全了,容易受到恶意代码的攻击。验证阶段在虚拟机整个类加载过程当中占了很大一部分,不符合规范的将抛出 java.lang.VerifyError 错误。像一些低版本的 JVM,是没法加载一些高版本的类库的,就是在这个阶段完成的。sql
从这部分开始,将为一些类变量分配内存,并将其初始化为默认值。此时,实例对象尚未分配内存,因此这些动做是在方法区上进行的。数据库
下面两段代码,code-snippet 1 将会输出 0,而 code-snippet 2 将没法经过编译。
code-snippet 1:
public class A {
static int a ;
public static void main(String[] args) {
System.out.println(a);
}
}
code-snippet 2:
public class A {
public static void main(String[] args) {
int a ;
System.out.println(a);
}
}
为何会有这种区别呢?编程
这是由于局部变量不像类变量那样存在准备阶段。类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值(也能够是指定值);另一次在初始化阶段,赋予程序员定义的值。
所以,即便程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不同了,若是没有给它赋初始值,是不能使用的。tomcat
解析在类加载中是很是很是重要的一环,是将符号引用替换为直接引用的过程。这句话很是的拗口,其实理解起来也很是的简单。安全
符号引用是一种定义,能够是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。架构
直接引用的对象都存在于内存中,你能够把通信录里的女朋友手机号码,类比为符号引用,把面对面和你吃饭的人,类比为直接引用。app
解析阶段负责把整个类激活,串成一个能够找到彼此的网,过程不可谓不重要。那这个阶段都作了哪些工做呢?大致能够分为:
咱们来看几个常常发生的异常,就与这个阶段有关。
解析过程保证了相互引用的完整性,把继承与组合推动到运行时。
若是前面的流程一切顺利的话,接下来该初始化成员变量了,到了这一步,才真正开始执行一些字节码。
接下来是一道试题,你能够猜测一下,下面的代码,会输出什么?
public class A {
static int a = 0 ;
static {
a = 1;
b = 1;
}
static int b = 0;
public static void main(String[] args) {
System.out.println(a);
System.out.println(b);
}
}
结果是 1 0。a 和 b 惟一的区别就是它们的 static 代码块的位置。
这就引出一个规则:static 语句块,只能访问到定义在 static 语句块以前的变量。因此下面的代码是没法经过编译的。
static {
b = b + 1;
}
static int b = 0;
咱们再来看第二个规则:JVM 会保证在子类的初始化方法执行以前,父类的初始化方法已经执行完毕。
因此,JVM 第一个被执行的类初始化方法必定是 java.lang.Object。另外,也意味着父类中定义的 static 语句块要优先于子类的。
说到这里,不得再也不说一个试题:<cinit> 方法和 <init> 方法有什么区别?
主要是为了让你弄明白类的初始化和对象的初始化之间的差异。
public class A {
static {
System.out.println("1");
}
public A(){
System.out.println("2");
}
}
public class B extends A {
static{
System.out.println("a");
}
public B(){
System.out.println("b");
}
public static void main(String[] args){
A ab = new B();
ab = new B();
}
}
先公布下答案:
1
a
2
b
2
b
你能够看下这张图。其中 static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,因此上面的 static 代码块只会执行一次,它对应的是 <cinit> 方法。
而对象初始化就不同了。一般,咱们在 new 一个新对象的时候,都会调用它的构造方法,就是 <init>,用来初始化对象的属性。每次新建对象的时候,都会执行。
因此,上面代码的 static 代码块只会执行一次,对象的构造方法执行两次。再加上继承关系的前后原则,不难分析出正确结果。
整个类加载过程任务很是繁重,虽然这活儿很累,但总得有人干。类加载器作的就是上面 5 个步骤的事。
若是你在项目代码里,写一个 java.lang 的包,而后改写 String 类的一些行为,编译后,发现并不能生效。JRE 的类固然不能轻易被覆盖,不然会被别有用心的人利用,这就太危险了。
那类加载器是如何保证这个过程的安全性呢?其实,它是有着严格的等级制度的。
首先,咱们介绍几个不一样等级的类加载器。
这是加载器中的大 Boss,任何类的加载行为,都要经它过问。它的做用是加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。固然这些 jar 包的路径是能够指定的,-Xbootclasspath 参数能够完成指定操做。
这个加载器是 C++ 编写的,随着 JVM 启动。
扩展类加载器,主要用于加载 lib/ext 目录下的 jar 包和 .class 文件。一样的,经过系统变量 java.ext.dirs 能够指定这个目录。
这个加载器是个 Java 类,继承自 URLClassLoader。
这是咱们写的 Java 类的默认加载器,有时候也叫做 System ClassLoader。通常用来加载 classpath 下的其余全部 jar 包和 .class 文件,咱们写的代码,会首先尝试使用这个类加载器进行加载。
自定义加载器,支持一些个性化的扩展功能。
双亲委派机制的意思是除了顶层的启动类加载器之外,其他的类加载器,在加载以前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都没法胜任,它才会真正的加载。
打个比方。有一个家族,都是一些听话的孩子。孙子想要买一块棒棒糖,最终都要通过爷爷过问,若是力所能及,爷爷就直接帮孙子买了。
但你有没有想过,“类加载的双亲委派机制,双亲在哪里?明明都是单亲?”
咱们仍是用一张图来说解。能够看到,除了启动类加载器,每个加载器都有一个parent,并无所谓的双亲。可是因为翻译的问题,这个叫法已经很是广泛了,必定要注意背后的差异。
咱们能够翻阅 JDK 代码的 ClassLoader#loadClass 方法,来看一下具体的加载过程。和咱们描述的同样,它首先使用 parent 尝试进行类加载,parent 失败后才轮到本身。同时,咱们也注意到,这个方法是能够被覆盖的,也就是双亲委派机制并不必定生效。
这个模型的好处在于 Java 类有了一种优先级的层次划分关系。好比 Object 类,这个毫无疑问应该交给最上层的加载器进行加载,即便是你覆盖了它,最终也是由系统默认的加载器进行加载的。
若是没有双亲委派模型,就会出现不少个不一样的 Object 类,应用程序会一片混乱。
下面咱们就来聊一聊能够打破双亲委派机制的一些案例。为了支持一些自定义加载类多功能的需求,Java 设计者其实已经做出了一些妥协。
tomcat 经过 war 包进行应用的发布,它实际上是违反了双亲委派机制原则的。简单看一下 tomcat 类加载器的层次结构。
对于一些须要加载的非基础类,会由一个叫做 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。这个加载器用来隔毫不同应用的 .class 文件,好比你的两个应用,可能会依赖同一个第三方的不一样版本,它们是相互没有影响的。
如何在同一个 JVM 里,运行着不兼容的两个版本,固然是须要自定义加载器才能完成的事。
那么 tomcat 是怎么打破双亲委派机制的呢?能够看图中的 WebAppClassLoader,它加载本身目录下的 .class 文件,并不会传递给父类的加载器。可是,它却可使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。
可是你本身写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不一样,但对于顶层来讲,仍是同样的。
Java 中有一个 SPI 机制,全称是 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它能够用来启用框架扩展和替换组件。
这个说法可能比较晦涩,可是拿咱们经常使用的数据库驱动加载来讲,就比较好理解了。在使用 JDBC 写程序以前,一般会调用下面这行代码,用于加载所须要的驱动类。
Class.forName("com.mysql.jdbc.Driver")
这只是一种初始化模式,经过 static 代码块显式地声明了驱动对象,而后把这些信息,保存到底层的一个 List 中。
可是你会发现,即便删除了 Class.forName 这一行代码,也能加载到正确的驱动类,什么都不须要作,很是的神奇,它是怎么作到的呢?
咱们翻开 MySQL 的驱动代码,发现了一个奇怪的文件。之因此可以发生这样神奇的事情,就是在这里实现的。
路径:
mysql-connector-java-8.0.15.jar!/META-INF/services/java.sql.Driver
里面的内容是:
com.mysql.cj.jdbc.Driver
经过在 META-INF/services 目录下,建立一个以接口全限定名为命名的文件(内容为实现类的全限定名),便可自动加载这一种实现,这就是 SPI。
SPI 其实是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载。
这种方式,一样打破了双亲委派的机制。
DriverManager 类和 ServiceLoader 类都是属于 rt.jar 的。它们的类加载器是 Bootstrap ClassLoader,也就是最上层的那个。而具体的数据库驱动,却属于业务代码,这个启动类加载器是没法加载的。这就比较尴尬了,虽然凡事都要祖先过问,但祖先没有能力去作这件事情,怎么办?
咱们能够一步步跟踪代码,来看一下这个过程。
//part1:DriverManager::loadInitialDrivers
//jdk1.8 以后,变成了lazy的ensureDriversInitialized
...
ServiceLoader <Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
...
//part2:ServiceLoader::load
public static <T> ServiceLoader<T> load(Class<T> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
经过代码你能够发现 Java 玩了个魔术,它把当前的类加载器,设置成了线程的上下文类加载器。那么,对于一个刚刚启动的应用程序来讲,它当前的加载器是谁呢?也就是说,启动 main 方法的那个加载器,究竟是哪个?
因此咱们继续跟踪代码。找到 Launcher 类,就是 jre 中用于启动入口函数 main 的类。咱们在 Launcher 中找到如下代码。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}
到此为止,事情就比较明朗了,当前线程上下文的类加载器,是应用程序类加载器。使用它来加载第三方驱动,是没有什么问题的。
OSGi 曾经很是流行,Eclipse 就使用 OSGi 做为插件系统的基础。OSGi 是服务平台的规范,旨在用于须要长运行时间、动态更新和对运行环境破坏最小的系统。
OSGi 规范定义了不少关于包生命周期,以及基础架构和绑定包的交互方式。这些规则,经过使用特殊 Java 类加载器来强制执行,比较霸道。
好比,在通常 Java 应用程序中,classpath 中的全部类都对全部其余类可见,这是毋庸置疑的。可是,OSGi 类加载器基于 OSGi 规范和每一个绑定包的 manifest.mf 文件中指定的选项,来限制这些类的交互,这就让编程风格变得很是的怪异。但咱们不难想象,这种与直觉相违背的加载方式,确定是由专用的类加载器来实现的。
随着 jigsaw 的发展(旨在为 Java SE 平台设计、实现一个标准的模块系统),我我的认为,如今的 OSGi,意义已经不是很大了。OSGi 是一个庞大的话题,你只须要知道,有这么一个复杂的东西,实现了模块化,每一个模块能够独立安装、启动、中止、卸载,就能够了。
如何替换 JDK 中的类?好比,咱们如今就拿 HashMap为例。
当 Java 的原生 API 不能知足需求时,好比咱们要修改 HashMap 类,就必需要使用到 Java 的 endorsed 技术。咱们须要将本身的 HashMap 类,打包成一个 jar 包,而后放到 -Djava.endorsed.dirs 指定的目录中。注意类名和包名,应该和 JDK 自带的是同样的。可是,java.lang 包下面的类除外,由于这些都是特殊保护的。
由于咱们上面提到的双亲委派机制,是没法直接在应用中替换 JDK 的原生类的。可是,有时候又不得不进行一下加强、替换,好比你想要调试一段代码,或者比 Java 团队早发现了一个 Bug。因此,Java 提供了 endorsed 技术,用于替换这些类。这个目录下的 jar 包,会比 rt.jar 中的文件,优先级更高,能够被最早加载到。
一个 Java 类的加载,通过了加载、验证、准备、解析、初始化几个过程,每个过程都划清了各自负责的事情。
Java 自带的三个类加载器。main 方法的线程上下文加载器,实际上是 Application ClassLoader。
通常状况下,类加载是遵循双亲委派机制的。咱们也认识到,这个双亲,颇有问题。经过 3 个案例的学习和介绍,能够看到有不少打破这个规则的状况。类加载器经过开放的 API,让加载过程更加灵活。
不管是远程存储字节码,仍是将字节码进行加密,这都是业务需求。要作这些,咱们实现一个新的类加载器就能够了。