类加载过程
- 加载: 通过磁盘IO读取 .class 文件,将文件作为字节流读入,当使用到该类时才会加载,例如运行 main程序,new一个对象等等,加载阶段会生产一个 java.lang.Class 对象,作为方法区访问类数据的入口;
- 验证:验证字节码文件的正确性;
- 准备:给类的静态属性分配内存,并赋初始值;
- 解析: 将符号引用转化为直接应用。
符号引用: 指的是用于描述目标的符号,可以是任何字面量,只要能定位到目标即可。例如 test()是一个方法,那么test就是可以定位到tets()方法的字面量;
直接引用:直接指向目标的指针、相对偏移量或间接定位到目标的句柄。比如方法,静态变量的直接引用就是方法区的指针,方法区里的方法表含有类的所有方法的信息,通过解析符号引用可以定位到目标方法在类中方法的位置,从而使方法可以被调用。 - 初始化:执行静态方法块,未静态变量赋值。
- 使用
- 销毁
类加载器
在Java的世界里,有下面几个类加载器
- 引导类加载器(BootstrapClassLoader): 负责加载JVM运行所必须的,位于JRE的lib目录下的核心类库,如 rt.jar,charsets.jar;
- 扩展类加载器(ExtClassLoader):负责加载JVM运行所必须的,位于JRE下,lib/ext目录下的扩展类库;
- 应用程序类加载器(AppClassLoader):负责加载classpath目录下或者第三方jar包的类,主要内容是自己编写的类;
- 自定义类加载器:负责加载自定义目录下的类。
用一段程序验证:
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.nio.zipfs.ZipPath.class.getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader());
}
}
输出结果:
null
sun.misc.Launcher$ExtClassLoader@6e0be858
sun.misc.Launcher$AppClassLoader@14dad5dc
双亲委派
双薪委派可用一张图描述:
总结起来就是:
当 JVM 尝试加载一个类时,会尝试先向上委托它的父类加载器加载,它的父类加载器会再委托它的父类加载器加载,如果父类加载器能够加载,就让父类加载器加载;如果不能再让它本身路径的加载器加载,如果最底层的加载器都加载不了,就会抛出 ClassNotFoundException。
举个例子,当 ClassLoaderTest 要加载 String.class时,向上委托到 BootstrapClassLoader 时已经发现加载过了,就让BootstrapClassLoader 加载。但是加载 ClassLoaderTest 时在父类加载器都没有找到,就让 AppClassLoader 加载。
双亲委派源码:
public abstract class ClassLoader {
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 {
c = findBootstrapClassOrNull(name); //父加载器为空,那么委托BootstrapClassLoader加载
}
} 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); //如果父加载加载不到,也就是用户编写的类都会走到这里,调用URLClassLoader.findClass()
// 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;
}
}
}
为什么要设计双亲委派?
1. 安全: 防止核心类库被人篡改,如果用户在自己的目录下也定义了一个 java.lang.String,JVM 是不会加载这个类的;
做两个简单的验证:
package java.lang;
public class String {
public static void main(String[] args) {}
}
输出:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
=============================================================================================================
package java.lang;
public class UserDefin{
public static void main(String[] args) {}
}
输出:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"
2. 避免类的重复加载。
全盘委托机制
指一个类加载器尝试加载一个类时,如果没有显式的指明另一个类加载器,那么这个类以及它的依赖都由这个类加载加载。
自定义类加载器
首先需要将一个 .class 文件拷贝到项目外的任意一个目录。
public class MyClassLoader extends ClassLoader {
String path;
public MyClassLoader(String filePath) {
this.path = filePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getData();
return defineClass(name, classData, 0, classData.length);
}
private byte[] getData() {
File file = new File(path);
if (file.exists()) {
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
} else {
return null;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("E:/temp/Cat.class");
// 将 Cat 交给类加载器加载
Class catClass = myClassLoader.loadClass("Cat");
Method method = catClass.getDeclaredMethod("toString",null);
Object obj = catClass.newInstance();
System.out.println(method.invoke(obj,null));
System.out.println(catClass.getClassLoader().getClass().getName());
}
}
运行结果:
Hi, this is greeting from Cat.class
com.classloader.MyClassLoader
打破双亲委派
按照前面对 ClassLoader.loadClass() 的分析,打破双亲委派很简单,定义自定义类加载器,重写 loadClass(),不调用父类的 loadClass() 即可。
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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;
}
}
}
这样一来,所有的 Java 类都由自定义类加载器加载了。但是并不能运行,会出现错误:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
可以看出,即使使用自定义类加载器,也没办法篡改Java的核心类库。
打破双亲委派的好处
在某些场合下,打破双亲委派是很有用处的。
拿 tomcat 举例。Tomcat 如果使用默认的双亲委派,会存在下面几个问题:
- Tomcat可能需要同时部署多个应用程序,每个应用程序依赖的类库虽然相同,但是版本很可能不同,如果要求同一个类库在每个容器之间都是相同的,那么就会出现问题,必须保持容器之间的相互隔离;
- Tomcat也有自己依赖的类库,不能将自己的类库与应用程序的类库混淆;
- 需要支持 jsp 的修改,满足启动应用程序后,修改 jsp 不需要重启。jsp 本质上也是一个 .class 文件,在编译后,无论怎么改变,类加载器还是会直接加载方法区内的信息。
所有Tomcat需要打破双亲委派,但也要保证在 Tomcat 内部署的版本相同的类库可以共享,不然如果需要运行100个应用程序,就会有多个相同的类库信息加载到 JVM 中