类的加载过程

大多数情况下,类会按照图中给出的顺序进行加载
加载
从jar包或war包找到并加载.class文件的二进制数据到Java的方法区
验证
验证在类的加载过程中占了很大一部分,主要是为了防止恶意攻击,因为并非所有的.class都能被加载,不符合规范的将会抛出java.lang.VeriFyError错误。一些低版本的 JVM,是无法加载一些高版本的类库的,就是在这个阶段完成的。
准备
为变量分配内存,并初始化为默认值。此时实例对象还没有分配到内存,所以这些动作是在方法区进行的
下面两段代码,code1 将会输出 0,而 code2 将无法通过编译。原因是code1中类变量a虽然没有显式的赋值,但是类变量有两次赋初始值的过程,一次是在准备阶段,赋予初始值(也可以是指定值),一次是在初始化阶段,赋予程序员定义的值。因此类变量不设置初始值也没关系,而局部变量没有准备阶段,不赋初始值是不能使用的
code1:
public class A {
static int a ;
public static void main(String[] args) {
System.out.println(a);
}
}
code2:
public class A {
public static void main(String[] args) {
int a ;
System.out.println(a);
}
}
解析
解析是将符号引用替换为直接引用的过程。符号是一种定义,可以是任何字面的含义,而直接引用是直接指向目标的指针,相对偏移量,直接引用的对象都存在于内存中,你可以把通讯录里的朋友手机号码,类比为符号引用,把面对面和你吃饭的人,类比为直接引用。
解析阶段负责把整个类激活,串成一个可以找到彼此的网,这个阶段主要做了以下工作:
1.类或接口的解析
2.类方法解析
3.接口方法解析
4.字段的解析
我们来看几个经常发生的异常,就与这个阶段有关。
java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。
java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。
java.lang.NoSuchMethodError 找不到相关方法时的错误。
解析过程保证了相互引用的完整性,把继承与组合推进到运行时。
初始化
初始化成员变量
接下来是另一道面试题,你可以猜想一下,下面的代码,会输出什么?
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 代码块的位置
规则1: static 语句块最先执行,并且只能访问到定义在 static 语句块之前的变量。所以下面的代码是无法通过编译的。
static {
b = b + 1;
}
static int b = 0;
规则2: JVM会保证,JVM 会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕。
所以,JVM 第一个被执行的类初始化方法一定是 java.lang.Object。另外,也意味着父类中定义的 static 语句块要优先于子类的。
<cinit>与<init>
面试题:<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>,用来初始化对象的属性。每次新建对象的时候,都会执行。

类加载器
Bootstrap ClassLoader
这是加载器中的大 Boss,任何类的加载行为,都要经它过问。它的作用是加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。当然这些 jar 包的路径是可以指定的,-Xbootclasspath 参数可以完成指定操作。
这个加载器是 C++ 编写的,随着 JVM 启动。
Extention ClassLoader
扩展类加载器,主要用于加载 lib/ext 目录下的 jar 包和 .class 文件。同样的,通过系统变量 java.ext.dirs 可以指定这个目录。
这个加载器是个 Java 类,继承自 URLClassLoader。
App ClassLoader
这是我们写的 Java 类的默认加载器,有时候也叫作 System ClassLoader。一般用来加载 classpath 下的其他所有 jar 包和 .class 文件,我们写的代码,会首先尝试使用这个类加载器进行加载。
Custom ClassLoader
自定义加载器,支持一些个性化的扩展功能。
双亲委托机制
除了顶层的启动类加载器以外,其余的类加载器,在加载一个类之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到父加载器们都无法胜任,它才会真正的加载。
为什么叫“双亲”?翻译的问题,不用太在意。可以看到,除了启动类加载器,每一个加载器都有一个parent,并没有所谓的双亲

翻阅 JDK 代码的 ClassLoader#loadClass 方法,来看一下具体的加载过程。和我们描述的一样,它首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。同时,我们也注意到,这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效。

这个模型的好处在于 Java 类有了一种优先级的层次划分关系。比如 Object 类,这个毫无疑问应该交给最上层的加载器进行加载,即使是你覆盖了它,最终也是由系统默认的加载器进行加载的。
如果没有双亲委派模型,就会出现很多个不同的 Object 类,应用程序会一片混乱。
一些自定义加载器
下面我们就来聊一聊可以打破双亲委派机制的一些案例。为了支持一些自定义加载类多功能的需求,Java 设计者其实已经作出了一些妥协。
案例一:tomcat
tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。简单看一下 tomcat 类加载器的层次结构。

对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。这个加载器用来隔绝不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。
如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。
那么 tomcat 是怎么打破双亲委派机制的呢?可以看图中的 WebAppClassLoader,它加载自己目录下的 .class 文件,并不会传递给父类的加载器。但是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。
但是你自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。
案例二:SPI
Java 中有一个 SPI 机制,全称是 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩
展的 API,它可以用来启用框架扩展和替换组件。
案例三:OSGi
OSGi 曾经非常流行,Eclipse 就使用 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 中的文件,优先级更高,可以被最先加载到。
endorsed技术
在JAVA运行环境中有一个叫endorsed的目录,它充许你将一些特殊的类库放到其中以供项目使用。
官方说明:
Specifying the -Djava.endorsed.dirs=lib/endorsed system property on the [Java](http://www.2cto.com
/kf/ware/Java/) command line will force the JVM to prefer any library it finds in the endorsed directory over
its own system libraries. Copying the jars into $JAVA_HOME/jre/lib/endorsed will do the same thing.
其大意是:如果你在运行程序的时候指定了-D java.endorsed.dirs这个参数所指向的包含特别的jar包的目录,或者把那些jar复制到缺省的$JAVA_HOME/jre/lib/endorsed目录下。那么在项目运行时虚拟机会优先使用这些jar包,优先级比JDK自带的系统类库还要高,但是java.lang这个语言包下的类除处。
小节
一个 Java 类的加载,经过了加载、验证、准备、解析、初始化几个过程,每一个过程都划清了各自负责的事情。
接下来,我们了解到 Java 自带的三个类加载器。同时了解到,main 方法的线程上下文加载器,其实是 Application ClassLoader。
一般情况下,类加载是遵循双亲委派机制的。我们也认识到,这个双亲,很有问题。通过 3 个案例的学习和介绍,可以看到有很多打破这个规则的情况。类加载器通过开放的 API,让加载过程更加灵活。
Java 的类加载器是非常重要的知识点,也是面试常考的知识点,本课时提供了多个面试题,你可以实际操作体验一下。
所以我们在课时开始时的第三个问题就很简单了,无论是远程存储字节码,还是将字节码进行加密,这都是业务需求。要做这些,我们实现一个新的类加载器就可以了。