龙空技术网

一分钟掌握JVM的类加载机制

DT天梦 115

前言:

现在各位老铁们对“java程序中必须有主类吗”大约比较注重,看官们都需要了解一些“java程序中必须有主类吗”的相关内容。那么小编在网上汇集了一些对于“java程序中必须有主类吗””的相关资讯,希望兄弟们能喜欢,看官们一起来学习一下吧!

类的生命周期

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

类的加载

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

通过一个类的全限定名来获取其定义的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。类的连接

2.1)连接之验证

验证阶段确保被加载的类的正确性,大致会完成4个阶段的检验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范,比如是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,比如这个类是否有父类等。字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。符号引用验证:确保解析动作能正确执行。

2.2)连接之准备

准备阶段为类的静态变量分配内存,并将其初始化为默认值。

类的静态变量分配内存,而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。设置的初始值是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

// 在准备阶段,value的值为0,而不是3,value赋值为3的动作将在初始化阶段才会执行public static int value = 3;
类的属性同时被final和static修饰,那么在准备阶段变量value就会被初始化为显式指定的值。
// 在准备阶段,value的值为3public static final int value = 3;

2.3)连接之解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

类的初始化

初始化阶段为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

声明类变量是指定初始值使用静态代码块为类变量指定初始值

JVM初始化步骤:

假如这个类还没有被加载和连接,则程序先加载并连接该类 假如该类的直接父类还没有被初始化,则先初始化其直接父类假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

创建类的实例,也就是new的方式访问某个类或接口的静态变量,或者对该静态变量赋值调用类的静态方法反射,如Class.forName(“io.xiaojl.Test”)初始化某个类的子类,则其父类也会被初始化Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类类的结束

在如下几种情况下,Java虚拟机将结束生命周期:

执行了System.exit()方法程序正常执行结束程序在执行过程中遇到了异常或错误而异常终止由于操作系统出现错误而导致Java虚拟机进程终止类的加载

类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

寻找类加载器

Java虚拟机的角度来讲,只存在两种不同的类加载器:

启动类加载器:

它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;

所有其他的类加载器:

这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

Java开发人员的角度来看,类加载器可以大致划分为以下三类:

启动类加载器:

Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

扩展类加载器:

Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

应用程序类加载器:

Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。

因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,

因此如果编写了自己的ClassLoader,便可以做到如下几点:

在执行非置信代码之前,自动验证数字签名。动态地创建符合用户特定需要的定制化构建类。从特定的场所取得java class,例如数据库中和网络中。JVM类加载机制

2.1)全盘负责

当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

2.22)父类委托

也称为“双亲委派机制”,也就是先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

2.3)缓存机制

缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

双亲委派模型

其的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,

因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制加载过程:

1)当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2)当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

3)如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

4)若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

双亲委派模型意义:

系统类防止内存中出现多份同样的字节码保证Java程序安全稳定运行类的加载途径从本地系统中直接加载通过网络下载.class文件从zip,jar等归档文件中加载.class文件从专有数据库中提取.class文件 将Java源文件动态编译为.class文件类的加载方式命令行启动应用时候由JVM初始化加载通过Class.forName()方法动态加载通过ClassLoader.loadClass()方法动态加载

Class.forName()和ClassLoader.loadClass()区别:

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

注:Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass 方法即可。

package io.dt;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;/**   * <p>Title: MyClassLoader</p>   * * <p>Description: 自定义一个类加载器</p>   * * @author dt.xiao   * @date 2023年8月17日   */public class MyClassLoader extends ClassLoader {	private String classFileRoot;		public MyClassLoader(String classFileRoot) {		this.classFileRoot = classFileRoot;	}	@Override	protected Class<?> findClass(String name) throws ClassNotFoundException {		byte[] classData = loadClassData(name);		if (classData == null) {		    throw new ClassNotFoundException();		} else {		    // 获取到class的二进制文件数据后,就可以对它进行自定义操作(比如解密、安全校验等)				    return defineClass(name, classData, 0, classData.length);		}	}		private byte[] loadClassData(String className) {        String fileName = this.classFileRoot + File.separatorChar + className.replace('.', File.separatorChar) + ".class";        try {            InputStream ins = new FileInputStream(fileName);            ByteArrayOutputStream baos = new ByteArrayOutputStream();            int bufferSize = 1024;            byte[] buffer = new byte[bufferSize];            int length = 0;            while ((length = ins.read(buffer)) != -1) {                baos.write(buffer, 0, length);            }            return baos.toByteArray();        } catch (IOException e) {            e.printStackTrace();        }        return null;    }}

编写一个测试类

package io.dt;/**   * <p>Title: MyClassLoaderTest</p>   * * <p>Description: 自定义加载器的测试类</p>   * * @author dt.xiao   * @date 2023年8月17日   */public class MyClassLoaderTest {	public static void main(String[] args) {				MyClassLoader mcl = new MyClassLoader("D:\\myclass");				Class<?> clz = null;		try {			clz = mcl.loadClass("io.dt.entity.UserEntity");			Object obj = clz.newInstance();						System.out.println("该类的加载器:"+obj.getClass().getClassLoader());					} catch (ClassNotFoundException e) {			e.printStackTrace();		} catch (InstantiationException e) {			e.printStackTrace();		} catch (IllegalAccessException e) {			e.printStackTrace();		}	}}

测试执行结果

该类的加载器:io.dt.MyClassLoader@2e7b2e05

总结

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

1、这里传递的文件名需要是类的全限定性名称,即io.dt.entity.UserEntity格式的,因为 defineClass 方法是按这种格式进行处理的。2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。3、这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 io/dt/entity/UserEntity.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。

标签: #java程序中必须有主类吗