类加载过程
将java文件打成jar包或者war包之后,由某个主类的main函数进行启动,需要通过类加载器把主类加载到JVM,jar包里的类不是一次性加载,是在使用到的时候才会加载。
类加载步骤
- 加载:在硬盘上查找并通过IO读取字节码文件
- 验证:验证字节码文件正确性
- 准备:给类的静态变量分配内存,并赋默认值。例如:int=0、boolean=false、对象=null
- 解析:将静态方法符号引用替换为直接引用——静态链接;程序运行期间完成符号引用替换为直接引用——动态链接
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
类加载器
引导类加载器(启动类加载器)——BootstrapClassLoader
由C++实现,嵌入在JVM内部,负责加载JRE的lib目录下的核心类库,比如rt,jar、charsets.jar等
扩展类加载器——ExtClassLoader
由Java代码实现,在JVM启动器实例(sun.misc.Lanucher)初始化时创建,是负责加载JRE的lib目录下ext的jar包
应用程序类加载器——AppClassLoader
由Java代码实现,在JVM启动器实例(sun.misc.Lanucher)初始化时创建,负责加载ClassPath路径下的类包,主要就是加载自己写的类
自定义加载器
负责加载自定义路径下的类包
类加载器初始化过程
底层C++代码调用Java代码创建JVM启动器实例sum.misc.Launcher,Launcher初始化使用了单例模式,保证JVM虚拟机只有一个sum.misc.Launcher实例。
在Launcher构造方法内部创建了两个类加载器,一个是扩展类加载器,一个是应用类加载器。JVM默认使用getAppClassLoader()方法返回的类加载器来加载我们的应用程序。
示例代码如下:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//构造扩展类加载器ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构造应用类加载器AppClassLoader,并设置父类加载器为ExtClassLoader,设置Launcher的loader属性为AppClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
}
双亲委派机制
下面我们来看下应用类加载器AppClassLoader加载类的双亲委派机制源码,源码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先判断下类是否被加载过,如果被加载过直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//判断父类加载器是否为空
if (parent != null) {
//若父类加载器不为空,则由父类加载器加载
c = parent.loadClass(name, false);
} else {
//若父类加载器为空,则由引导类加载器BootstrapClassLoader加载()
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果仍未找到,则调用当前类加载器的findClass方法继续查找
long t1 = System.nanoTime();
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;
}
}
结合源码双亲委派大致步骤如下:
- 首先先判断类是否被加载过,如果被加载过直接返回。
- 如果没有被加载过,如果有父类加载器则有父类加载,如果没有父类加载器则有JVM内置的引导类加载器进行加载。
- 如果父类加载器及引导类加载器(BootstrapClassLoader)都未查找到指定类,则由调用当前的类加载器的findClass方法自行加载。
为什么要设计双亲委派机制
- 沙箱安全机制:防止核心类库被篡改
- 避免类的重复加载:父类已经加载了该类,没必要子类再加载一次,保证被加载类的唯一性
自定义类加载器
自定义加载器只需要集成ClassLoader类,这个类有两个核心方法,一个是loadClass方法实现了双亲委派机制、一个是findClass方法默认,自定义类加载器主要就是重写findClass方法。
示例代码如下:
public class MyClassLoaderTest {
@AllArgsConstructor
@Data
static class MyClassLoader extends ClassLoader{
private String classPath;
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
int length = inputStream.available();
byte[] data = new byte[length];
inputStream.read(data);
inputStream.close();
return data;
}
}
public static void main(String[] args) throws Exception {
//自定义类加载器加载路径
MyClassLoader myClassLoader = new MyClassLoader("/Users/yyyz/env/jvmtest");
//类加载器加载指定类,将指定加载类的class文件放在指定的加载路径中
Class clazz = myClassLoader.loadClass("com.demo.zsydemo.jvm.Test");
Object object = clazz.newInstance();
Method method = object.getClass().getMethod("sout");
method.invoke(object,null);
//输出当前类加载器是否是自定义的类加载器
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
public class Test {
public static void sout(){
System.out.println("这是自定义类加载器加载的");
}
}
运行结果:
这是自定义类加载器加载的
com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader
如何打破双亲委派
类加载器中实现双亲委派的方法是loadClass方法,我们只需在掐面写的自定义类加载器中重写一下loadClass方法,干掉原生loadClass方法中的向上委托的地方即可打破双亲委派机制。下面是尝试打破双亲委派机制,用自定义类加载器加载自己实现的java.lang.String.class的示例代码如下:
public class MyClassLoaderTest {
@AllArgsConstructor
@Data
static class MyClassLoader extends ClassLoader {
private String classPath;
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);
// } 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();
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;
}
}
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
int length = inputStream.available();
byte[] data = new byte[length];
inputStream.read(data);
inputStream.close();
return data;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("/Users/yyyz/env/jvmtest");
Class clazz = myClassLoader.loadClass("java.lang.String");
Object object = clazz.newInstance();
Method method = object.getClass().getMethod("replace");
method.invoke(object, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:66)
at com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:48)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at com.demo.zsydemo.jvm.MyClassLoaderTest.main(MyClassLoaderTest.java:86)
Exception in thread "main" java.lang.ClassNotFoundException
at com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:69)
at com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:48)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at com.demo.zsydemo.jvm.MyClassLoaderTest.main(MyClassLoaderTest.java:86)
这个异常是返回的安全异常,禁止加载java.lang包,这个就是前面讲的沙箱安全机制,这个机制可以防止核心类库被篡改。
打破双亲委派的场景
Tomcat容器如何打破双亲委派机制
tomcat是一个web容器,它需要解决什么问题:
- 一个web容器可能需要部署多个应用,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个类库都是独立的,保证相互隔离
- 部署在同一个web容器中,相同的类库的相同版本可以共享
- web容器也有自己依赖的类库,不能需应用程序的类库混淆,需要将容器类库与应用程序类库保持隔离
- web容器需要支持jsp的修改,需要支持修改jsp后不重启
基于上面四个问题,我们可以看下tomcat为什么要打破双亲委派
第一个问题,如果使用默认的类加载机制,是不支持加载相同类库不同版本的,默认类加载器需要确保被加载类的唯一性
第二个问题,默认类加载器可以实现,默认类加载器的职责就是唯一的
第三个问题和第一个问题一样
第四个问题,如果要实现jsp文件的热加载,jsp文件其实也就是class文件,如果文件修改,类名称不变,默认类加载器还是会去直接取方法区中已经存在的,修改后的jsp文件是不会被重新加载的,每个jsp文件都会对应一个jsp类加载器,如果jsp被修改了,就会直接卸载这个jsp类加载器,重新创建类加载器,重新加载jsp文件
Tomcat主要类加载器:
- CommonClassLoader:Tomcat最基本的类加载器,加载路径的class对web容器和各个webapp都可见。
- CatalinaClassLoader:web容器私有的类加载器,加载路径的class只对web容器可见。
- SharedClassLoader:各个webapp共享的类加载器,加载路径的class对所有webapp可见,对web容器不可见。
- WebappClassLoader:各个webapp的私有类加载器,加载路径的class只对当前webapp可见。
通过上图的委派关系中可以看出:
- CommonClassLoader加载的类也都能被CatalinaClassLoader和SharedClassLoader使用,实现了公有库公用的问题。
- CatalinaClassLoader和SharedClassLoader自己能加载的类对对方相互隔离。
- WebappClassLoader可以使用SharedClassLoader加载的类,每个war包都有自己对应的WebappClassLoader,各个实例之间可以实现相互隔离。
- JaspLoader加载范围仅仅是这个jsp所编译出来的那一个class文件,出现的目的就是为了被丢弃,每当jsp文件被修改,会替换到当前JaspLoader的实例,并通过在新建一个新的jsp类加载器实现jsp的热加载功能。
tomcat这种为了实现隔离性,WebappClassLoader加载自己目录下的class文件,不会传递给父类加载器加载,打破了双亲委派机制。
下面是模拟tomcat实现WebappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离,也是基于上面的打破双亲委派的自定义类加载器实现的。
代码示例如下:
public class MyClassLoaderTest {
@AllArgsConstructor
@Data
static class MyClassLoader extends ClassLoader {
private String classPath;
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);
// } 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();
if (!name.startsWith("com.demo.zsydemo.jvm")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
int length = inputStream.available();
byte[] data = new byte[length];
inputStream.read(data);
inputStream.close();
return data;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("/Users/yyyz/env/jvmtest");
Class clazz = myClassLoader.loadClass("com.demo.zsydemo.jvm.TomcatTest");
Object object = clazz.newInstance();
Method method = object.getClass().getMethod("sout");
method.invoke(object, null);
System.out.println(clazz.getClassLoader().getClass().getName());
System.out.println("--------------分割线--------------");
MyClassLoader myClassLoader1= new MyClassLoader("/Users/yyyz/env/jvmtest01");
Class clazz1 = myClassLoader1.loadClass("com.demo.zsydemo.jvm.TomcatTest");
Object object1 = clazz1.newInstance();
Method method1 = object1.getClass().getMethod("sout");
method1.invoke(object1, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
运行结果:
模拟tomcat测试类——TomcatTest
com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader
--------------分割线--------------
另外一个模拟tomcat测试类——TomcatTest
com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader
同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可能是不一样,所以看两个对象是否是同一个,除了看类名和包名是否是同一个,还要看类加载器是否是同一个。