JVM系列整体栏目
内容 | 链接地址 |
---|---|
【一】初识虚拟机与java虚拟机 | https://blog.csdn.net/zhenghuishengq/article/details/129544460 |
【二】jvm的类加载子系统以及jclasslib的基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/129610963 |
【三】运行时私有区域之虚拟机栈、程序计数器、本地方发栈 | https://blog.csdn.net/zhenghuishengq/article/details/129684076 |
深入理解jvm的类加载器子系统
1,jvm的内存结构
在jvm的内存中结构中,其主要结构如下。
在jvm内部,需要将磁盘上的字节码文件通过这个类加载加载到内存中。在类加载子系统中,也需要经过一定的阶段将才能将这个文件加载到内存的运行时数据区中,如一些加载,验证,准备,解析,初始化等工作。在加载到运行时数据区之后,内部主要由一些共享的方法区、堆,以及私有的程序计数器、虚拟机栈、本地方法栈这些。这些字节码最终是需要通过执行引擎去执行的,执行引擎中主要包括解释器,JIT即时编译器,垃圾回收器等。
2,类加载器加载过程
在类加载器子系统中,主要会经过加载,链接和初始化三个阶段,链接又包括验证,准备和解析三个阶段,所以合起来就是加载,验证,准备,解析,初始化五个阶段。
类加载器主要负责从文件系统或者网络中加载Class文件,并且类加载器只负责将文件加载,至于是否可以运行,还得由Execution Engine执行引擎决定。
2.1,加载阶段
加载阶段的加载器主要有引导类加载器,扩展类加载器,系统类加载器和自定义类加载器,主要是通过一个类的全限定名获取此类的二进制字节流,然后将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构,然后在内存中生成一个java.lang.Class文件,作为方法区这个类的各种数据的访问入口。其主要就是将文件加载出来
常见的类加载方式有以下几种方式
- 从本地系统直接加载
- 从网络中获取
- 从压缩包中获取,如zip
- 运行时生成,如动态代理
- 其他文件生成,典型的场景有:JSP应用
- 数据库中获取 .class文件
- 从加密文件中获取
- 反射,序列化,克隆等
2.2,链接阶段
链接阶段又可以分为三个阶段,分别是验证,准备和解析
2.2.1,验证
验证的主要目的在于确保Class文件的字节流中所包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全,相当于一种自我保护。如果编译器发现了有违法的信息之后,则编译器可以选择直接抛出异常或者拒绝编译。
主要包括四种验证:文件格式验证,元数据验证,字节码验证和符号引用验证。
2.2.2,准备
在准备阶段为类分配内存,并且设置该类的变量默认初始值,如整型的初始值为0。
public static int x = 10; //在准备阶段赋值默认值为0,并且分配内存
public static void main(String[] args) {
System.out.println(j);
}
这里主要是为变量进行一个默认的初始赋值,如果变量被static final修饰,那么这个变量会被变为常量,并且会在编译阶段就会分配内存。同时这里也不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
2.2.3,解析
就是将常量池内的符号引用转化为直接引用的过程,并随着JVM在执行完初始化之后再执行。
符号引用:以一组符号来描述引用的目标,只要能无歧义的定位到目标即可
直接引用: 相当于寻址的直接指针或者句柄
解析动作主要针对接口,类,字段,类方法,接口方法,方法类型,句柄和调用点限定符
2.3,初始化阶段(重点)
2.3.1,jclasslib的安装
在查看字节码文件之前,也可以在idea中安装查看对应的字节码指令的插件,在插件中搜索jclasslib即可,安装完成之后需要restart重启。
在安装完成之后,可以在view的位置来打开这个Bytecode字节码文件。
在点击这个Show Bytecode With Jclasslib 之后,就会出现以下的界面,会有一些版本,协议号,当前类,父类,接口数,文件数,方法数,属性数等。
2.3.2,clinit
初始化阶段就是执行类构造器方法()的过程,通过javac编译器自动收集类中的所有类变量赋值动作和静态代码块中的语句合并而来的。就是说这个clinit会将类变量的显示的初始化和静态代码块的初始化合并到一起,如果没有类变量的赋值操作或者静态代码块的赋值操作,那么这个clinit就不会出现在字节码文件中。
并且在整个流程中,变量的初始赋值是在这个准备阶段,而真正的赋值是在这个初始化阶段。
public static int x = 10; //当前阶段中此时x的值为10
在这个Methods中,可以看到给这个类变量赋值,是有这个clinit的
或者再静态代码块中给类变量赋值,也是可以有这个clinit的,可以看下图右边Methods中的第二点。
如果该类具有父类,那么JVM会保证先加载父类的 ,再加载子类的 。并且在多线程中,虚拟机会保证一个类的 方法会加同步锁
2.3.3,init
在每个类中,都会有一个隐示的构造方法或者显示的构造方法,通过 来进行初始化。如在以下的代码中,显示的写了一个代码的构造器,先将初始值加载,或者再加载构造器里面的值。
3,类加载器
3.1,类加载器的分类
在加载阶段中,主要有引导类加载器,扩展类加载器,应用程序类加载器和自定义加载器。在jvm中,规定支持两种类加载器,分別是引导类加载器和自定义类加载器,而扩展类和系统类都是属于自定义类加载器。
并且在这几个来加载器中,这个引导类加载器是用c语言写的,而其他的类加载器都是使用这个JAVA语言写的。接下来通过代码查看一下这个类加载器,也可以发现这个引导类加载器不是java语言写的,所以获取不到,并且这个自定义类的加载器是通过系统类加载器来加载的。
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(systemClassLoader);
//获取上层扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
//sun.misc.Launcher$ExtClassLoader@15615099
System.out.println(extClassLoader);
//获取上层引导类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
//null 尝试获取失败,该类由c语言编写
System.out.println(bootStrapClassLoader);
//获取自定义类类加载器,以当前类为例
ClassLoader classLoader = ClassLoad.class.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
// 可以发现当前自定义类的了地价在其为系统类加载器
System.out.println(classLoader);
而像一些系统的核心类库,如String这种,是通过引导类加载器加载的。并且该加载器作为扩展类和系统类加载器的父类加载器,该加载器主要加载包名为java,javax,sun等开头的类
ClassLoader StringClassLoader = String.class.getClassLoader();
System.out.println(StringClassLoader); //null
接下来可以获取一下这个引导类中,加载的全部内容
//获取引导类加载器可以加载的全部url
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) {
System.out.println(urLs[i]);
}
3.2,自定义类加载器的场景
一般情况使用引导类,扩展类和系统类是可以满足日常的开发需求的,但是在必要时,也可以手动自定义其他的类加载器。
引入自定义类加载器的原因
- 隔离加载类
- 修改类加载方式
- 扩展加载源
- 防止源码泄漏
自定义类加载器的实现步骤
- 1,可以通过继承抽象类 java.lang.ClassLoader 类,实现自定义类加载器
- 2,重写findClass()方法,然后将逻辑写在方法内部
- 3,如果没有特别复杂的要求,可以直接继承URLClassLoader类
获取ClassLoader的途径
- 获取当前类的ClassLoader:clazz.getClassLoader()
- 获取上下文线程方式:Thread.currentThread.getContextClassLoader()
- 获取系统的ClassLoader:ClassLoader.getSystemClassLoader()
- 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()
3.3,双亲委派机制
在jvm中,对class文件采用的是按需加载的方式,也就是说在需要使用到该类时才会加载,然后将class文件加载到内存生成class对象。并且java虚拟机采用的是一种双亲委派机制模式
其工作原理如下:
- 1,如果一个类加载器收到了加载请求,他并不会自己去加载,而是将这个请求委托给父类加载器去执行
- 2,如果父加载器还有其他的父加载器,那么会进一步的向上委托,一次递归到顶点
- 3,如果父类可以完成任务,则将值返回;反之,则由子类尝试去加载
其源码如下
// 检查当前类加载器是否已经加载了该类
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();
//都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
c = findClass(name);
通过源码也可以知道:
1,首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
2,如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
3,如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。ClassLoader的loadClass方法,里面实现了双亲委派机制。
双亲委派机制的好处
1,沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
2,避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
3.4,自定义类加载器
需要继承ClassLoader类,并且重写里面的findClass()方法。可以在本地磁盘里面创建一个类,如何加载的时候直接通过本地磁盘加载,而不需要使用到那几个类加载器加载,这样就完成了自定义类的加载器
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
* @Author: zhenghuisheng
* @Date: 2023/3/16 23:09
*/
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
/**
* 下面的磁盘路径需要手动创建
* @param args
* @throws Exception
*/
public static void main(String args[]) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/zhenghuisheng/jvm 几级目录,将User类的复制类User.class丢入该目录
//需要创建一个User类在这个路径下
Class clazz = classLoader.loadClass("com.zhenghuisheng.jvm.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
3.5,打破双亲委派模型
应用程序里面有这个类,磁盘里面也有这个自定义的类,直接通过磁盘一次性加载,不需要利用到父类加载器,这样就打破了双亲委派机制。主要就是通过重写里面的findClass()方法,将里面的双亲委派机制的逻辑修改即可。让这个findClass直接找磁盘里面的路径,而不需要再写那些层层找父加载器加载即可。
3.6,其他
JVM中的两个class对象是否为同一个类
- 类的完整名必须一致
- 加载这个类的ClassLoader必须相同
在jvm中,即使这两个对象来源于同一个Class文件,被同一个虚拟机所加载,但只要加载他们的ClassLoader实例对象不一致,那么这两个类对象也是不相等的。