系列索引
Arthas从入门到精通(二) 理解的前提-ClassLoad机制剖析
Arthas从入门到精通(三) 理解的前提-ClassLoad机制应用
背景
- 前面已经分析过java的classLoader模型以及双亲委派机制的实现。
- 这里主要分析一下如何应用classLoader的简单应用,以及在一些主流框架上的实践分析。
自定义CLassLoader
可以注意到,之前分析的ClassLoader都是从某个目录或者jar包里,加载的class文件,如果需要本地硬盘或者从网络上动态加载一个class文件,就需要自己自定义一个ClassLoader实现。
三个步骤:
- 编写一个类继承自ClassLoader抽象类。
- 一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
- 复写它的findClass()方法。
- 在loadClass中最后调用
- 在findClass()方法中调用defineClass()。
-
这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。
-
测试类Test.java
package com.destiny;
public class Test {
public void say() {
System.out.println("hello world");
}
}
对这个文件执行javac之后生成Test.class文件,放到~/class 目录下
DiskClassLoader.java
在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。
public class DiskClassLoader extends ClassLoader {
private String mLibPath;
public DiskClassLoader(String path) {
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getFileName(name);
File file = new File(mLibPath, fileName);
// 读取file的二进制流
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if (index == -1) {
return name + ".class";
} else {
return name.substring(index + 1) + ".class";
}
}
}
ClassLoaderTest
最终能正确动过Test.class文件,通过反射执行到say方法。
public class ClassLoaderTest {
public static void main(String[] args) {
//创建自定义classloader对象。
DiskClassLoader diskLoader = new DiskClassLoader("~/class/");
try {
//加载class文件
Class c = diskLoader.loadClass("com.destiny.Test");
if (c != null) {
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say", null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
对一个自定义的ClassLoader来说,最重要的是路径,即从哪里加载class文件,可以是磁盘, 网络,内存等。
自定义ClassLoader的一些妙用:
- class文件加密,对.class执行一些加密操作,在findClass处理二进制的时候,加载回来,可以完成对.class文件的加密
Context ClassLoader 线程上下文类加载器
在thread中,可以设置当前线程自己的context ClassLoader,它只是一个线程私有的概念,并不和之前介绍的3个类加载器一样是一个具体的实现。
每个Thread都有一个相关联的ClassLoader,默认是AppClassLoader。并且子线程默认使用父线程的ClassLoader除非子线程特别设置。
Thread相关源码如下:
public class Thread implements Runnable {
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
}
下面演示如何利用线程的context classLoader 分别加载同包名,同类名的class,一般我们在同一个classLoader下,因为双亲委派模型的存在,是不可能实现的。
接口:
package com.destiny.test;
public interface ISpeak {
public void speak();
}
实现类1:
package com.destiny.test;
// 生成.class文件后在 /user/destiny/Documents/speak/test/ 下
public class SpeakTest implements ISpeak {
@Override
public void speak() {
System.out.println("Test");
}
}
实现类2:
package com.destiny.test;
// 生成.class文件后在 /user/destiny/Documents/speak/ 下
public class SpeakTest implements ISpeak {
@Override
public void speak() {
System.out.println("I\' destiny");
}
}
在一个jvm中同时加载实现类1和实现类2:
public class ClassLoaderTest {
public static void main(String[] args) {
DiskClassLoader diskLoader1 = new DiskClassLoader("/Users/destiny/Documents/speak/test/");
Class cls1 = null;
try {
//加载class文件
cls1 = diskLoader1.loadClass("com.destiny.test.SpeakTest");
System.out.println(cls1.getClassLoader().toString());
if (cls1 != null) {
try {
Object obj = cls1.newInstance();
Method method = cls1.getDeclaredMethod("speak", null);
//通过反射调用Test类的speak方法
method.invoke(obj, null);
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("----------------------next thread--------------------------------------------");
DiskClassLoader diskLoader = new DiskClassLoader("/Users/destiny/Documents/speak/");
System.out.println("Thread " + Thread.currentThread().getName() + " classloader: " + Thread.currentThread().getContextClassLoader().toString());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getName() + " classloader: " + Thread.currentThread().getContextClassLoader().toString());
try {
//方法1: 用新构造的classLoader加载class文件, 输出I\' destiny
Thread.currentThread().setContextClassLoader(diskLoader);
Class c = diskLoader.loadClass("com.destiny.test.SpeakTest");
//方法2: 直接用线程原来的上下文加载器, AppClassLoader,会直接找不到SpeakTest
//ClassLoader cl = Thread.currentThread().getContextClassLoader();
//Class c = cl.loadClass("com.destiny.test.SpeakTest");
System.out.println(c.getClassLoader().toString());
if (c != null) {
try {
Object obj = c.newInstance();
//SpeakTest1 speak = (SpeakTest1) obj;
//speak.speak();
Method method = c.getDeclaredMethod("speak", null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}).start();
}
}
可以看到DiskClassLoader1和DiskClassLoader分别加载了自己路径下的SpeakTest.class文件,并且它们的类名是一样的com.frank.test.SpeakTest
,但是执行结果不一样,因为它们的实际class文件内容不一样。
Tomcat的ClassLoader机制简析
tomcat需要解决的问题:
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
- 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
- web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。
tomcat加载设计:
我们看到,前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*
、/server/*
、/shared/*
(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*
中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
Tomcat的类加载:
当tomcat启动时,会创建几种类加载器:
Bootstrap 引导类加载器
- 加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
System 系统类加载器
- 加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
Common 通用类加载器
- 加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar
webapp 应用类加载器
- 每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
当应用需要到某个类时,则会按照下面的顺序进行类加载:
- 使用bootstrap引导类加载器加载
- 使用system系统类加载器加载
- 使用应用类加载器在WEB-INF/classes中加载
- 使用应用类加载器在WEB-INF/lib中加载
- 使用common类加载器在CATALINA_HOME/lib中加载
Tomcat7逻辑关系图
具体实现逻辑见: Bootstrap.initClassLoaders()
tomcat的类加载机制是违反了双亲委托原则的, 具体实现见WebAppClassLoaderBase.loadClass(),主要流程如下:
- 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在
resourceEntries
这个数据结构中),如果已经加载即返回,否则 继续下一步。 - 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
- 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
- 最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。
// 来自WebappClassLoaderBase.loadClass
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
// (0) Check our previously loaded local class cache
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (0.1) Check our previously loaded class cache
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// Use getResource as it won't trigger an expensive
// ClassNotFoundException if the resource is not available from
// the Java SE class loader. However (see
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
// details) when running under a security manager in rare cases
// this call may trigger a ClassCircularityError.
tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null);
} catch (ClassCircularityError cce) {
// The getResource() trick won't work for this class. We have to
// try loading it directly and accept that we might get a
// ClassNotFoundException.
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}