类装载子系统
0. 你将获得什么?
1. 什么是类装载子系统?
虚拟机的类加载器子系统负责从文件系统或者网络中加载Class文件,对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java.lang.Class,并将其放置于一块叫做方法区的内存区域。
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
2. JVM启动过程
在正式讲解类加载子系统之前我们首先讲解JVM的启动过程。
当你启动java虚拟机时,你通常会使用这样的命令:
java Main.class
windows开始运行{JRE_HOME}/bin/java.exe程序,java.exe 程序将完成以下步骤:
- 根据JVM内存配置要求,为JVM申请特定大小的内存空间;
- 创建一个引导类加载器实例BootstrapClassLoader,初步加载系统类到内存方法区区域中;
- 创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader,是AppClassLoader实例;
- 使用上述获取的ClassLoader实例加载我们定义的Main类;
- 加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法;
- 结束,java程序运行结束,JVM销毁。
2.1 STEP1.根据JVM内存配置要求,为JVM申请特定大小的内存空间
JVM内存按照功能上的划分,可以粗略地划分为方法区(Method Area) 和堆(Heap)。
2.2 STEP2.创建一个引导类加载器实例BootstrapClassLoader,初步加载系统类到内存方法区区域中;
JVM申请好内存空间后,JVM会创建一个引导类加载器(Bootstrap Classloader)实例,引导类加载器(Bootstrap Classloader)是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.Object、java.lang.String等等。
引导类加载器(Bootstrap Classloader)会读取 {JRE_HOME}/lib下的jar包和配置,然后将这些系统类加载到方法区内。(具体如何加载一个类就是本文第3节需要讲解的内容)
2.3 STEP3.创建JVM 启动器实例 Launcher,并取得类加载器AppClassLoader
上述步骤完成,JVM基本运行环境就准备就绪了。接着,要让JVM工作起来了。JVM虚拟机调用已经加载在方法区的类sun.misc.Launcher 的静态方法getLauncher(), 获取sun.misc.Launcher 实例。
sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher(); //获取Java启动器ClassLoader classLoader = launcher.getClassLoader(); //获取类加载器ClassLoader用来加载class到内存来
在Launcher的内部,其定义了两个类加载器(ClassLoader),分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)
和sun.misc.Launcher.AppClassLoader(应用类加载器)
launcher.getClassLoader()
方法将会返回 AppClassLoader 实例,AppClassLoader将ExtClassLoader作为自己的父加载器。
注意:这里的父加载器不是一种继承关系,而是我们之后要讲的双亲委派机制。
2.4 STEP4. 使用类加载器ClassLoader加载Main类
ClassLoader classloader = launcher.getClassLoader();//取得AppClassLoader类classLoader.loadClass("Main");//加载自定义类
上述定义的Main类被编译成Main class二进制文件,这个class文件中有一个叫常量池(Constant Pool)的结构体来存储该class的常亮信息。常量池中有CONSTANT_CLASS_INFO类型的常量,表示该class中声明了要用到那些类。
Main类要想正常工作,首先要能够保证这些其内部声明的类加载成功。所以AppClassLoader会再次利用双亲委派机制,加载这些内部声明的类。
2.5 STEP5. 使用Main类的main方法作为程序入口运行程序
方法的执行。
2.6 STEP6. 方法执行完毕,JVM销毁,释放内存
3. 如何加载一个类?类加载子系统工作原理
加载一个类主要分为3个步骤:加载(loading)、链接(linking)、初始化(initialization)。其中链接又分为验证、准备、解析。
3.1 加载
- 通过一个类的
全限定名
获取定义此类的二进制字节流
(以物理磁盘为例),这个二进制字节流我们在上一篇文章中已经详细讲解过。 - 多种类加载器利用双亲委派机制将这个字节流所代表的静态存储结构转化为
方法区
的运行时数据结构 - 在内存中生成一个代表这个类的java. lang.
Class
对象,作为方法区这个类的各种数据的访问入口
3.2 链接
3.2.1 验证
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求
,保证被加载类的正确性
,不会危害虚拟机自身安全。(虚拟机要求:例如字节码文件以cafebabe开头等)
- 主要包括
四种验证
,文件格式验证,元数据验证,字节码验证,符号引用验证。(验证出错会报VerifyError错误)
3.2.2 准备
为类变量分配内存并且设置该类变量的默认初始值。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。
3.2.3 解析
将常量池内
的符号引用转换为直接引用的过程。
- 符号引用就是一组符号来描述所引用的目标。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等
。对应常量池中的CONSTANT_ Class_ info、 CONSTANT_ Fieldref_ info、 CONSTANT Methodref_ info等。
3.3 初始化
初始化阶段就是执行类构造器方法()
的过程。
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句(也就是static修饰的)合并而来。Clinit构造器会把显示初始化和构造代码块初始化合并在一起构成构造器方法,如果没有类变量(静态变量)的赋值动作或者是静态代码块语句那么就不会生成这个clinit方法了.
- 构造器方法中指令按语句在源文件中出现的
顺序执行,不区分是静态字段还是静态代码块
- 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
4. 双亲委派机制
4.1 各种ClassLoader的介绍
- Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。我们已经见识到了,创建JVM的时候有一步就是创建启动类加载器,然后加载核心类库到内存中。
- ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
- AppClassLoader:主要负责加载应用程序的主函数类。
4.2 双亲委派机制流程图
这张图很好的讲解了双亲委派机制,核心是询问是否已经加载过时自底向上,如果都没有加载过则自顶向下加载。
4.3 双亲委派机制优点
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。