类加载子系统
关于本人的JVM文档大部分取自于尚硅谷的JVM的讲课中,存在我本人的理解话语,这不过是本人的学习总结,强烈推荐观看尚硅谷由宋红康老师讲解的《JVM从入门到精通》视频
在Java的虚拟机中,对于Java文件如何运行有做规范
其中Java虚拟机能够执行的字节码文件的就是靠类加载子系统完成。
类加载子系统中分有三个阶段
- 加载阶段
- 链接阶段
- 初始化阶段
需要注意:类加载子系统只负责文件的加载,至于是否能够运行,则是由Java虚拟机中另一个模块——Execution Engine决定的
加载类的信息一般会保存在被称作方法区的内存空间中,也就是说类的信息存放在方法去中。
除了存放类的信息,方法区还会存放运行时常量池信息,可能还包括字符串变量和数字常量(这部分常量信息是Class/字节码文件中常量池部分的内存映射)
字节码文件的加载大致流程
- 字节码文件存在于本地硬盘中,可以理解为设计师画在纸板上的模板,而最终这个模板要执行的时候是要加载进JVM当中,根据这个文件的模板实例化出n个一模一样的实例
- 字节码文件被加载进JVM中,被称为DNA元数据模板,放在方法区
- 在字节码文件->JVM->最终成为元数据模板,这个过程需要ClassLoader类加载器扮演快递员的身份运输
类加载子系统的大致流程则是加载阶段->验证阶段(其中又分为验证->准备->解析三个阶段)->初始化阶段
加载阶段
加载阶段的详细流程如下
- 通过一个类的全限定类名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代码这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
加载字节码文件的方式还分为:
- 从本地硬盘直接加载
- 从网络获取,比如Web Servlet
- 从zip压缩包读取,也就是日后的jar、war
- 运行时计算生成,使用最多的就是动态代理技术
- 有其它文件生成,比如JSP
- 从专有数据库中提取字节码文件,但比较少见
- 从加密文件获取,典型的防字节码文件被反编译的保护措施
链接阶段
-
验证
- 目的在于确保字节码文件的字节流中包好的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危及虚拟机自身安全
- 主要包括四种验证:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
-
准备
- 为***类变量***分配内存并且设置该类变量的默认初始值,即零值
- 这里不包含使用final修饰的static,因为final变量在编译时就会分配了,准备阶段显式初始化
- 这里不会为实例变量分配初始化,类变量会在分配方法区中,而实例变量会随着对象一起被分配到Java堆中
-
解析
- 将常量池的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完成初始化之后再执行
- 符号引用就是一组符号描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。直接饮用就是直接指向目标的指针、相对偏移量、或一个间接定位到目标的句柄
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中
CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
等
类加载器的分类
-
JVM中支持两种类型的类加载器,分别为:
- 引导类加载器(BootStrap ClassLoader)
- 自定义类加载器(User-Defined ClassLoader)
-
从概念上来讲,自定义类加载器一般指程序中由开发人员自定义的一类加载器,但是Java虚拟机规范却没有那么定义,而是***将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器***
-
但无论类加载器如何划分,在程序中常见到的类加载器只有三个
- BootStrap ClassLoader
- Extension ClassLoader
- System Class Loader
自上而下是包含关系
虚拟机自带的类加载器
启动类加载器(引导类加载器,BootStrap ClassLoader)
-
这个类加载器使用C/C++语言实现的,嵌套在JVM内部
-
他用来加载Java的核心类库,例如
- JAVA_HOME/jre/lib/rt.jar
- resource.jar
- sun.boot.class.path
以上路径下的内容,用于提供JVM自身需要的类
-
并不继承Java原生的java.lang.ClassLoader,没有父加载器
-
出于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- 由Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 实际上ExtClassLoader是个内部类,他在sun.misc.Launcher中
- 派生于ClassLoader
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar包也放在这下面,扩展类加载器也会加载这个jar
应用类加载器(Application ClassLoader)
- Java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 实际上ExtClassLoader是个内部类,他在sun.misc.Launcher中
- 派生于ClassLoader类
- 父加载器为扩展类加载器
- 他负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载器是程序中默认的类加载器,一般来说,Java应用都是通过他来完成加载
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
用户自定义类加载器
- 在日常的Java应用开发中,类的加载几乎是由上述三种类加载器相互配合执行的,在必要时,也可以自定义类加载器来定制加载方式
- 为什么要定制自定义类加载器
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 放置源码泄露
如何使用
- 开发人员可以通过继承java.lang.ClassLoader类的方式,实现自己的类加载器,满足一些特殊需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类的加载类,而在JDK1.2之后不再推荐去重写loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
- 在编写自定义类加载器时,如果没有太复杂的需求,可以直接继承URLClassLoader,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁
ClassLoader的使用说明
ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(但不包括Bootstrap ClassLoader)
部分通用的方法有
- getParent():返回该类加载器的超类/父类加载器
- loadClass(String name):加载名称为name的类,返回结果为java.lang.Class类的实例
- findClass(String name):查找名称为name的类,返回结果为java.lang.Class类的实例
- findLoadedClass(String name):查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例
- definedClass(String name,byte[] b,int off,int len):吧字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例
- resolveClass(Class<?> c):连接指定的一个Java类
- getClassLoader():获取当前类的类加载器
- Thread.currentThread().getContextClassLoader():获取当前线程上下文的ClassLoader
- ClassLoader.getSystemClassLoader():获取系统的类加载器
- DriverManager.getCallerClassLoader():获取调用者的类加载器
还有关于类加载器的细节,除开BootStrap ClassLoader是C/C++写的,其他两个类加载器都是由Java语言写的,他们都在sun.misc.Launcher类中都得到了实现,因此Launcher类是Java虚拟机的入口应用
双亲委派机制
Java虚拟机对于字节码文件采取的是按需加载的方式,也就是说当需要使用该类是才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件是,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式
工作原理
- 如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果这个父类加载器还存在其父类加载器,则进一步向上委托,一次递归,请求最终将达到顶层的启动类加载器
- 如果父类加载器可以完成类加载的任务,就成功返回,倘若父类加载器无法完成此任务,子加载器才会尝试自己加,这就是双亲委派模式
优势
- 避免类的重复加载
- 保护程序安全,防止API被随意篡改
沙箱安全机制
如果在src下创建java.lang.String这个类,然后在这个类下定义main方法,尝试运行发现报错说没有main方法,这是因为String是rt.jar中的类,让我们尝试运行自定义的类的main方法时,rt.jar里的String可没有main方法,因此报错,这就保证了外界无法对rt.jar这类核心jar做任何修改,这就是沙箱安全机制
其他
-
在JVM中表示两个class对象是否为同一类存在的两个必要条件:
- 类的全限定类名要一致
- 加载这个类的类加载器必须相同
-
换句话说,在JVM中,即使这两个对象(class对象)来源同一个Class文件,被同一个虚拟机加载,但只要加载他们的类加载器实例对象不同,那么这两个类对象也是不相同的
-
KVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用是,JVM需要保证这两个类型的类加载器是相同的
-
Java程序对类的使用方式分为主动使用和被动使用
-
主动使用分为七种情况
-
创建类的实例
-
访问某个类的接口或者静态变量,或者对该静态变量赋值
-
调用类的静态方法
-
反射(比如Class.forName(“com.liu.Test”))
-
初始化一个类的子类
-
Java虚拟机启动时被标明为启动器的类
-
JDK7开始提供的动态语言支持
java.lang.invoke.MethodHandle实例的解析结果
REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
-
-
除了以上七种情况,其他使用Java类的方式都被看做是对类的被动使用,都不是导致类的初始化
-