java类加载机制、类加载器、自定义类加载器的案例
作者:康斌825 发布时间:2023-02-06 07:35:33
类加载机制
java类从被加载到JVM到卸载出JVM,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。
其中验证、准备和解析三个部分统称为连接(Linking)。
1、加载
加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
类的加载通过JVM提供的类加载器完成,类加载器是程序运行的基础。程序在启动的时候,并不会一次性加载程序所要用到的所有class文件,而是根据需要,通过java的类加载器机制(classLoader)来动态加载某个class文件到内存中。
jvm在运行时会产生三个classLoader:
启动类加载器(BootStrap ClassLoader):是java类加载层次中最顶层的类加载器,负责加载jdk中的核心类库。由C++实现,不是classLoader的子类。
扩展类加载器(Extension ClassLoader):负责加载java的扩展类库,比如lib/ext或者java.ext.dirs系统属性指定的目录中的jar包。父类加载器为null。
系统类加载器(App ClassLoader):负责加载来自java命令的-classpath选项、java.class.path系统属性所指定的jar包和类路径。程序可以通过classLoader的静态方法getSystemClassLoader(),来获取系统类加载器。由java语言实现,父类加载器为ExtClassLoader。
除了java默认提供的这三个classLoader之外,用户可以根据需要定义自己的classLoader,这些自定义的classLoader都必须继承自java.lang.ClassLoader类。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据。通常有如下几种情况:
从本地文件系统加载class文件,这是绝大部分实例程序的类加载方式。从jar包加载class类,这种方式也很常见。通过网络加载class类把一个java源文件动态编译,并执行加载,比如jsp。
2、连接
当类被加载之后,系统为之生成一个对应的class对象,接着进入连接阶段(验证-准备-解析),连接阶段负责把类的二进制数据合并到jre中。
验证:用于检测被加载的类是否有正确的内部结构,并和其他类协调一致。
包括四种验证:文件格式验证、元数据验证、字节验证和符号引用验证。准备:负责为类变量分配内存,并设置默认初始值。
解析:将类的二进制数据中的变量进行符号引用替换成直接引用。
3、初始化
在初始化阶段,主要为类的静态变量赋予正确的初始值。其实就是执行类构造器<clinit>()方法的过程。
在java类中对类变量指定初始值有两种方式:a.声明类变量时指定初始值;b.使用静态初始化块为类变量指定初始值。
jvm初始化一个类包含如下步骤:
加载并连接该类先初始化其直接父类依次执行初始化语句当执行第2步时,系统对直接父类的初始化也遵循1~3,以此类推。
当一个类被主动引用后会触发初始化过程:
遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
当使用jdk7+的动态语言支持时,如果java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发器 初始化。
当一个类如果是被动引用的话,不会触发初始化过程:
通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义该字段的类才会被初始化,因此当我们通过子类来引用父类中定义的静态字段时,只会触发父类的初始化,而不会触发子类的初始化。
通过数组定义来引用类,不会触发此类的初始化。
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
4、使用
(略)
5、卸载
如果出现下面的情况,类就会被卸载:
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
类加载器
类加载器负责加载所有的类。其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。
正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。
这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
前面我们已经介绍了java中的几种类加载器,下面我们用一张图展示他们的层次关系:
类加载步骤
类加载器加载class大致需要如下8个步骤:
检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
从文件中载入Class,成功后跳至第8步。
抛出ClassNotFountException异常。返回对应的java.lang.Class对象。
类加载机制
全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制:保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。
这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
自定义的类加载器
jvm除跟类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过拓展ClassLoader的子类,并重写该ClassLoader所包含的方法实现自定义的类加载器。
ClassLoader有如下两个关键方法:
loadClass(String name,boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类的class对象。
findClass(String name):根据指定名称来查找类如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写findClass()方法而不是loadClass()方法。
classLoader()方法的执行步骤:
1)findLoadedClass():来检查是否加载类,如果加载直接返回;
2)父类加载器上调用loadClass()方法。如果父类加载器为null,则使用跟类加载器加载;
3)调用findClass(String)方法查找类。从这边可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托,缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。如有错误或未考虑完全的地方,望不吝赐教。
来源:https://blog.csdn.net/kangbin825/article/details/105335633
猜你喜欢
- AOP拦截Controller获取@PathVariable注解传入参数前言:最近项目中需要对controller传入的应用标识(appMa
- 前言 红包文化源远流长。从古时的红色纸包,到手
- 图的实际应用在现实生活中,有许多应用场景会包含很多点以及点点之间的连接,而这些应用场景我们都可以用即将要学习的图这种数据结构去解决。地图:我
- C++/java 继承类的多态详解学过C++和Java的人都知道,他们二者由于都可以进行面向对象编程,而面向对象编程的三大特性就是封装、继承
- 对于 * ,学过AOP的应该都不会陌生,因为代理是实现AOP功能的核心和关键技术。那么今天我们将开始 * 的学习:一、引出 * 生活中
- 要想使Java运行,我们可以设计一个面向Java语言特性的虚拟机,并通过编译器将Java程序转换为它可以识别的指令序列,也称为Java字节码
- 1.前置准备默认服务器上的hadoop服务已经启动本地如果是windows环境,需要本地配置下hadoop的环境变量本地配置hadoop的环
- 前言当同一类型的很多对象组成一个树结构的时候,可以考虑使用组合模式,组合模式涉及三个类:Component接口:定义树的各个节点的一些操作L
- 星期天小哼和小哈约在一起玩桌游,他们正在玩一个非常古怪的扑克游戏——“小猫钓鱼”。游戏的规则是这样的:将一副扑克牌平均分成两份,每人拿一份。
- 本文介绍了Java 三种方式实现接口校验,主要包括AOP,MVC * ,分享给大家,具体如下:方法一:AOP代码如下定义一个权限注解pack
- 本文实例讲述了Winform下实现图片切换特效的方法,是应用程序开发中非常实用的一个功能。分享给大家供大家参考之用。具体方法如下:本实例源自
- 1. 运算符是什么?1.1 定义:对常量和变量进行运算操作的符号程序对数据进行运算时要用运算符1.2 常见运算符的概述1.3 表达式1.3.
- 本文实例讲述了Java数组的定义、初始化、及二维数组用法。分享给大家供大家参考,具体如下:数组的定义1.数组是有序数据的集合,数组中的每个元
- 本文实例讲述了C#编程实现获取文件夹中所有文件的文件名。分享给大家供大家参考,具体如下:想实现这样一个功能:批量修改一个目录所有jpg文件的
- 背景今天学习Springboot,但是用的apache-maven 3.0 ,导入springboot1.5.19 ,Maven项目老是爆红
- 本文实例为大家分享了java导出百万以上数据的excel文件,供大家参考,具体内容如下1.传统的导出方式会消耗大量的内存,2003每个she
- 这篇文章主要介绍了Java反射通过Getter方法获取对象VO的属性值过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定
- 本文基于jdk1.8进行分析。ReentrantLock是一个可重入锁,在ConcurrentHashMap中使用了ReentrantLoc
- 前言这几天看《Java并发编程之美》的时候又遇到了ThradLocal这个类,不得不说,这个类在平时很多场景都遇得到,所以对其进行一个系统性
- 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题:class Grandpa{static{System.o