看过很多讲JVM类加载的文章,觉得不够通俗,下面是我总结的,只要有实际开发经验并了解过类加载器的,应该都能看懂了。
---------------------------------------------------------------------------------------------------------------------------------
java.exe调用底层的jvm.dll文件创建java虚拟机(C++实现)
java.exe 底层C或C++实现,通过C++实现的代码调用C、C++语言的库函数jvm.dll,创建java虚拟机。
dll文件相当于java的jar包。
C++语言的启动程序,整了一个JVM,然后JVM会创建很多java实现的类加载器,通过这些类加载器去真正调用类加载器里面loadClass方法,去加载磁盘上的字节码文件。
加载到内存之后,C语言会直接调用main方法。
-------------------分隔符----------------------------------------上面是对jvm简单印象------------------------------
记得很多年前,我们更新项目,如果小改动或者打个小补丁,都是直接替换class文件,然后重启即可。
有没有考虑过target里面的.class文件是什么样的,如何加载到内存里的,其实这个过程就是类加载。
javap -v 类名.class 能把字节码文件转换成可读性高一点的内容,但是两者是一个文件,无区别,只是可读性好一点。(想读的可以百度下字面量)
-----------------------------------------------------------------------------------------------------------------------------
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute(){//一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c= (a+b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
public class User {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void sout() {
System.out.println("=======自己的加载器加载类调用方法=======");
}
}
奉上部分源码:
初始化Launcher
launcher.getClassLoader(这里是获取的类加载器),ClassLoader loader在初始化LauNcher时就赋值了,是AppClassLoader。
证明:
验证:是否符合规范
准备:静态变量赋一个初始值,int为0 boolean为false
解析:用人话解释 符号引用替换为直接引用,比如main方法加载到内存中是有内存地址的(方法在内存区域的一个位置或地址),main这种静态的名称就是符号,它的位置就是代码的直接引用。(将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过 程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用)
初始化:静态变量初始化为指定值,执行静态代码块
其实这也就解释了,为什么在类里定义的静态变量会在项目启动的时候就加载好了
注意:new 对象名(); 属于引用 ,会执行该类里的静态代码,
类名 变量名 = null; 不会执行类里的静态代码。
public class TestDynamicLoad {
static {
System.out.println("*************load TestDynamicLoad************");
}
public static void main(String[] args) {
new A();
System.out.println("*************load test************");
B b = null; //B不会加载,除非这里执行 new B()
}
}
class A {
static { System.out.println("*************load A************");
}
public A(){
System.out.println("*************initial A************");
}
}
class B { static {
System.out.println("*************load B************");
}
public B() {
System.out.println("*************initial B************");
}
}
//运行结果为:
*************load TestDynamicLoad************
*************load A************
*************initial A************
*************load test************
可以看出,B没有被加载。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------类加载器和双亲委派机制-----------------------------------------------
人话版解释如下:首先,要明白类加载器的概念
注意图中红色箭头,类加载器的名字,再对照jre目录,就能明白,
引导类加载器:C++语言实现的,加载lib目录下的jar包,引导类加载器对象不是java对象是C++生成的对象,所以看不到
扩展类加载器: 叫Ext,加载的是ext目录下的jar包
应用程序类加载器:叫App,加载的是自己写的类
Ext、App这两个都是Launcher类下的。
Launcher类是JVM启动器里面非常核心的一个类。 (Launcher中文翻译:启动器;桌面启动器;桌面;枪炮师;发射器)。
(2021年12月1日23点44分35秒开始修改)C++再创建好引导类加载器之后,它会通过C++代码调用Java代码里的Launcher类,它会初始化这个Launcher类,Launcher类就是JVM类加载器的一个启动器,它初始化这个Launcher类之后,再去加载其他类。在初始化Launcher类的过程中,会把其他类加载器全部生成出来。
类加载器示例代码:
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println("--------善良分割线1----------");
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassLoader.getParent();
System.out.println("the bootstrapLoader : "+bootstrapLoader);
System.out.println("the extClassloader : "+extClassLoader);
System.out.println("the appClassLoader : "+appClassLoader);
System.out.println("---------善良分割线2---------");
System.out.println("bootstrapLoader加载以下文件:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) {
System.out.println(urLs[i]);
}
System.out.println("---------善良分割线3---------");
System.out.println("extClassloader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("---------善良分割线4---------");
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
打印结果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
--------善良分割线1----------
the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@548c4f57
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
---------善良分割线2---------
bootstrapLoader加载以下文件:
file:/D:/Program%20Files/Java/jdk1.8.0_152/jre/lib/resources.jar
file:/D:/Program%20Files/Java/jdk1.8.0_152/jre/lib/rt.jar
file:/D:/Program%20Files/Java/jdk1.8.0_152/jre/lib/sunrsasign.jar
file:/D:/Program%20Files/Java/jdk1.8.0_152/jre/lib/jsse.jar
file:/D:/Program%20Files/Java/jdk1.8.0_152/jre/lib/jce.jar
file:/D:/Program%20Files/Java/jdk1.8.0_152/jre/lib/charsets.jar
file:/D:/Program%20Files/Java/jdk1.8.0_152/jre/lib/jfr.jar
file:/D:/Program%20Files/Java/jdk1.8.0_152/jre/classes
---------善良分割线3---------
extClassloader加载以下文件:
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
---------善良分割线4---------
appClassLoader加载以下文件:
D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar;D:\other\spring-boot-timer\target\classes;D:\develop\repository\org\springframework\boot\spring-boot-starter-web\1.5.8.RELEASE\spring-boot-starter-web-1.5.8.RELEASE.jar;D:\develop\repository\org\springframework\boot\spring-boot-starter\1.5.8.RELEASE\spring-boot-starter-1.5.8.RELEASE.jar;D:\develop\repository\org\springframework\boot\spring-boot\1.5.8.RELEASE\spring-boot-1.5.8.RELEASE.jar;D:\develop\repository\org\springframework\boot\spring-boot-autoconfigure\1.5.8.RELEASE\spring-boot-autoconfigure-1.5.8.RELEASE.jar;D:\develop\repository\org\springframework\boot\spring-boot-starter-logging\1.5.8.RELEASE\spring-boot-starter-logging-1.5.8.RELEASE.jar;D:\develop\repository\ch\qos\logback\logback-classic\1.1.11\logback-classic-1.1.11.jar;D:\develop\repository\ch\qos\logback\logback-core\1.1.11\logback-core-1.1.11.jar;D:\develop\repository\org\slf4j\jcl-over-slf4j\1.7.25\jcl-over-slf4j-1.7.25.jar;D:\develop\repository\org\slf4j\jul-to-slf4j\1.7.25\jul-to-slf4j-1.7.25.jar;D:\develop\repository\org\slf4j\log4j-over-slf4j\1.7.25\log4j-over-slf4j-1.7.25.jar;D:\develop\repository\org\springframework\spring-core\4.3.12.RELEASE\spring-core-4.3.12.RELEASE.jar;D:\develop\repository\org\yaml\snakeyaml\1.17\snakeyaml-1.17.jar;D:\develop\repository\org\springframework\boot\spring-boot-starter-tomcat\1.5.8.RELEASE\spring-boot-starter-tomcat-1.5.8.RELEASE.jar;D:\develop\repository\org\apache\tomcat\embed\tomcat-embed-core\8.5.23\tomcat-embed-core-8.5.23.jar;D:\develop\repository\org\apache\tomcat\tomcat-annotations-api\8.5.23\tomcat-annotations-api-8.5.23.jar;D:\develop\repository\org\apache\tomcat\embed\tomcat-embed-el\8.5.23\tomcat-embed-el-8.5.23.jar;D:\develop\repository\org\apache\tomcat\embed\tomcat-embed-websocket\8.5.23\tomcat-embed-websocket-8.5.23.jar;D:\develop\repository\org\hibernate\hibernate-validator\5.3.5.Final\hibernate-validator-5.3.5.Final.jar;D:\develop\repository\javax\validation\validation-api\1.1.0.Final\validation-api-1.1.0.Final.jar;D:\develop\repository\org\jboss\logging\jboss-logging\3.3.1.Final\jboss-logging-3.3.1.Final.jar;D:\develop\repository\com\fasterxml\classmate\1.3.4\classmate-1.3.4.jar;D:\develop\repository\com\fasterxml\jackson\core\jackson-databind\2.8.10\jackson-databind-2.8.10.jar;D:\develop\repository\com\fasterxml\jackson\core\jackson-annotations\2.8.0\jackson-annotations-2.8.0.jar;D:\develop\repository\com\fasterxml\jackson\core\jackson-core\2.8.10\jackson-core-2.8.10.jar;D:\develop\repository\org\springframework\spring-web\4.3.12.RELEASE\spring-web-4.3.12.RELEASE.jar;D:\develop\repository\org\springframework\spring-aop\4.3.12.RELEASE\spring-aop-4.3.12.RELEASE.jar;D:\develop\repository\org\springframework\spring-beans\4.3.12.RELEASE\spring-beans-4.3.12.RELEASE.jar;D:\develop\repository\org\springframework\spring-context\4.3.12.RELEASE\spring-context-4.3.12.RELEASE.jar;D:\develop\repository\org\springframework\spring-webmvc\4.3.12.RELEASE\spring-webmvc-4.3.12.RELEASE.jar;D:\develop\repository\org\springframework\spring-expression\4.3.12.RELEASE\spring-expression-4.3.12.RELEASE.jar;D:\develop\repository\org\jodd\jodd-all\4.3.2\jodd-all-4.3.2.jar;D:\develop\repository\com\alibaba\fastjson\1.1.26\fastjson-1.1.26.jar;D:\develop\repository\org\bouncycastle\bcprov-jdk16\1.46\bcprov-jdk16-1.46.jar;D:\develop\repository\com\whalin\Memcached-Java-Client\3.0.2\Memcached-Java-Client-3.0.2.jar;D:\develop\repository\commons-pool\commons-pool\1.6\commons-pool-1.6.jar;D:\develop\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\develop\repository\org\apache\commons\commons-lang3\3.7\commons-lang3-3.7.jar;D:\develop\repository\org\apache\httpcomponents\httpclient\4.5.4\httpclient-4.5.4.jar;D:\develop\repository\org\apache\httpcomponents\httpcore\4.4.8\httpcore-4.4.8.jar;D:\develop\repository\commons-codec\commons-codec\1.10\commons-codec-1.10.jar;D:\develop\repository\org\projectlombok\lombok\1.16.18\lombok-1.16.18.jar;D:\Program Files\JetBrains\IntelliJ IDEA 2021.1.2\lib\idea_rt.jar
ext加载器加载的文件,在ext目录下。
-------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
所有的类加载器,都继承自ClassLoader。
Launcher.getLauncher();(这个还是写在这里,比较直观,以后再拆分,现在写的很细,会有些乱) URLClassLoader后续再说,这个类会通过读取路径,把文件加载到内存中,先知道即可。
一路跟进去,注意图中箭头和红框
由此可发现:
1、在初始化Launcher类的过程中,会把ExtClassLoader和AppClassLoader初始化。
2、AppClassLoader的parent是ExtClassLoade,ExtClassLoader的的parent是null(引导类加载器,没有)。(parent是一个属性,为ClassLoader,见URLClassLoader)
其实Ext的ClassLoader算是引导类加载器,到是引导类加载器是C++写的。
---------------------------------------------------------------------------------------------------------------------------------
可以打印加载的内容,用图中的代码.
会发现App和Ext加载的内容,就体现了双亲委派,有些虽然打印出来了,但是不会再加载。
双亲委派流程用人话讲:
比如加载一个自己写的类A,
1、首先通过应用程序类加载器去加载,它会先检查有没有加载,如果有直接返回,如果没有就向上委托,让父加载器去加载,就这样,一直到引导类加载器,必然不会找到我自己写的A类。
2、既然引导类加载器也找不到,就会向下级委托加载,一直这样,到应用程序类加载器,它会到ClassPath下面找,这样就能找到了。
注意:这里说不是父类的意思,是父加载器
加载自定义类的都是应用程序类加载器,源码就这么写的,
应用程序类加载器的loadClass方法
注意,现在parent不为空,应用程序类加载器的父加载器为:扩展类加载器ExtClassLoader,会循环调用loadClass,
扩展类加载器ExtClassLoader调用loadClass,开始扩展类加载器loadClass方法。(可以看做应用程序类加载器向上委托)。
parent为null,父加载器为:引导类加载器BootstrapLoader,C++产生的对象,所以为null
走else内的findBootstrapClassOrNull(name),(可以看做扩展类加载器向上委托)。
调用本地native方法,去lib目录下加载相应类
必然为null,lib目录下是没有Math的,
扩展类加载器,向上委托引导类加载器加载Math类,引导类加载器没有找到,
那扩展类加载器就自己尝试加载--------------------(此处存疑,没有确定c = findClass(name);为扩展类加载器自己找)。
向下走
都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
没有找到,会抛异常
扩展类加载器循环调用loadClass方法抛异常,被捕获,回到应用程序类加载器的loadClass方法。
应用程序类加载器的loadClass方法,继续运行,因为没有找到,c继续为null.
URLClassLoader的findClass方法,找到了Math,正常返回,这时c就赋值了
双亲委派主要部分结束。(debug视频已出,后期上链接,需要可以留言)
URLClassLoader里的findClass才是去拿文件的,装载到内存调用的是defineClass方法,跟到最后还是native本地方法。
---------------------------------------------------------------------------------------------------------------------------
要考虑为什么这么做:
jvm开发人员才知道,我只是猜测,
1、web应用程序95%都是自己写的类,都是应用程序类加载器加载,只有第一次加载的时候,才会循环走一遍,以后再用,直接在程序类加载器就能拿到结果。
如果都从引导类走,那95%的类,都要走一遍上面的两个类,每次加载都要走。
记住Ext是加载不到自定义类的,因为是嵌套调用,会继续用App去记载。看上面源码注意此时执行的是哪一个加载类,这个加载类的父类是谁。
-----------------------------------------------------------------------------------------------------------------------------
那为什么要用这种机制呢
1、沙箱安全机制,防止核心API库呗随意篡改
2、避免类的重复加载,保证加载类的唯一性
可以自己写个Sring类试试,参考图中类包名类名,最终会有引导类加载器返回JDK中的String,但是找不main方法,就会报错
package java.lang;
//双亲委派保证被加载类的唯一性测试
public class String {
public static void main(String[] args) {
System.out.println("**************My String Class**************");
}
}
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
---------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------
自定义类加载器
人话版:
核心方法就是加载类的逻辑
根据位置找到这个类,再做一系列的加载逻辑。
重写findClass方法
继承ClassLoader 类
注意,ClassLoader 类中的findClass方法是一个空实现,是要重写的。
defineClass方法调用一系列本地方法,进行加载,复用即可。
package com.jvm;
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
* @date 2021/12/2 1:52
*/
//自定义类加载器示例
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();
}
}
}
public static void main(String[] args) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class<?> clazz = classLoader.loadClass("com.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj,null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
=======自己的加载器加载类调用方法=======
com.jvm.MyClassLoaderTest$MyClassLoader
--------------------------------------------------------------------------------------------------------------------
自定义类加载器 默认的父加载器是 应用程序类加载器(源码可以看图,初始化时传的是Launcher里的ClassLoader,Launcher的ClassLoader就是AppClassLoader),自定义类加载器 也遵守双亲委派。
为什么这样设计? 答:还是为了满足双亲委派机制。
---------------------------------------------------------------------------------------------------------------------------------
打破双亲委派机制
这个先说下思路吧,今天没时间写了,改天出个视频。
直接用自定义类加载器加载,不让父加载器加载
1、在加载类的时候,调用的是loadClass方法,我复制这个方法到自己的类加载器,重写即可,修改双亲委派的规则。
2、修改加载规则,比如:如果是需要打破双亲委派机制的类,就直接用自定义加载器加载。
如果是其他类,则走双亲委派机制,如:Object类
package com.jvm;
import java.io.FileInputStream;
import java.lang.reflect.Method;
//打破双亲委派机制示例
public class MyClassLoaderTest2 {
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);
return defineClass(name,data,0,data.length);
}catch (Exception e){
throw new ClassNotFoundException();
}
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
**/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
long t1 = System.nanoTime();
if (!name.startsWith("com.jvm")){
System.out.println("name为:"+name);
c = this.getParent().loadClass(name);
}else {
System.out.println("name为:"+name);
c = findClass(name);
}
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
public static void main(String[] args) throws Exception{
MyClassLoader classLoader = new MyClassLoader("D:/test");
//尝试用自己改写类加载机制去加载自己写的java.lang.String.class
Class<?> clazz = classLoader.loadClass("com.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj,null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
name为:com.jvm.User1
name为:java.lang.Object
name为:java.lang.String
name为:java.lang.System
name为:java.io.PrintStream
=======自己的加载器加载类调用方法=======
com.jvm.MyClassLoaderTest2$MyClassLoader
---------------------------------------------------------------------------------------------------------------------------------
Tomcat是否打破双亲委派机制
打破了
主要是为了不同项目,隔离运行,这个改天会出一份详细的资料
https://blog.csdn.net/b416055728/article/details/121455040
-------------------------------------------------------------------------------------------------------------------------------