JVM(二)类加载子系统
类加载子系统的作用
- 类加载子系统负责从文件或者网络中加载class文件,class文件再文件开头有特定的标识
- ClassLoader只负责class的加载,至于它是否可以运行,则由执行引擎决定
- 加载的类信息存放在叫方法区的内存空间。除了类信息外,方法区中还会存放运行时常量池,还可能包括字符串常量和数字常量。
类加载起ClassLoader角色
- class文件存在于本地硬盘上,可以理解为设计师在纸上的模板,而最终这个模板在执行的时候是要加载JVM中来,根据这个文件实例化出n个一模一样的实例
- class文件加载到内存中,被称为DNA元数据模板
- 在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
类加载过程
public class HelloLoader {
public static void main(String[] args) {
System.out.println("谢谢ClassLoader加载我....");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
这一段代码的是怎么加载的呢?
- 执行 main() 方法(静态方法)就需要先加载main方法所在类 HelloLoader
- 加载成功,则进行链接、初始化等操作。完成后调用 HelloLoader 类中的静态方法 main
- 如果加载失败则抛出异常
-
类的加载过程分为加载、验证、准备、解析、初始化这几个步骤。
第一个环节加载不是指完整的加载过程,仅仅是将class文件加载到内存中。
Loading
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种的数据的入口
加载class文件的方式:
4. 从本地系统中直接获取
5. 通过网络获取,典型场景:Web Applet
6. 从压缩包中获取
7. 运行时计算生成,如动态代理
8. 由其他文件生成,如jsp
9. 从加密文件中获取,典型的防class文件反编译的保护措施
Linking
链接分为三个阶段:验证、准备、解析
- 验证
验证时链接阶段的第一步,这一阶段的目的是为二零确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证主要包括四种验证方式:- 文件合适验证
验证字节流是否符合class文件的规范,并且能被当前版本的虚拟机处理。 - 元数据验证
对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言规范的要求。 - 字节码验证
通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;如果通过了,也不能说明就一定安全的。 - 符号引用验证
发生在虚拟机将符号引用转化为直接应用的时候,这个转化的动作在丽连接的第三阶段中发生。对类自身意外的信息进行匹配性校验。
- 文件合适验证
- 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都在方法区中进行分配。
类变量在主备阶段后的初始值是其类型默认的“零”值,而不是定义时的值。这些过程在类构造器<clinit>()
方法中。如果一个类变量时static且时fianl的,在准备阶段就会直接赋值为定义时的值。
注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
- 解析
将常量池内符号引用替换为直接引用的过程。
事实上,解析操作往往在jvm执行完初始化化操作之后才会进行。- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
反编译 class 文件后可以查看符号引用,下面带# 的就是符号引用
Initialzation
类初始化阶段时类加载过程的最后一步,这一阶段才真正开始执行类中定义的Java程序代码。
在准备阶段,变量已经赋过依次系统要求的初始值,而在初始化阶段才会根据Java代码中的定义区初始化这些值。初始化阶段时执行类构造器<clinit>()
方法的过程。
<clinit()>
方法与类的构造函数不同,它时由编译器自动收集类中所有的类变量的赋值动作和静态代码块中的语句合并产生的,收集顺序和这些语句在源文件中出现的顺序一样。所以这个方法不是必须的,如果一个类中没有静态代码块,也没有对静态代码的赋值操作,那么就不会生成这个方法。
在执行子类的<clinit>()
方法之前,虚拟机会确保其父类的该方法已经执行完毕。因此在虚拟机中第一个被初始化的类是java.lang.Oject
。
- 类初始化的时机
- 创建类的实例
- 访问某个类或者接口的静态变量
- 调用类的静态方法
- 使用反射
- 初始化一个类的子类
- java虚拟机规定为启动类的类
- JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,即不会执行初始化阶段(不会调用 clinit() 方法和 init() 方法)
类加载器的分类
-
JVM严格来讲支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
-
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
-
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示
那么怎么获取这些加载器呢?
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
- 当尝试获取引导类加载器时,会发现获取了
null
,也就是说在java程序中是拿不到引导类加载器的。但着并不代表引导类加载器不存在。 - 系统类加载器是全局唯一的,上面两次输出的应用值是相同的。
引导类加载器
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个加载器使用c/c++实现,嵌套在JVM内部
- 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
- 它并不继承java.lang.ClassLoader,没有父加载器。
- 加载扩展类加载器和应用程序类加载器。
- 启动类加载器只加载包名为java、javax、sum等的类。
扩展类加载器
扩展类加载器(Extension ClassLoader)
- 由Java语言编写,由
sum.misc.Lancher$ExtClassloader
实现 - 派生于
ClassLoader
类 - 父加载器为引导类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的的Jar文件放在该目录下,也会被加载。
系统类加载器
应用程序类加载器(AppClassLoader)
- Java语言编写
sum.misc.Lancher$AppClassloader
实现 - 派生于
ClassLoader
类 - 父加载器为扩展类加载器
- 负责加载系统变量classpath或者系统属性java.class.path指定路径下的类
- 是程序中默认的类加载器,一般来说,Java应用的类都是由系统类加载器加载。
- 通过`ClassLoader.getSystemClassLoader()``可以获取到该加载器。
用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。那为什么还需要自定义类加载器?
- 隔离加载类(比如说我假设现在Spring框架,和RocketMQ有包名路径完全一样的类,类名也一样,这个时候类就冲突了。不过一般的主流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)
- 修改类加载的方式
- 扩展加载源(还可以考虑从数据库中加载类,路由器等等不同的地方)
- 防止源码泄漏(对字节码文件进行解密,自己用的时候通过自定义类加载器来对其进行解密)
- 如何自定义类记载器?
- 过继承抽象类java.lang.ClassLoader类的方式
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
- 关于ClassLoader类
这个类是一个抽象类,除了引导类加载器,其他加载器都继承于这个类。
双亲委派模型
ava虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
双亲委派模型要求除了顶层的引导类加载器外,其余的类加载器都应当由自己的父类加载器。这里的类加载器之间的父子关系不是以继承的关系来体现,而是都使用组合关系来复用类加载器的代码。
- 如果一个类加载器加载器收到了类加载的请求,它首先不会自己加载这个类,而是把这个请求委派给它的父加载器去完成
- 每一层的加载器都是如此,最终请求将会到达启动类加载器
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
- 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常
为什么要有双亲委派机制呢?
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改,比如自定义一个java.lang.String 的类将不会被加载
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
这里静态代码块里的输出语句将不会被执行。
沙箱安全机制
自定义String类时:在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java.lang.String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。
这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
破坏双亲委派机制
双亲委派机制对于保证Java程序的稳定运行很重要,但它的实现却非常简单,实现双亲委派机制的代码都集中在ClassLoader
类的loadClass()
方法中。
- 先检查请求加载的类是否已经被加载过了,若没有加载则调用父加载器的
loadClass()
- 若父加载器为空则默认使用启动类加载器作为父加载器。
- 如果父加载器加载失败,抛出ClassNotFound异常后,在调用自己的
findclass()
方法进行加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
要破环这个机制,重写这个函数即可。