类加载器子系统
类加载器子系统作用
Class
文件并非特指具体磁盘中的一个文件,而应该是一串二进制字节流。由于JVM
跨语言平台的特性(Class
文件不一定来源于Java
,还可以来源于Kolin
、Groovy
、Scala
等。JVM
只关心字节码,不关心语言),只要符合JVM规范的二进制字节流都可以被识别,包括但不限于磁盘文件、网络、数据库、内存或者动态产生等。
而类加载器子系统负责将字节码文件从以上的来源中加载Class
文件。其中Class Loader
只负责Class
文件的加载,而这个程序是否可以执行,由Execution Engine
决定。
非正文(琐碎的笔记):
- 加载的类信息存放于一块称之为方法区的内存空间
- 除了类的信息外,方法区还会存放运行时常量池信息,可能包括字符串字面量和数字常量(这部分常量信息是Class文件中常量部分的内存映射)
- 常量池在运行的时候加载到内存中,就是运行时常量池
- 比如有个类为Car.java,编译完之后成为Car.class,类的加载器将这个Car.class加载到方法区中(成为DNS元数据模板)
- 通过getClassLoader()方法可以得到类的加载器,然后在内存中调用Car.class中的构造器,创建Car的对象。
- 通过实例car.getClass()也可以找到这个类本身
- class对象(对象数据)在堆,class元数据模板(结构数据)在方法区,前者根据后者创建
- 如果调用一个方法,首先要先用ClassLoader()去加载这个方法所在的类。
类加载机制
JVM
将Class
文件加载到内存中,然后经过验证、准备、解析和初始化过程,得到JVM
可以直接使用的Java
类型,这个过程被成为JVM的类加载机制。由于这个过程发生在程序运行期间,所以也避免不了性能上的开销,但是也为Java
极高的扩展性和灵活性奠定了基础。
整个过程可以分为:加载(Loading
) --> 链接(Linking
)(验证、准备、解析)–> 初始化(Initialization
)
加载
“加载”(Loading
)阶段是整个“类加载”(Class Loading
)过程中的一个阶段,JVM
在此阶段主要完成以下三件事情:
- 通过一个类的全限定名获取定义此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
方法区:jdk7以前叫做永久代,之后叫做元空间
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。这个大Class
类所对应的一个实例,就代表了这个二进制字节流对应的类本身。
对于第一点,“通过一个类的全限定名获取此类的二进制字节流”,其中获取的源头、获取的方式有很多:
- 从ZIP压缩包中读取,之后可以为从
JAR
、EAR
、WAR
中读取; - 网络中获取,典型应用就是
Web Applet
; - 运行时计算生成。在
java.lang.reflect.Proxy
中利用ProxyGenerator.generateProxyClass()
为特定的接口生成形式为"*$Proxy"
的代理类的二进制字节流;代理模式可以参考之前的博客记录:设计模式-代理模式; - 从其他文件中生成,例如
JSP
应用中,由JSP
生成对应的Class
文件; - 从数据库中获取;
- 从加密文件中获取。防止
Class
被反编译的保护措施,通过加载时解密Class
文件来确保安全; - …
链接
验证
确保Class
文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。(因为Class
文件不一定要由Java
源码编译而来,所以完全可以人为在二进制编辑器用0
和1
敲出Class
文件,存在风险)
主要包括四种验证方式:
-
文件格式验证
-
元数据验证
-
字节码验证
-
符号引用验证
准备
准备阶段是正式为类中定义的变量(静态变量,被static
修饰的变量)分配内存并设置变量初始值(零值)的阶段。不包含用final
修饰的static
,因为final
在编译的时候就会分配了,准备阶段会显示初始化。这个阶段不会为实例变量分配初始化(因为此时还未创建对象),类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。
public static final int a = 1;
public static int b = 2;
对于两个变量a和b。其中a在准备阶段的时候,初始值已经是1,而不是零值0,因为此时a被static final修饰,在编译成Class文件的时候,就已经对变量a进行了显示初始化。但是对于变量b,在准备阶段的时候,初始值为0,而不是2。2的赋值操作会在初始化阶段执行<clinit>()
方法。基本数据类型的零值如下:
类型 | 默认初始值 |
---|---|
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
char | \u0000 |
boolean | false |
reference | null |
解析
-
解析阶段是将常量池中的符号引用替换为直接引用的过程。
-
符号引用就是一组符号来描述引用的目标,符号可以是任何形式的字面量,只要使用无歧义的定位到目标即可。
-
直接引用是可以指向目标的指针、相对偏移量或者是一个可以间接定位到目标的句柄。
-
-
解析动作通常伴随着
JVM
在执行完后再执行。
相关解释:
符号引用:编译的时候(也就是生成Class文件的时候,此时是和JVM布局没有关系的!),这个时候JVM并不能确定引用类的具体地址,所以说大多都是以符号进行标记,而解析阶段就是真正将符号引用转换成直接引用的过程。
直接引用:该引用是和虚拟机的布局密切相关的,不同的虚拟机对于不同的符号引用翻译出来的直接引用一般是不同的。如果有了直接引用,则该变量一定是在内存中的!
或者也可以参考:符号引用、直接引用
初始化
初始化阶段,就是执行类构造器<clinit>()
方法的过程:
<clinit>()
是由编辑器自动收集类中的所有类变量的赋值动作和静态代码块(static{}
块)中合并产生的。对于static final修饰的变量,因为它是无法在次赋值的,所致该变量是不会经过<clinit>()
方法的;<clinit>()
方法和类的构造函数(虚拟机视角中的实例构造器<init>()
方法)不同,它不需要显式的调用父类构造器。由于父类的加载要早于子类的加载,所以说JVM
第一个被执行<clinit>()
方法的类一定是java.lang.Object
.
public class ClinitTest1 {
static class Father{
// 准备阶段:0 --> 解析阶段:先是赋值操作:A=1,然后是静态代码块:A=2
public static int A = 1;
static {
A = 2;
}
}
static class Son extends Father{
public static int B = A;
}
public static void main(String[] args) {
// 加载Father类,其次加载Son类
System.out.println(Son.B); // 2
}
}
如果父类代码修改为:
static class Father{
static {
A = 2;
}
public static int A = 1;
}
此时 System.out.println(Son.B);
的输出结果为1
,静态代码块和显式赋值的操作顺序按照语句出现的前后执行。
其中要注意避免非法前向引用的发生:
public class ClassInitTest {
static {
number = 20;
System.out.println(number); // 此时不能够访问number变量,因为这个时候还没有声明变量
// 报错:
// 非法的前向引用:因为我们声明的是在后面,我们无法进行调用,可以赋值
}
private static int number = 10;
}
<clinit>()
并不是必须执行的,如果一个类中没有赋值操作或者静态代码块,则编译器不会为这个类生成<clinit>()
方法。<clinit>()
方法是天然的线程安全的。在多线程环境中,只有一个线程可以去执行这个类的<clinit>()
方法,其他线程都出于阻塞状态,直到这个活动线程完成该方法的操作。
类的加载器
JVM
支持两种类加载器:
- 引导类加载器(
BootStrap Class Loader
):由C/C++
编写,Java
环境中显示为null
值,无访问权限。 - 用户自定义类加载器(
User-Defined Class Loader
):继承于ClassLoader
抽象类,由Java
编写,可以访问。
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取了Launcher的一个内部类
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// 获取其上层:sun.misc.Launcher$ExtClassLoader@1b6d3586
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);
// sun.misc.Launcher$AppClassLoader@18b4aac2
// sun.misc.Launcher$ExtClassLoader@1b6d3586
// 两者是一个包含的关系
// 获取其上层:null(引导类加载器)
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(bootStrapClassLoader);
// 对应用户自定义类的加载器:默认使用系统类加载器加载用户自定义的类
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader);
// 间接说明了:String类的类加载器是BootStrap ClassLoader加载的
// 因为是null的
// java的核心类库都是Boot ..加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); // null
}
}
引导类加载器(BootStrap Class Loader
)
BootStrap Class Loader
是c/c++
编写的,嵌套在JVM
内部;- 用来加载Java的核心类库(
JAVA_HOME/jre/lib/rt.jar
、resources.jar
或sun.boot.class.path
路径下的内容),用于提供JVM
自身需要的类 - 并不继承于
java.lang.ClassLoader
,没有父类加载器 - 出于安全考虑,
BootStrap
启动类加载器只加载包名为java
,javax
,sun
等开头的类 - 加载扩展类和应用程序类加载器,并指定为它们的父类加载器
扩展类加载器(Extension ClasssLoader
)
- 以
sun.misc.Launcher$ExtClassLoader
形式实现 - 负责加载路径:
jre/lib/ext
应用程序类加载器(系统加载器,AppClassLoader
)
-
以
sun.misc.Launcher$AppClassLoader
形式实现 -
父类加载器为扩展类加载器(父类加载器不是父类!)
-
该类加载的是程序中默认的类加载器(用户自定义的类一般来说都是它加载的)
-
它负责加载环境变量
classpath
或系统属性,java.class.path
指定路径下的类库 -
通过
ClassLoader#getSystemClassLoader()
方法可以获取到该类加载
自定义类加载器
日常开发中,类的加载几乎是由上面3种类加载器相互配合执行的,在必要时,还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
- 隔离加载类(中间件拥有自己依赖的
jar
包,避免冲突) - 修改类加载的方式(
BootStrap
是必须的,其余的不必要) - 扩展加载源(加载的来源可以扩展,例如从数据库中)
- 防止源码泄露(对字节码文件的加密,采用自定义类加载器进行解密)
ClassLoader抽象类常见方法
方法名称 | 描述 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名为name 的类,返回结果为 java.lang.Class 类的实例 |
findClass(String name) | 查找名为name 的类,返回结果为 java.lang.Class 类的实例 |
findLoadedClass(String name) | 查找名为name 的已经被加载过类,返回结果为 java.lang.Class 类的实例 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例 |
resolveClass(Class<?> c) | 连接指定的一个 Java类 |
双亲委派机制
一个类加载器收到了类加载的请求,它并不会自己先去加载,而是将这个请求委托给父类加载器去执行。
如果这个父类加载器还有父类加载器,则进一步向上委托,依次委托,请求最终到达启动类加载器。
如果父类加载器可以完成类的加载操作,就成功返回。如果无法加载此类,子加载器才会自己去尝试加载,这就是双亲委派机制。
优势:
- 避免了类的重复加载(类只有一个)
- 保护程序安全,防止核心API被随意的篡改
public class QybStart {
public static void main(String[] args) {
// java.lang包访问时需要权限的
// 阻止用户在核心包下创建自定义类
// 此时报错:Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
// 出于安全的考虑
System.out.println("Hello~");
}
}
沙箱安全机制
自定义String
类,但是加载自定义的String
类会率先使用引导类加载器加载,但是引导类加载器在加载的过程中会优先加载jdk
自带的文件(rt.jar
包中的java\lang\String.class
),报错信息说没有main
方法,就是因为加载的是rt.jar
下的String
类,这样可以保证对java
核心源代码的保护,这就是沙箱安全机制。
其他
在JVM
中表示两个class
对象是否为同一个类存在的两个必要条件:
- 类的完整类名必须一致,包括包名;(全限定名一致)
- 加载这个类的
ClassLoader
(指ClassLoader
实例对象)必须相同。
对类加载器的引用
JVM
必须知道这个类是用启动类加载器加载的还是用户类加载器加载的。
引导类加载器的实例对象是null
,所以就不会记录引导类加载器。但是如果一个类是有用户类加载器加载的,那么JVM就会将这个类加载器的一个引用作为信息的一部分保存在方法区。当解析一个类型到另一个类型的引用时,JVM
需要保证这两个类型的类加载器是相同的。
类的主动引用和被动引用
Java
程序对类的使用方式分为两种
-
主动使用情况
-
创建类的实例
-
访问某个类或接口的静态变量,或者对该静态变量赋值
-
调用类的静态方法
-
反射(如:
Class.forName("cn.duniqb.Test")
) -
初始化一个类的子类
-
Java
虚拟机启动时被标明为启动类的类 -
JDK7
开始提供的动态语言支持java.lang.invoke.MethodHandle
实例的解析结果,REF_getStatic
,REF_putStatic
,REF_invokeStatic
句柄对应的类没有初始化,则初始化
-
-
除了以上7种,其他方式都被看做是被动使用,都不会导致类的初始化