双亲委派机制
由于Java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底由谁加载的问题。双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过, 再由顶向下进行加载。所以类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
1. 类加载器加载流程
判断是否可以加载就是看当前类加载器设置的类加载路径目录是否有这个类,有的话就加载返回这个类。
类加载器的父子关系可以通过classloader -t
查看: ArthasClassloader的类加载器可以不用管。
2. 双亲委派机制的作用
- 保证类加载的安全性
通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.string
,确保核心类库的完整性和安全性。 - 避免重复加载
双亲委派机制可以避免同一个类被多次加载。
3. 打破双亲委派机制
如果要改变类加载的顺序,有三种方式:
- 自定义类加载器:自定义类加载器并且重写loadclass方法,就可以将双亲委派机制的代码去除。
- 线程上下文类加载器:利用上下文类加载器加载类,比如JDBC和JNDI等。
- Osgi框架的类加载器: 历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载。
4. 自定义类加载器
Tomcat通过这种方式实现应用之间类的隔离。如果不打破双亲委派机制,应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的MyServlet类就无法被加载了。 Tomcat使用了自定义类加载器来实现应用之间类的隔离。每一个应用会有一个独立的类加载器加载对应的类。
4.1 Classloader原理
Classloader接口中有4个核心方法: Classloader接口源码:
4.2 重写loadClass方法实现
package com.rocket.learn.jvm;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.regex.Matcher;
public class MyClassloader extends ClassLoader{
private String basePath;
private final static String FILE_EXT = ".class";
public void setBasePath(String basePath) {
this.basePath = basePath;
}
private byte[] loadClassData(String name) {
try {
String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
return Files.readAllBytes(Paths.get(basePath + tempName + FILE_EXT));
} catch (Exception e) {
System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
return null;
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 默认我们字节码都会添加Object作为父类,
// 所以会先去加载java.lang.Object, 本目录没有避免找不到java。lang
// 需要交给父类去加载java.lang.Object
if(name.startsWith("java.")){
return super.loadClass(name);
}
byte[] classBytes = loadClassData(name);
return defineClass(name, classBytes, 0, classBytes.length);
}
}
package com.rocket.learn.jvm;
import java.io.IOException;
public class Jvm01 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
MyClassloader classloader = new MyClassloader();
classloader.setBasePath("F:\\Code\\demo-test\\target\\classes\\");
Class<?> stringClass = classloader.loadClass("com.rocket.learn.jvm.UserInfo");
System.out.println(stringClass);
}
}
package com.rocket.learn.jvm;
public class UserInfo {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
需要注意的是仍然不能正常加载java.lang.String这种java中的核心类,原因是加载的后续步骤校验就会通不过: 以Jdk8为例,ClassLoader类中提供了构造方法设置parent的内容:
其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader。
问题:两个自定义类加载器加载相同限定名的类,不会冲突吗?
不会冲突,在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。 可以通过arths命令
sc -t 全类名
查看: 发现com.rocket.learn.jvm.UserInfo被加载了两次。
5. 线程上下文类加载器
JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动。DriverManager类位于rt.jar包中,由启动类加载器加载。 由于我们的驱动包不在jdk目录下面,Bootstrap类加载器只能交给Application类加载器去处理,然而怎么知道要加载的驱动在哪儿?
5.1 SPI机制
spi全称为(Service Provider Interface),是JDK内置的一种服务提供发现机制。
spi的工作原理:
- 在classpath路径下的META-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。
- 使用ServiceLoader加载实现类。
5.2 分析DriverManager源码
public class Jvm02 {
public static void main(String[] args) throws SQLException {
String user = "root";
String pass = "root";
String jdbcUrl = "jdbc:mysql://hadoop104:3307/test?useSSL=false";
Connection conn = DriverManager.getConnection(jdbcUrl, user, pass);
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery("select 1");
resultSet.next();
System.out.println(resultSet.getInt(1));
conn.close();
}
}
可见DriverManager是使用静态方法getConnection()获取连接的,调用静态方法之前需要执行静态代码块,点击查看DriverManager的静态代码块: 可以看到DriverManager里面使用了ServiceLoader.load()方法,使用SPI机制去加载Dirver的实现类:
DriverManager要使用应用类加载器进行加载驱动,需要先要拿到应用类加载器才行,直接获取DriverManager自己的类加载器肯定不行。
SPI机制中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。点击load()方法:
这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式(看起来像是没打破,那是因为这里指的的类加载器是AppClassLoader, 但是你想你可以随意指定其他),打破了双亲委派机制。
6. Osgi框架的类加载器
不必了解,Osgi已经被jiagsw代替。
7. 通过arths热修复线上问题
热部署/热修复指的是在服务不停止的情况下,动态地更新字节码文件到内存中。
7.1 线上部署arthas
在出问题的服务器上部署一个arthas,并启动
7.2 获取反编译文件
使用命令jad --source-only 类全限定名 > 目录/文件名.java
, 然后可以在反编译的文件基础上进行修复bug。
7.3 编译修改的文件
使用命令mc –c 类加载器的hashcode 目录/文件名.java -d 输出目录
编译文件。需要提前获取类加载器的hashcode,可以使用命令sc -d 修改的类全类名
得到他所用的类加载hashcode。
7.4 加载新的字节码
使用命令retransform class文件所在目录/xxx.class
,加载新的字节码。
注意事项
1、程序重启之后,字节码文件会恢复,除非将class文件放入jar包中进行更新。
2、使用retransform不能添加方法或者字段,只能更新现有的方法,而且不能更新正在执行中的方法。
3. 使用retransform只是用来应急,正常部署流程还是需要走的。