实例说明类加载过程
本篇为学习JAVA虚拟机的第十三篇文章,本文从一个简单程序入手,将前面所学串联起来,详细来看看类加载的过程到底是什么样子的。
零、Java虚拟机启动、加载类过程分析
下面我将定义一个非常简单的java程序并运行它,来逐步分析java虚拟机启动的过程。
1 | package org.luanlouis.jvm.load; |
在windows
命令行下输入: java org.luanlouis.jvm.load.Main
当输入上述的命令时: windows
开始运行{JRE_HOME}/bin/java.exe
程序,java.exe
程序将完成以下步骤:
1.根据JVM内存配置要求,为JVM申请特定大小的内存空间;
2.创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;
3.创建JVM 启动器实例
Launcher
,并取得类加载器ClassLoader
;4.使用上述获取的
ClassLoader
实例加载我们定义的org.luanlouis.jvm.load.Main
类;5.加载完成时候JVM会执行Main类的
main
方法入口,执行Main类的main
方法;6.结束,java程序运行结束,JVM销毁。
下面逐一分析一下这几个步骤。
一、根据JVM内存配置要求,为JVM申请特定大小的内存空间
JVM内存按照功能上的划分,可以粗略地划分为方法区(Method Area
) 和堆(Heap
),而所有的类的定义信息都会被加载到方法区中。
二、创建一个引导类加载器实例,初步加载系统类到内存方法区区域中
JVM申请好内存空间后,JVM会创建一个引导类加载器(Bootstrap Classloader
)实例,引导类加载器是使用C++
语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String
, java.lang.Object
等等。
引导类加载器(Bootstrap Classloader
)会读取 {JRE_HOME}/lib
下的jar包和配置,然后将这些系统类加载到方法区内。
本例中,引导类加载器是用 {JRE_HOME}/lib
加载类的,不过,你也可以使用参数 -Xbootclasspath
或 系统变量sun.boot.class.path
来指定的目录来加载类。
一般而言,{JRE_HOME}/lib
下存放着JVM正常工作所需要的系统类,如下表所示:
文件名 | 描述 |
---|---|
rt.jar | 运行环境包,rt即runtime,J2SE 的类定义都在这个包内 |
charsets.jar | 字符集支持包 |
jce.jar | 是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC) |
jsse.jar | 安全套接字拓展包Java™ Secure Socket Extension |
classlist | 该文件内表示是引导类加载器应该加载的类的清单 |
net.properties | JVM 网络配置信息 |
引导类加载器(Bootstrap ClassLoader
) 加载系统类后,JVM内存会呈现如下格局:
- 引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。
- 类加载器的引用,由于这些类是由引导类加载器(
Bootstrap Classloader
)进行加载的,而 引导类加载器是由C++语言实现的,所以是无法访问的,故而该引用为NULL - 对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
三、创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader
上述步骤完成,JVM基本运行环境就准备就绪了。接着,我们要让JVM工作起来了:运行我们定义的程序 org.luanlouis,jvm.load.Main
。
此时,JVM虚拟机调用已经加载在方法区的类sun.misc.Launcher
的静态方法getLauncher()
, 获取sun.misc.Launcher
实例:
1 | //获取Java启动器 |
sun.misc.Launcher
使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher
实例。
在Launcher
的内部,其定义了两个类加载器(ClassLoader
),分别是sun.misc.Launcher.ExtClassLoader
和sun.misc.Launcher.AppClassLoader
,这两个类加载器分别被称为拓展类加载器(Extension ClassLoader
) 和 应用类加载器(Application ClassLoader
).如下图所示:
四、使用类加载器ClassLoader加载Main类
通过 launcher.getClassLoader()
方法返回AppClassLoader
实例,接着就是AppClassLoader
加载 org.luanlouis.jvm.load.Main
类的时候了。
1 | lassLoader classloader = launcher.getClassLoader();//取得AppClassLoader类 |
上述定义的org.luanlouis.jvm.load.Main
类被编译成org.luanlouis.jvm.load.Main class
二进制文件,这个class文件中有一个叫常量池(Constant Pool
)的结构体来存储该class的常量信息。常量池中有CONSTANT_CLASS_INFO
类型的常量,表示该class中声明了要用到那些类:
当AppClassLoader
要加载 org.luanlouis.jvm.load.Main
类时,会去查看该类的定义,发现它内部声明使用了其它的类: sun.security.pkcs11.P11Util
、java.lang.Object
、java.lang.System
、java.io.PrintStream
、java.lang.Class
;org.luanlouis.jvm.load.Main
类要想正常工作,首先要能够保证这些其内部声明的类加载成功。所以AppClassLoader
要先将这些类加载到内存中。(注:为了理解方便,这里没有考虑懒加载的情况,事实上的JVM加载类过程比这复杂的多)
加载顺序:
- 加载
java.lang.Object
、java.lang.System
、java.io.PrintStream
、java,lang.Class
AppClassLoader
尝试加载这些类的时候,会先委托ExtClassLoader
进行加载;而
ExtClassLoader
发现不是其加载范围,其返回null;
AppClassLoader
发现父类加载器ExtClassLoader
无法加载,
则会查询这些类是否已经被BootstrapClassLoader
加载过,
结果表明这些类已经被BootstrapClassLoader
加载过,
则无需重复加载,直接返回对应的Class<T>
实例;
- 加载
sun.security.pkcs11.P11Util
此在
{JRE_HOME}/lib/ext/sunpkcs11.jar
包内,属于ExtClassLoader
负责加载的范畴。
AppClassLoader
尝试加载这些类的时候,会先委托ExtClassLoader
进行加载;而
ExtClassLoader
发现其正好属于加载范围,故ExtClassLoader
负责将其加载到内存中。
ExtClassLoader
在加载sun.security.pkcs11.P11Util
时也分析这个类内都使用了哪些类,
并将这些类先加载内存后,才开始加载sun.security.pkcs11.P11Util
,
加载成功后直接返回对应的Class<sun.security.pkcs11.P11Util>
实例;
- 加载
org.luanlouis.jvm.load.Main
AppClassLoader
尝试加载这些类的时候,会先委托ExtClassLoader
进行加载;
而ExtClassLoader
发现不是其加载范围,其返回null;
AppClassLoader
发现父类加载器ExtClassLoader
无法加载,
则会查询这些类是否已经被BootstrapClassLoader
加载过。
而结果表明BootstrapClassLoader
没有加载过它,
这时候AppClassLoader
只能自己动手负责将其加载到内存中,
然后返回对应的Class<org.luanlouis.jvm.load.Main>
实例引用;
以上三步骤都成功,才表示classLoader.loadClass("org.luanlouis.jvm.load.Main")
完成,上述操作完成后,JVM内存方法区的格局会如下所示:
如上图所示:
- JVM方法区的类信息区是按照类加载器进行划分的,每个类加载器会维护自己加载类信息;
- 某个类加载器在加载相应的类时,会相应地在JVM内存堆(
Heap
)中创建一个对应的Class<T>
,用来表示访问该类信息的入口
五、使用Main类的main方法作为程序入口运行程序
就是去执行指令,过程与Java如何执行一个最简单的程序类似。
六、方法执行完毕,JVM销毁,释放内存
对于本程序,主程序执行完毕,释放主函数所在的栈帧,释放堆中的内存。
七、再来回顾回顾java类加载器相关的概念吧
本处的内容为再次简单说明,具体见双亲委派模型。类加载器(Class Loader
):顾名思义,指的是可以加载类的工具。JVM自身定义了三个类加载器:引导类加载器(Bootstrap Class
Loader)、拓展类加载器(Extension Class Loader
)、应用加载器(Application Class Loader
)。当然,我们有时候也会自己定义一些类加载器来满足自身的需要。
引导类加载器(Bootstrap Class Loader
): 该类加载器使JVM使用C++/C底层代码实现的加载器,用以加载JVM运行时所需要的系统类,这些系统类在{JRE_HOME}/lib
目录下。由于类加载器是使用平台相关的底层C++/C
语言实现的, 所以该加载器不能被Java代码访问到。但是,我们可以查询某个类是否被引导类加载器加载过。我们经常使用的系统类如:java.lang.String
,java.lang.Object
,java.lang*
… 这些都被放在 {JRE_HOME}/lib/rt.jar
包内, 当JVM系统启动的时候,引导类加载器会将其加载到 JVM内存的方法区中。
拓展类加载器(Extension Class Loader
): 该加载器是用于加载 java 的拓展类 ,拓展类一般会放在{JRE_HOME}/lib/ext/
目录下,用来提供除了系统类之外的额外功能。拓展类加载器是是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的。(注意,其实引导类加载器不能算是扩展类加载器的父类,我们从源码中可以看出来的)
应用类加载器(Applocatoin Class Loader
): 该类加载器是用于加载用户代码,是用户代码的入口。我经常执行指令 java xxx.x.xxx.x.x.XClass
, 实际上,JVM就是使用的AppClassLoader
加载 xxx.x.xxx.x.x.XClass
类的。
用户自定义类加载器(Customized Class Loader
):用户可以自己定义类加载器来加载类。所有的类加载器都要继承java.lang.ClassLoader
类。
关于双亲委派模型,就不再赘述了。
八、线程上下文加载器
Java 任何一段代码的执行,都有对应的线程上下文。如果我们在代码中,想看当前是哪一个线程在执行当前代码,我们经常是使用如下方法:
1 | Thread thread = Thread.currentThread();//返回对当当前运行线程的引用 |
相应地,我们可以为当前的线程指定类加载器。在上述的例子中, 当执行 java org.luanlouis.jvm.load.Main
的时候,JVM会创建一个Main
线程,而创建应用类加载器AppClassLoader
的时候,会将AppClassLoader
设置成Main线程的上下文类加载器:
1 | public Launcher() { |
线程上下文类加载器是从线程的角度来看待类的加载,为每一个线程绑定一个类加载器,可以将类的加载从单纯的 双亲加载模型解放出来,进而实现特定的加载需求。