【JVM基础】Java类加载机制
一:类加载子系统
1:类加载器子系统的作用
-
类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
-
ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
-
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
2:加载器 ClassLoader 的角色
-
class file 存在于本地磁盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到 JVM 当中来的,根据这个文件实例化出N个一模一样的实例。
-
class file 加载到JVM中,被称为DNA元数据模板,放在方法区;
-
在.class文件->JVM->最终成为元数据模板,此过程就要一个运输工具(类装载器 ClassLoader),扮演一个快递员的角色。
3:类的加载过程
其中类加载的过程包括了加载
、验证
、准备
、解析
、初始化
五个阶段。在这五个阶段中,加载
、验证
、准备
和初始化
这四个阶段发生的顺序是确定的,而解析
阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载阶段
ClassLoader类的作用:
加载阶段:
-
通过一个类的全限定获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
-
加载.class文件的方式有:
-
从本地系统中直接加载;
-
通过网络获取,典型场景:Web Applet(小程序);
-
从zip压缩包中读取,比如:jar、war格式的文件
-
运行时计算生成,使用最多的是:动态代理技术。
-
验证阶段:确保被加载的类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证
: 验证字节流是否符合Class文件格式的规范;例如: 是否以0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。元数据验证
: 对字节码描述的信息进行语义分析(注意: 对比javac
编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如: 这个类是否有父类,除了java.lang.Object
之外。字节码验证
: 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。符号引用验证
: 确保解析动作能正确执行。
准备阶段:为类的静态变量分配内存,并将其初始化为默认值
- 为类变量分配内存并且设置该类变量的默认初始值,即
零值
- 这里不包含用final修饰的static,因为final在编译的时候就已经分配了默认值,准备阶段会显式初始化
- 注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
举例:
变量a在准备阶段会赋初始值,但不是1,而是0,在初始化阶段会被赋值为 1
public class HelloApp {
private static int a = 1; //prepare:a = 0 ---> initial : a = 1
public static void main(String[] args) {
System.out.println(a);
}
}
解析阶段:把类中的符号引用转换为直接引用
-
将常量池内的符号引用转换为直接引用的过程
-
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
-
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
-
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
初始化阶段
-
初始化阶段就是执行类构造器方法()的过程;
-
此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来;
-
构造器方法中指令按语句在源文件中出现的顺序执行;
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
注意:如果当前类不存在static变量,那么它的字节码文件是不会存在( )
public class ClassInitTest {
private static int num = 1;
static {
num = 3;
number = 20;
System.out.println(num); //3
//System.out.println(number); //报错:非法的前向引用(可以赋值,但不能调用)
}
//linking阶段的prepare环节:number = 0 --> Initialization阶段:20 --> 10
private static int number = 10;
public static void main(String[] args) {
System.out.println(ClassInitTest.num); //3
System.out.println(ClassInitTest.number); //10
}
}
注意:若该类具有父类,JVM会保证子类的( )执行前,父类的( )已经执行完毕
public class ClinitTest1 {
static class Father{
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
}
}
如上代码,加载流程如下:
- 首先,执行 main( ) 方法需要加载 ClinitTest1 类
- 获取 Son.B 静态变量,需要加载 Son 类
- Son 类的父类是 Father 类,所以需要先执行 Father 类的加载,再执行 Son 类的加载
类初始化时机:
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName(“com.pdai.jvm.Test”))
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
总结
加载阶段:将.class文件加载到内存中,加载.class文件所需要使用到的类(例如:java/lang/System、java/lang/String等);
链接阶段:为类型变量赋予初始默认值;
初始化阶段:给类型变量赋值(程序员干的事!)(java/lang/System.out: 操作必须要在这一步完成后,才能进行)!
4:类加载器分类
-
JVM支持两种类型的加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader);
-
从概念上来说,自定义类加载器一般指的是程序中由开发人员自定义的一类加载器,但是 Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器;
-
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类 :
启动类加载器
: 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)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
- 在执行非置信代码之前,自动验证数字签名。
- 动态地创建符合用户特定需要的定制化构建类。
- 从特定的场所取得java class,例如数据库中和网络中。
代码说明:
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader); //null
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
}
System.out.println(classLoader); //null 再次证明我们无法获取到启动类加载器
*********启动类加载器**************
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/classes
null
**********扩展类加载器*************
/Users/xiexu/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
sun.misc.Launcher$ExtClassLoader@355da254
用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
- 那为什么还需要自定义类加载器?
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄露
如何自定义类加载器?
-
开发人员可以通过
继承抽象类java.lang.ClassLoader类
的方式,实现自己的类加载器,以满足一些特殊的需求 -
在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass( )方法,从而实现自定义的类加载类,但是在JDK1.2之后已
不再建议
用户去覆盖loadClass( )方法,而是建议
把自定义的类加载逻辑写在findclass( )方法中
-
在编写自定义类加载器时,如果没有太过于复杂的需求,可以
直接继承URIClassLoader类
,这样就可以避免自己去编写findclass( )方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。代码举例:
/** * 自定义用户类加载器 */ public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(name); if (result == null) { throw new FileNotFoundException(); } else { return defineClass(name, result, 0, result.length); } } catch (FileNotFoundException e) { e.printStackTrace(); } throw new ClassNotFoundException(name); } private byte[] getClassFromCustomPath(String name) { //从自定义路径中加载指定类:细节略 //如果指定路径的字节码文件进行了加密操作,则需要在此方法中进行解密操作。 return null; } public static void main(String[] args) { CustomClassLoader customClassLoader = new CustomClassLoader(); try { Class<?> clazz = Class.forName("One", true, customClassLoader); Object obj = clazz.newInstance(); System.out.println(obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
5:关于ClassLoader类的了解
-
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
-
常用的方法及描述