JVM系列-类加载器机制
内容整理自:
一、简述
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制
二、类的加载过程以及生命周期
类加载的过程分为三个步骤(五个阶段) :加载
-> 连接(验证、准备、解析)
-> 初始化
加载
加载的过程描述:
- 通过类的全限定名定位.class文件,并获取其二进制字节流。
- 把字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在Java堆中生成一个此类的java.lang.Class对象,作为方法区中这些数据的访问入口
连接
连接:包括验证、准备、解析三步
验证
验证是连接阶段的第一步,用于确保Class字节流中的信息是否符合虚拟机的要求
具体验证形式
文件格式验证
:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。元数据验证
:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。字节码验证
:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。符号引用验证
:确保解析动作能正确执行
准备
为类的静态变量分配内存
,并将其初始化为默认值。准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口信息等
具体行为:
- 这时候进行内存分配的
仅包括类变量(static)
,而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。 - 这里所设置的初始值通常情况下
是数据类型默认的零值
(如0、0L、null、false等),而不是在Java代码中被显式赋值的(被显式赋值的常量
例外)
解析
解析:把类中对常量池内的符号引用
转换为直接引用
符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是接受的符号引用必须是一致的,因为符号引用的字面量形式已经明确定义在Class文件格式中
直接引用(Direct References): 直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么它一定已经存在于内存中了
常量池中常量类型:
- 常量池中常量数量不固定,因此常量池开头放置一个 u2 类型的无符号数,用来存储当前常量池的容量。
- 常量池的每一项常量都是一个表,表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量属于哪种常量类型
类型 | tag | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
解析动作主要针对类或接口
、字段
、类方法
、接口方法
、方法类型
、方法句柄
和调用点限定符
等7类符号引用进行
初始化
初始化:对类静态变量赋予正确的初始值
初始化的目标
- 实现对声明类静态变量时指定的初始值的初始化;
- 实现对使用静态代码块设置的初始值的初始化。
初始化的步骤
- 如果此类没被加载、连接,则先加载、连接此类;
- 如果此类的直接父类还未被初始化,则先初始化其直接父类;
- 如果类中有初始化语句,则按照顺序依次执行初始化语句。
初始化的时机
其中情况1中的4条字节码指令在Java里最常见的场景是:
- new一个对象时
- set或者get一个类的静态字段(除去那种被final修饰放入常量池的静态字段)
- 调用一个类的静态方法
Java中父类和子类初始化顺序
- 父类中静态成员变量和静态代码块
- 子类中静态成员变量和静态代码块
- 父类中普通成员变量和代码块,父类的构造函数
- 子类中普通成员变量和代码块,子类的构造函数
类的主动引用与被动引用
在java虚拟机规范中,严格规定了,只有对类进行主动引用,才会触发其初始化方法。而除此之外的引用方式称之为被动引用,不会触发类的初始化方法
主动引用
主动引用:在类加载阶段,只执行加载、连接操作,不执行初始化操作
被动引用
主动引用之外的引用情况都称之为被动引用,这些引用不会进行初始化
被动引用的几种形式:
- 通过子类引用父类的的静态字段,不会导致子类初始化;
- 定义类的数组引用而不赋值,不会触发此类的初始化;
- 访问类定义的常量,不会触发此类的初始化
三、三种类加载器
- Bootstrap Classloader 是在Java虚拟机启动后初始化的。
- Bootstrap Classloader 负责加载 ExtClassLoader,并且将 ExtClassLoader的父加载器设置为 Bootstrap Classloader
- Bootstrap Classloader 加载完 ExtClassLoader 后,就会加载 AppClassLoader,并且将 AppClassLoader 的父加载器指定为 ExtClassLoader
Bootstrap ClassLoader
启动类加载器
:负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
Extension ClassLoader
扩展类加载器
:该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
Application ClassLoader
应用程序类加载器
:该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(程序自己classpath下的类)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
类加载器的隔离问题
每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name) 进行搜索来检测这个类是否已经被加载了。
JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName
,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个类不是由一个 ClassLoader 加载,是无法将一个类的实例强转为另外一个类的,这就是 ClassLoader 隔离性。
为了解决类加载器的隔离问题,JVM引入了双亲委托机制
四、双亲委托模型
核心思想:其一,自底向上检查类是否已加载
;其二,自顶向下尝试加载类
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类
具体加载过程
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在%JAVA_HOME%/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 如果ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
双亲委派模型意义
- 系统类防止内存中出现多份同样的字节码,使得类有了层次的划分
- 保证Java程序安全稳定运行
就拿java.lang.Object
来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找rt.jar
里面的java.lang.Object加载到JVM中, 这样如果有不法分子自己造了个java.lang.Object,里面嵌了不好的代码,但是如果按照双亲委派模型来实现类加载的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护
五、类的加载方式
- 命令行启动应用时候由JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
Class.forName()和ClassLoader.loadClass()
- Class.forName():把类的.class文件加载到JVM中,对类进行解释的同时执行类中的static静态代码块;
- ClassLoader.loadClass():只是把.class文件加载到JVM中,不会执行static代码块中的内容,只有在newInstance才会去执行
六、自定义加载器
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
- 在执行非置信代码之前,自动验证数字签名。
- 动态地创建符合用户特定需要的定制化构建类。
- 从特定的场所取得java class,例如数据库中和网络中