一 类加载运行过程
通过java命令运行某个类的main函数来启动程序时,首先需要通过类加载器将主类加载到JVM中;
源码:
java
package com.ddu.jvm;
public class HelloWordHelper {
public static void main(String[] args) {
User user = new User();
user.setAge(1);
user.setName("1岁");
add(1, 2);
}
private static int add(int a, int b) {
return a + b;
}
static class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
通过Java命令执行代码的大体过程如下:
其中loadClass的类加载过程有如下几步:
加载=>验证=>准备=>解析=>初始化=>使用=>卸载
- **加载:**在本地硬盘或者网络资源上,通过IO读取字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
- **验证:**验证字节码的正确性;
- **准备:**给类的静态变量分配内存,并赋予默认值;
- 解析: 将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换指向数据锁在内存的指针或句柄等(即直接引用),这就是所谓的静态链接 (类加载期间完成),动态链接是在程序运行期间完成的,将符号引用替换为直接引用;
- **初始化:**针对类的静态变量初始化为指定的值,执行静态代码块;
类被加载到方法区中后,主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载的引用、对应class实例的引用等信息;
- **类加载器的引用:**这个类到类加载器实例的引用;
- **对应class实例的应用:**类加载器在加载类信息放到方法区中后,会创建一个对应的Class类型的对象实例放到堆(Heap)中,作为开发人员访问方法区中类定义的入口和切入点;
其中,主类在运行过程中如果需要使用到其他类,会逐步加载这些类;即jar包或war包里的类不是一次性全部加载的,是使用到时才会加载;
源码:
java
package com.ddu.jvm;
public class HelloWordHelper {
static {
System.out.println("load HelloWordHelper...");
}
public static void main(String[] args) {
// User user = new User();
// user.setAge(1);
// user.setName("1岁");
// add(1, 2);
new Order();
System.out.println("load HelloWordHelper");
// Address不会加载,除非这里执行new Address();
Address address = null;
}
private static int add(int a, int b) {
return a + b;
}
static class Order{
static {
System.out.println("loader Order ......");
}
public Order() {
System.out.println("initial Order ... ");
}
}
static class Address{
static {
System.out.println("loader Address ......");
}
public Address() {
System.out.println("initial Address ... ");
}
}
static class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
运行结果:
java
load HelloWordHelper...
loader Order ......
initial Order ...
load HelloWordHelper
二 类加载器和双亲委派机制
上面的类加载过程主要是通过类加载来实现的,Java中有如下几种类加载器:
- 引导类加载器:负责加载支撑JVM运行的位于JRE安装目录lib目录下的核心类库,比如rt.jar、charsets.jar
- 扩展类加载器:负载加载支持JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR包;
- 应用程序类加载器:负载加载ClassPath路径下的类包,主要是加载开发人员自己开发的类;
- 自定义类加载器:负责加载用户自定义路径下的类包;
类加载器示例:
java
package com.ddu.jvm;
import sun.misc.Launcher;
import java.net.URL;
public class DduClassLoaderHelper {
public static void main(String[] args) {
// 预期是null,因为String是引导类加载器加载的,而引导类加载器是C++实现的,JVM内存中没有C++的实例
System.out.println(String.class.getClassLoader());
// DESKeyFactory类是JRE/lib/ext包下的,类加载器为ExtClassLoader
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
// 业务自己定义的类,类加载器为AppClassLoader
System.out.println(DduClassLoaderHelper.class.getClassLoader().getClass().getName());
System.out.println("---------------------------------");
// 系统类加载器,即为AppClassLoader
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// 系统类加载器的父加载器为扩展类加载器=>ExtClassLoader
ClassLoader extClassLoader = systemClassLoader.getParent();
// 扩展类加载器的父类加载器为系引导类加载器=>BootstrapClassLoader
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(systemClassLoader);
System.out.println(extClassLoader);
System.out.println(bootStrapClassLoader);
System.out.println("--------BootstrapClassLoader加载如下的class文件-----------------------");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL);
}
System.out.println("----------ExtClassLoader加载如下目录的class文件---------------------");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("----------AppClassLoader加载如下目录的class文件---------------------");
System.out.println(System.getProperty("java.class.path"));
}
}
运行结果:
java
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
---------------------------------
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@506e1b77
null
--------BootstrapClassLoader加载如下的class文件-----------------------
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_333/jre/classes
----------ExtClassLoader加载如下目录的class文件---------------------
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
----------AppClassLoader加载如下目录的class文件---------------------
C:\Program Files\Java\jdk1.8.0_333\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_333\jre\lib\rt.jar;D:\project\ddu-base\ddu-base\ddu-java\target\classes;D:\repository\org\springframework\boot\spring-boot-starter\2.1.4.RELEASE\spring-boot-starter-2.1.4.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot\2.1.4.RELEASE\spring-boot-2.1.4.RELEASE.jar;D:\repository\org\springframework\spring-context\5.1.6.RELEASE\spring-context-5.1.6.RELEASE.jar;D:\repository\org\springframework\spring-aop\5.1.6.RELEASE\spring-aop-5.1.6.RELEASE.jar;D:\repository\org\springframework\spring-beans\5.1.6.RELEASE\spring-beans-5.1.6.RELEASE.jar;D:\repository\org\springframework\spring-expression\5.1.6.RELEASE\spring-expression-5.1.6.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot-autoconfigure\2.1.4.RELEASE\spring-boot-autoconfigure-2.1.4.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot-starter-logging\2.1.4.RELEASE\spring-boot-starter-logging-2.1.4.RELEASE.jar;D:\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\repository\org\apache\logging\log4j\log4j-to-slf4j\2.11.2\log4j-to-slf4j-2.11.2.jar;D:\repository\org\apache\logging\log4j\log4j-api\2.11.2\log4j-api-2.11.2.jar;D:\repository\org\slf4j\jul-to-slf4j\1.7.26\jul-to-slf4j-1.7.26.jar;D:\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;D:\repository\org\springframework\spring-core\5.1.6.RELEASE\spring-core-5.1.6.RELEASE.jar;D:\repository\org\springframework\spring-jcl\5.1.6.RELEASE\spring-jcl-5.1.6.RELEASE.jar;D:\repository\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;D:\repository\org\springframework\boot\spring-boot-starter-test\2.1.4.RELEASE\spring-boot-starter-test-2.1.4.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot-test\2.1.4.RELEASE\spring-boot-test-2.1.4.RELEASE.jar;D:\repository\org\springframework\boot\spring-boot-test-autoconfigure\2.1.4.RELEASE\spring-boot-test-autoconfigure-2.1.4.RELEASE.jar;D:\repository\com\jayway\jsonpath\json-path\2.4.0\json-path-2.4.0.jar;D:\repository\net\minidev\json-smart\2.3\json-smart-2.3.jar;D:\repository\net\minidev\accessors-smart\1.2\accessors-smart-1.2.jar;D:\repository\org\ow2\asm\asm\5.0.4\asm-5.0.4.jar;D:\repository\org\slf4j\slf4j-api\1.7.26\slf4j-api-1.7.26.jar;D:\repository\junit\junit\4.12\junit-4.12.jar;D:\repository\org\assertj\assertj-core\3.11.1\assertj-core-3.11.1.jar;D:\repository\org\mockito\mockito-core\2.23.4\mockito-core-2.23.4.jar;D:\repository\net\bytebuddy\byte-buddy\1.9.12\byte-buddy-1.9.12.jar;D:\repository\net\bytebuddy\byte-buddy-agent\1.9.12\byte-buddy-agent-1.9.12.jar;D:\repository\org\objenesis\objenesis\2.6\objenesis-2.6.jar;D:\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar;D:\repository\org\hamcrest\hamcrest-library\1.3\hamcrest-library-1.3.jar;D:\repository\org\skyscreamer\jsonassert\1.5.0\jsonassert-1.5.0.jar;D:\repository\com\vaadin\external\google\android-json\0.0.20131108.vaadin1\android-json-0.0.20131108.vaadin1.jar;D:\repository\org\springframework\spring-test\5.1.6.RELEASE\spring-test-5.1.6.RELEASE.jar;D:\repository\org\xmlunit\xmlunit-core\2.6.2\xmlunit-core-2.6.2.jar;D:\repository\org\projectlombok\lombok\1.18.22\lombok-1.18.22.jar;D:\soft\Intelijidea\lib\idea_rt.jar
2.1 类加载器初始化过程
类运行加载过程中,会创建JVM启动器实例sun.misc.Launcher,在Launcher构造方法内部,创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器),JVM默认使用Launcher的getClassLoader()方法返回的类加载器(即为AppClassLoader)加载我们的应用程序;
以下是sun.misc.Launcher部分源码:
java
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
ExtClassLoader var1;
try {
// 扩展类加载器,在构造当前类加载器时,将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 构造应用类加载器,在构造过程中,将器父类加载器设置为ExtClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
省略其他代码
public ClassLoader getClassLoader() {
return this.loader;
}
Launcher.ExtClassLoader.getExtClassLoader()实现:
java
static class ExtClassLoader extends URLClassLoader {
private static volatile ExtClassLoader instance;
public static ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = ExtClassLoader.class;
synchronized(ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
private static ExtClassLoader createExtClassLoader() throws IOException {
try {
return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
return new ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
void addExtURL(URL var1) {
super.addURL(var1);
}
public ExtClassLoader(File[] var1) throws IOException {
// 此处设置当前类加载器的父类加载器为null
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
Launcher.AppClassLoader.getAppClassLoader(extClassLoader)实现源码
java
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new AppClassLoader(var1x, var0);
}
});
}
AppClassLoader(URL[] var1, ClassLoader var2) {
// 此处var2即为设置父类加载器
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
省略其他代码
2.2 双亲委派机制
JVM类加载是有亲子层级结构的,如下图 :
类加载其实就是一个双亲委派机制,加载某个类时,会先委托给父类加载器寻找加载目标类,父类加载器会继续委托给自己的父类加载器,直到当前类加载器的父类加载器为null(实际是C++实现的bootstapClassLoader)时,开始尝试加载当前类,如果所有的父类加载器都无法加载到当前类,则在自己的类加载路径加载当前类;
例如:HelloWordHelper类,
- 逐层委托:首先会找应用类加载器加载,应用类加载器则会先委托给扩展类加载器加载,扩展类加载器会先委托给引导类加载器;
- 自顶向下开始尝试加载目标类:
- BootstapClassLoader加载不到HelloWordHelper,则退回至ExtClassLoader加载目标类;
- ExtClassLoader加载不到目标类HelloWordHelper,则退回至AppClassLoader加载器尝试加载目标类;
- AppClassLoader在class.path目录下加载到目标类,完成目标类加载;
简单点说:双亲委派机制就是儿子啃老,有事情先找老爹,老爹有事情先找爷爷,爷爷办不到老爹自己想办法,老爹实在办不到就儿子自己想办法了;
2.3 AppClassLoader源码是如何完成双亲委派机制的?
源码:
2.3.1 AppclassLoader实现了loadClass方法
最终会调用ClassLoader.loadClass方法
java
static class AppClassLoader extends URLClassLoader {
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
// ClassLoader中实现了双亲委派机制
return super.loadClass(var1, var2);
}
}
}
2.3.2 ClassLoader.loadClass方法实现双亲委派机制
java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 当前类加载器存在父类加载器,则委托给父类加载器加载目标类
if (parent != null) {
c = parent.loadClass(name, false);
// 如果当前类没有父类加载器,则委托给BootstapClassLoader尝试加载,最终调用native方法
} 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);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2.4 为什么要设计双亲委派机制?
- 沙箱安全机制:研发人员自己开发的java.lang.String.class类不会被加载,防止核心API库被随意修改;
- 避免类的重复加载:当父类已经加载过该类时,就没必要子ClassLoader再加载一次,保证被加载类的唯一性;
- 被加载类唯一性:加载器类+目标类
示例:
java
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
2.5 委托机制的传播性
当一个ClassLoader加载一个类时,除非显示的使用另外一个ClassLoader,否则该类所依赖及引用的类也由这个ClassLoader载入;
2.6 自定义类加载器
自定义类加载器只需要加成java.lang.ClassLoader类,该类有两个核心方法:
- loadClass(String,boolean):实现了双亲委派机制;
- findClass:默认是空方法,自己类加载器时,主要是重写findClass方法;
示例:
java
package com.ddu.jvm;
import java.io.FileInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* 自定义类加载器
*/
public class DduClassLoaderHelper {
public static void main(String[] args) {
// d://uclazz/com/ddu/jvm/Order1.class
DduClassLoader dduClassLoader = new DduClassLoader("D:/uclazz");
try {
Class<?> clazz = dduClassLoader.loadClass("com.ddu.jvm.Order1");
Object order = clazz.newInstance();
Method method = clazz.getDeclaredMethod("print", null);
method.invoke(order, null);
System.out.println(clazz.getClassLoader().getClass().getName());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
static class DduClassLoader extends ClassLoader {
private String classPath;
public DduClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = null;
byte[] data = null;
try {
// d://uclazz/com/ddu/jvm/Order1.class
fis = new FileInputStream(classPath + "/" + name + ".class");
int length = fis.available();
data = new byte[length];
fis.read(data);
} catch (Exception e) {
throw new RuntimeException("加载class文件异常!");
} finally {
if (Objects.nonNull(fis)) {
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 RuntimeException(e);
}
}
}
}
Order1源码:
放置目录:D:\uclazz\com\ddu\jvm
自己javac Order1.java
java
package com.ddu.jvm;
import java.io.File;
public class Order1 {
public void print(){
File file = new File("");
System.out.println("d: uclazz order load path:"+file.getAbsoluteFile());
}
}
运行结果:
java
d: uclazz order load path:D:\project\ddu-base\ddu-base
com.ddu.jvm.DduClassLoaderHelper$DduClassLoader
2.7 tomcat是如何打破双亲委派机制的?
2.7.1 tomcat作为web容器,需要解决什么问题?
- 一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个三方包的不同版本,因此需要保证每个应用程序的依赖类库都是独立的,保证相互隔离;
- 部署在同一个web容器中的相同类库相同的版本可以共享,否则如果一个web容器下部署了n个应用程序,那么要用n份相同的依赖类库加载到虚拟机;
- web容器本身也需要依赖三方类库,容器依赖的三方类库与应用程序的依赖三方类库不能混淆,基于安全考虑,应该让容器的类库和程序的类库隔离开来;
- web容器要支持jsp的修改,而jsp文件最终是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是很常见的事情,web容器需要支持jsp修改后不重启就可以生效;也就是大家常说的:jsp热加载;
2.7.2 tomcat如果使用默认的双亲委派加载机制行不行?
- 答案:不行;
- 原因:
- 问题1,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,同一个类全限定名同一个类加载器仅加载一次;
- 问题2,默认类加载器是可以实现的,因为它的职责就是保证唯一性;
- 问题3和问题1是一样的问题;
- 问题4,如何实现jsp的热加载?
- jsp文件最终都会转成class文件,虽然内容发生了修改,但是类名还是一样的,类加载器会直接取方法区中已经存在的类信息,修改后的jsp是不会重新加载的,所以应该如何实现呢?思路:卸载掉当前jsp文件的类加载器,也就是说每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载,重新创建类加载器,重新加载jsp文件;
2.7.3 tomcat自定义类加载器详解
tomcat几个主要的类加载器:
- CommonLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat容器本身以及各个Webapp程序访问;
- CatalinaLoader:tomcat容器私有的类加载器,加载路径中的class对Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于tomcat容器不可见;
- WebappClassLoader:各个WebApp私有的类加载器,加载路径中的class仅对当前webapp可见;
从上图的委派关系可以看出,
- CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公共类库的共用;
- CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方互相隔离;
- WebappClassLoader可以使用SharedClassLoader加载到的类,但是各个WebappClassLoader实例之间互相隔离;
- JasperLoader加载范围仅仅是这个JSP文件所编译出来的那一个class文件,当Web容器检测到JSP文件被修改了,会替换掉目前的JasperLoader实例,并通过重新加载一个新的Jsp类加载器来实现JSP文件的热加载功能;
tomcat这种类加载机制是否违背了java推荐的双亲委派模型?
- 答案是违背了。因为tomcat为了实现隔离性,没有遵循双亲委派机制,每个WebappClassLoader加载自己目录下的class文件,不会向上委托给父类加载器,因此是打破了双亲委派机制;
2.7.4 tomcat打破双亲委派的简单实现
继承ClassLoader类,重写protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException方法,加载指定类时不再向上委托给父类加载器;
代码:
java
package com.ddu.jvm;
import java.io.FileInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* 自定义类加载器
*/
public class DduClassLoaderHelper {
public static void main(String[] args) {
// d://uclazz/com/ddu/jvm/Order1.class
DduClassLoader dduClassLoader2 = new DduClassLoader("D:/uclazz");
try {
Class<?> clazz2 = dduClassLoader2.loadClass("com.ddu.jvm.Order2");
Object order2 = clazz2.newInstance();
Method method1 = clazz2.getDeclaredMethod("print", null);
method1.invoke(order2, null);
System.out.println(clazz2.getClassLoader().getClass().getName());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
// d://uclazz/com/ddu/jvm/Order1.class
DduClassLoader dduClassLoader = new DduClassLoader("D:/uclazz");
try {
Class<?> clazz = dduClassLoader.loadClass("com.ddu.jvm.Order1");
Object order = clazz.newInstance();
Method method = clazz.getDeclaredMethod("print", null);
method.invoke(order, null);
System.out.println(clazz.getClassLoader().getClass().getName());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
static class DduClassLoader extends ClassLoader {
private String classPath;
public DduClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = null;
byte[] data = null;
try {
// d://uclazz/com/ddu/jvm/Order1.class
fis = new FileInputStream(classPath + "/" + name + ".class");
int length = fis.available();
data = new byte[length];
fis.read(data);
} catch (Exception e) {
throw new RuntimeException("加载class文件异常!");
} finally {
if (Objects.nonNull(fis)) {
fis.close();
}
}
return data;
}
/**
* 如果只是为了自定义类加载器,直接重写当前方法就可以
*
* @param name The <a href="#name">binary name</a> of the class
* @return
* @throws ClassNotFoundException
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 自定义的类不再委托给父类加载
if(name.startsWith("com.ddu.jvm")){
c = findClass(name);
// 非自定义的类还是按双亲委派机制加载
}else {
c = this.getParent().loadClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
}
运行结果:
java
2 d: uclazz order load path:D:\project\ddu-base\ddu-base
com.ddu.jvm.DduClassLoaderHelper$DduClassLoader
d: uclazz order load path:D:\project\ddu-base\ddu-base
com.ddu.jvm.DduClassLoaderHelper$DduClassLoader
Order源码:
两个类自己修改打印日志的内容然后重新编译就可以使用了
java
package com.ddu.jvm;
import java.io.File;
public class Order {
public void print(){
File file = new File("");
System.out.println("project ddu order load path:"+file.getAbsoluteFile());
}
}
2.7.5 tomcat的JasperLoader热加载简单实现
原理:后台启动线程监听JSP文件变化,如果文件内容发生了变化,找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器赋值给引用,然后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot,下次gc的时候会被垃圾回收;
具体tomcat实现的方式参考:Tomcat热部署与热加载_smarttomcat开启热部署-CSDN博客