
简介
在探探字节码文件中,我们学习了class文件
作为一个二进制字节流
,是如何通过二进制的方式表达编程思想的。那么,class文件
是由编译器编译得到的,默默的放在硬盘里面,当我们需要运行程序的时候,我们必须将class文件
加载进内存中。那么,class文件
需要经过一个什么样的过程才能到内存里准备好呢?
本篇文章:我们将讨论class文件作为一个二进制字节流
文件,JVM虚拟机是如何对这个文件加载
并且准备
、执行
的。

class加载
进内存,准备
、执行
的过程中,总共分三大步
。第一步是Loading
,第二步是Linking
,第三步是Intializing
。其中Linking又分成三小步,第一小步是Verification
(验证)
,第二小步是Preparation
(准备)
,第三小步是Resolution
(解析)
。
Loading
类加载器
Loading是把一个class文件load装进内存的,在JVM中有一套自定义的类加载器的层次,jvm所有的class都是被类加载器加载到内存的,这个类加载器也叫做ClassLoader
。每个类加载器本身也是一个普通的class文件,最终由最高层次的加载器加载进JVM中。
如果,我想知道类是由哪个加载器加载进内存里的,我们只需打印一下这个加载器,如下面代码:
1 |
|
这里为什么会打印空呢?原因为:Boostrap加载类,是由C++实现的,java里没有对应实现,所以为空。
由程序可以证明
- AppClassLoader、ExtClassLoader是sun.misc.Launcher的内部类。
- AppClassLoader、ExtClassLoader都是由Boostrap加载器加载的。
- AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BoostrapClassLoader。
Launcher是ClassLoader一个包装启动类,通过这个类我们可以看到每个类加载器的范围
1 | // BoostrapClassLoader |
我们可以测试一下
1 | public class ClassLoaderT03 { |
输出为:

双亲委派
双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程。双亲委派,主要出于安全
来考虑。
加载的过程如图所示:

查找的加载器路线是:
自定义的ClassLoader—> AppClassLoader—>ExtClassLoader—> BoostrapClassLoader
重复的动作是:findInCache -> parent.loadClass -> findClass()
委派的方向刚好相反:
BoostrapClassLoader—>ExtClassLoader—>AppClassLoader—>自定义的ClassLoader
其实,这个可以看ClassLoader的源码
1 | protected Class<?> loadClass(String name, boolean resolve) |
任何一个class,都会先从自定义的classLoader开始,因为内部维护了缓存,先从缓存中查找,如果没有,就委托给父加载器,父加载器也是重复同样的动作,直到最顶层的BoostrapClassLoader
。如果父加载器缓存没有,class文件也不在自己负责的文件范围内,就重新委托给当前加载器。
1 | protected Class<?> findClass(String name) throws ClassNotFoundException { |
当前加载器会从自己负责加载的文件夹查找,如果有,就加载。如果没有,就抛出ClassNotFoundException
。
整个加载过程如图

那么,为什么类的加载机制要搞一套双亲委派模型出来呢?
我们可以假设,如果任何一个class可以通过自定义的ClassLoader记载进内存里,那么,我们就可以把java.lang.String
交给自定义的加载器加载,我们可以动手脚,通过将程序打包给客户,用户使用后可以把密码发给我自己。
如果有了双亲委派,就会先去上面查有没有加载过,上面有加载过就直接返回,不会让自定义加载器加载。
自定义加载器
通过以上分析,我们很容易的知道自定义加载器只需要
- 继承ClassLoader父类
- 重写模版方法findClass
- 调用defineClass
代码如下
1 | public class T006_MSBClassLoader extends ClassLoader { |
Linking
Verification
验证文件是否符合JVM规定。如果加载进来的文件首4个字节不是CAFE BABE
开头,在这个步骤就被拒绝掉了。
Preparation
把class文件中的静态成员变量赋默认值,不是初始值。比如static int i=8
,注意在这个步骤并不是直接把i赋成8,而是先赋值成0。
Resolution
将类、方法、属性等符号引用解析为直接引用。class文件常量池里面用到的符号引用,要给它转换为直接内存地址,直接可以访问到的内容。
Initializing
调用类初始化代码 <clinit>
,给静态成员变量赋初始值。
LazyLoading
懒加载,严格上叫做懒初始化
,也叫延迟初始化
。jvm规范没有严格定义何时加载,但虚拟机的实现是懒加载,就是我什么时候需要这个类的时候才会去加载这个类,并不是我用到jar包里的一个类,就将jar包所有的类都加载进来。
我们来看一个例子
1 | public class ClassLoadingProcedureT00 { |
该程序输出的结果为
1 | /*output 2 */ |
由该程序可得
- 引用静态变量时,会对T初始化,赋初始值。
注意:初始化指的是是否会调用类初始化代码<clinit>
,给静态成员变量赋初始值,不要和实例构造函数<init>
混淆了。
思考:如果把11行放开,会输出什么呢?如果把12行放在11行前面,又会输出什么?
初始化的五种情况
在周老师的《深入理解java虚拟机》明确指出,懒初始化
在下面五种情况下才能触发:
- new getstatic putstatic invokestatic指令,访问final变量除外。
- java.lang.reflect对类进行反射调用时。
- 初始化子类的时候,父类首先初始化。
- 虚拟机启动时,被执行的主类必须初始化。
- 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化。
思考:我们可以逐步注释以下代码,看看会输出什么?
1 | public class T008_LazyLoading { |
总结
- classloading是使用
双亲委派机制
,从上到下
再从下到上
,从上找
,向下委托
。主要出于安全
考虑。 - LazyLoading五种情况在《深入理解java虚拟机》里有详细的列出来。
- ClassLoaer的源码:
findInCache
->parent.loadClass
->findClass()
。 - 自定义加载器
- extends
ClassLoader
- overwrite
findclass()
—>defineClass(bytes[])
- extends