类加载器
类加载器用于实现类的加载动作,负责读取java字节码,并将其转化为Class实例
类与类加载器的关系
任意一个类和加载它的类加载器一同确立其在JVM中的唯一性,并且每一个类加载器都有一个独立的类名称空间
比较两个类是否相等,前提是在同一个类加载器加载的前提下
public class ClassLoadTest {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object o = classLoader.loadClass("JVM.Class.classload.ClassLoadTest").newInstance();
System.out.println(o.getClass());
System.out.println(o instanceof JVM.Class.classload.ClassLoadTest);
}
}
结果
class JVM.Class.classload.ClassLoadTest
false
加载器分类
JVM角度
-
启动类加载器 Bootstrap ClassLoader
由C++实现
-
其他加载器
独立于虚拟机外部,都继承自抽象类ClassLoader
开发人员角度
-
启动类加载器 Bootstrap ClassLoader
Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载,比如String.class就是rt包下的)。
启动类加载器是无法被Java程序直接引用的,所以String.class.getClassLoader为null,同理如果一个类的classLoader为null那它就是被Bootstrap ClassLoader加载
简单来说就是加载 jdk的核心类库
-
拓展加载器 Extension ClassLoader
Extension ClassLoader,该加载器由
sun.misc.Launcher$ExtClassLoader
实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。 -
应用程序加载器 Application ClassLoader
也称系统类加载器(System ClassLoader)
ClassLoader.getSystemClassLoader()
方法的默认返回值Application ClassLoader,该类加载器由
sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
类加载器之间的关系
双亲委派模型
如果一个类收到类加载器的请求,它首先不会自己去尝试加载这个类,而是让父加载器去完成,依次向上,只有当父加载器在它搜索范围中没有找到所需的类时,即无法完成,子加载器才会尝试去加载该类
这种层级关系称为类加载器的双亲委派模型
双亲委派代码实现
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
//是否需要加载时去解析
if (resolve) {
resolveClass(c);
}
return c;
}
双亲委派的好处
同一个类只会被加载一次,保证java程序安全稳定的运行
查看各个加载器所以加载的东西
public static void main(String[] args) {
System.out.println("启动类加载器 Bootstrap ClassLoader");
Arrays.stream(Launcher.getBootstrapClassPath().getURLs()).forEach(url->{
System.out.println(url);
});
URLClassLoader systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
URLClassLoader extClassLoader = (URLClassLoader)systemClassLoader.getParent();
System.out.println("拓展加载器 Extension ClassLoader");
Arrays.stream(extClassLoader.getURLs()).forEach(url->{
System.out.println(url);
});
System.out.println("应用程序加载器 Application ClassLoader");
Arrays.stream(systemClassLoader.getURLs()).forEach(url->{
System.out.println(url);
});
}
输出:
启动类加载器 Bootstrap ClassLoader
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/resources.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/rt.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/sunrsasign.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/jsse.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/jce.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/charsets.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/jfr.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/classes
拓展加载器 Extension ClassLoader
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/access-bridge-32.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/localedata.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/sunec.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/zipfs.jar
应用程序加载器 Application ClassLoader
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/charsets.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/deploy.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/access-bridge-32.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/localedata.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/sunec.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/ext/zipfs.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/javaws.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/jce.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/jfr.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/jfxswt.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/jsse.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/management-agent.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/plugin.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/resources.jar
file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_202/jre/lib/rt.jar
file:/D:/study/ClassLoadTest/target/classes/
file:/D:/software/IntelliJ%20IDEA%202021.3/lib/idea_rt.jar
使用场景
我们应该何时使用类加载器呢?
1.隔离性
从Tcomcat来分析,为什么Tomcat打破双亲委派?
Tomcat容器允许同时运行多个Web程序,每个程序依赖的类的相互隔离的,如果采用双亲委派,A程序依赖hutool v1.0,B程序依赖hutool v2.0。当A程序加载完后,B程序的hutool2.0 将得不到加载。
所以Tomcat的Web程序都有属于自己的ClassLoader
2.热部署
通过Classloader可以实现动态替换类
一个简单的热部署梨子;
public interface AService {
String hello();
}
public class AServiceImpl implements AService {
@Override
public String hello() {
return "v1111";
}
}
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @program: ClassLoadTest
* @description:
* @author: TheBlindM
* @create: 2021-12-04 22:05
**/
public class HotClassLoadTest {
public static void main(String[] args) {
ThreadUtil.execute(new Monitor());
while (true) {
AService aService = ServiceFactory.getAService();
System.out.println(aService.hello());
ThreadUtil.sleep(2000);
}
}
static class ServiceFactory {
static volatile AService aService;
public static AService getAService() {
if (Objects.nonNull(aService)) {
return aService;
}
return aService = createAService();
}
private static AService createAService() {
HotClassLoad hotClassLoad = new HotClassLoad();
try {
Class<?> aClass = hotClassLoad.findClass("AServiceImpl");
if (Objects.nonNull(aClass)) {
return (AService) aClass.newInstance();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
public static void reloadAService() {
aService = createAService();
}
}
static class Monitor implements Runnable {
static final Map<String, Long> LAST_MODIFIEDS = new HashMap<>();
@Override
public void run() {
while (true) {
File resFile = FileUtil.file(this.getClass().getResource("").getPath());
Arrays.stream(resFile.listFiles()).forEach(file -> {
String key=file.getPath();
long lastModified = file.lastModified();
Long oldLastModified = LAST_MODIFIEDS.get(key);
if(Objects.isNull(oldLastModified)){
LAST_MODIFIEDS.put(file.getPath(),lastModified);
return;
}
if(!oldLastModified.equals(lastModified)){
LAST_MODIFIEDS.put(key,lastModified);
ServiceFactory.reloadAService();
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class HotClassLoad extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replaceAll("\\.", File.separator) + ".class";
// 获取类的class文件字节数组
byte[] classData = FileUtil.readBytes(path);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//直接生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
}
}
类加载方式
- JVM初始化加载
- 通过Class.forName()方法动态加载
- ClassLoader.loadClass()方法动态加载
ClassLoader.loadClass()与Class.forName()的区别
Class.forName( ):加载Class文件到jvm后,并且初始化类(比如static代码块)
ClassLoader.loadClass( ): 只加载到jvm,不会初始化类
参考:
深入理解jvm虚拟机