类加载器的作用
负责将.class文件加载在到内存中。
类加载过程
- 加载
通过类的全名,把class字节码加载到内存中。
- 验证
确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
- 准备
为静态变量分配内存,并设置默认初始化值。
- 解析
将符号引用替换为直接引用。
- 初始化
初始化类变量和其他资源。
类加载器
类加载器就是读取编译器生成的字节码文件,转化生成一个java.lang.Class类的一个实例。基本上所有的类加载器都是java.lang.ClassLoader类的一个实例。
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库。
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
- 应用程序类加载器:负责加载ClassPath路径下的类包。
- 自定义加载器:负责加载用户自定义路径下的类包。
双亲委派机制
类加载器收到类加载请求,会委托给父类加载器去执行,父类加载器还存在其父类加载器,则进一步向上委托,依次递归,直到顶层类加载器,如果顶层类加载器加载到该类,就成功返回class对象,否则委托给下级类加载器去执行,依次递归。双亲委派机制是为了避免重复加载和核心类篡改。
loadClass 方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。
public abstract class ClassLoader {
private final ClassLoader parent;//父加载器
public Class<?> loadClass(String name) {
Class<?> c = findLoadedClass(name);
if( c == null ){
if (parent != null) {
//委派父加载器加载
c = parent.loadClass(name);
}else {
//查找Bootstrap加载器是否加载过
c = findBootstrapClassOrNull(name);
}
}
//调用findClass加载
if (c == null) {
c = findClass(name);
}
return c;
}
//读取class文件到内存
protected Class<?> findClass(String name){
...
return defineClass(buf, off, len);
}
// 将字节数组转成Class对象
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
如何打破双亲委派机制
自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)。
自定义加载器的使用场景
- 解决依赖冲突
- 热部署
- 加密保护
自定义加载器简单实现
- 继承ClassLoader
- 重写findClass()方法
(1)先准备一个class文件,我暂时放在D盘。
public class TestClass {
public void hello(){
System.out.println("hello class !!!");
}
}
(2)自定义ClassLoader,实现方式就是读取类文件流,然后调用defineClass()。
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
private String classpath;
public MyClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
FileInputStream fis = new FileInputStream(classpath + File.separator + name.replace(".", File.separator).concat(".class"));
byte[] bytes = new byte[fis.available()];
fis.read(bytes);
fis.close();
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
(3)测试
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, MalformedURLException {
MyClassLoader myClassLoader = new MyClassLoader("D:\");
Class<?> aClass = myClassLoader.findClass("TestClass");
Object object = aClass.newInstance();
Method hello = aClass.getDeclaredMethod("hello");
hello.invoke(object);
}
}
(4)URLClassLoader
上面自定义类加载器主要是用来读取类文件,然而可以直接使用URLClassLoader,URLClassLoader支持从jar文件和文件夹中获取class。
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, MalformedURLException {
URLClassLoader ucl = new URLClassLoader(new URL[]{new URL("file:///D:\")});
Class<?> aClass = ucl.loadClass("TestClass");
Object object = aClass.newInstance();
Method hello = aClass.getDeclaredMethod("hello");
hello.invoke(object);
}
}
加载不同版本的JDBC驱动jar包
项目需求在同一个运行环境中同时加载多个不同版本的驱动对象,来兼容多个版本的Mysql。
实现思想:通过URLClassLoader实现。
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.util.Properties;
public class MysqlTest {
public static void main(String[] args) throws Exception {
//com.mysql.jdbc.Driver >> mysql-connector-java-5.1.34.jar
//com.mysql.cj.jdbc.Driver >> mysql-connector-java-8.0.15.jar
String className = "com.mysql.jdbc.Driver";
String jarName = "mysql-connector-java-5.1.34.jar";
getConn(className, jarName);
}
private static Connection getConn(String className, String jarName) throws Exception{
String userName = "root";
String password = "root";
String url = "jdbc:mysql://localhost:3306/cla";
String jarFilePath = "jar:file:d://jar//"+jarName+"!/";
URLClassLoader loader = new URLClassLoader(new URL[]{new URL(jarFilePath)}, null);
Driver driver = (Driver) Class.forName(className, true, loader).newInstance();
Connection connection = null;
Properties info = new Properties();
info.put("user", userName);
info.put("password", password);
connection = driver.connect(url, info);
System.out.println(connection.getMetaData().getDatabaseProductVersion());
System.out.println(connection.getMetaData().getDriverVersion());
return connection;
}
}
SPI机制
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
在Java中,SPI机制有一个非常典型的实现案例,就是数据库驱动java.jdbc.Driver。Java定义了一套JDBC的接口,当没有提供具体实现类,是由不同的厂商提供数据库实现包。
目的: ① 为了解耦。② 提高框架的扩展性。
简单使用SPI
(1)定义一个接口和实现类。
package com.syun.spi;
public interface Developer {
void hello();
}
package com.syun.spi;
public class DeveloperA implements Developer{
@Override
public void hello() {
System.out.println("hello developer a");
}
}
package com.syun.spi;
public class DeveloperB implements Developer{
@Override
public void hello() {
System.out.println("hello developer b");
}
}
(2)在resources下新建META-INF/services文件夹,以接口的全限定名创建文件。
com.syun.spi.DeveloperA
com.syun.spi.DeveloperB
(3)测试。
public class Test {
public static void main(String[] args) {
ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);
serviceLoader.forEach(Developer::hello);
}
}
小问题
java.lang.SecurityException: Prohibited package name: java
由于定义了以 java 开始的包,编译时错误。可以定位到java.lang.ClassLoader.preDefineClass代码:
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
自定义加载器加载java.lang.String的问题
有一个博主尝试使用的自定义加载器加载java.lang.String过程<blog.csdn.net/li281037846…>。
- 首先重写loadClass方法后,依旧报错,从ClassLoader的源码可以看出,只要加载java开头的包就会报错。所以真正原因是JVM安全机制,并不是因为双亲委派。
- 然后修改ClassLoader类,删除preDefineClass()代码,依旧报错。
- 最后通过修改native代码中systemDictionary.cpp的resolve_from_stream()方法,重新编译JVM后成功。