JAVA类隔离机制
目录
类加载概述
什么是类加载
- 我们写的java代码(.java文件)经过编译器编译后,生成字节码文件(.class文件),class文件中包含了JVM虚拟机指令集和符号表等信息。虚拟机把class文件的数据加载到内存,并对数据做一些校验、转换解析以及初始化,最终形成可以被jvm直接使用的java类型,就是虚拟机的类加载机制。
类加载的好处
在java程序中,类的加载、链接、初始化都是在运行期完成的,这种方式会稍微增加一些性能损耗,但是提高了java程序的灵活度。比如
- 面向接口的程序,可以再运行时在指定其具体的实现类。(举例:bcp的校验脚本编写,doom的自定义子调用处理等)
- 通过自定义classLoader,可以从网络或其他地方加载特定的class文件。(所以class文件不仅仅指存在本地磁盘的class文件,可以是任何符合jvm虚拟机规范的二进制流。)
类加载流程
类加载时机
一个java类从被加载到虚拟机内存,到卸载出内存,其生命周期包含7个阶段:

什么情况下开始类加载阶段的第一个阶段加载? java虚拟机并没有进行强制约束,但是以下5中情况,虚拟机规范则是严格规定了必须立即对类进行初始化:
- 1、使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。
- 2、使用java.lang.reflect包的方法对类进行反射调用的时候。
- 3、初始化一个类的子类的时候。若其父类未被初始化,则先触发父类的初始化。
- 4、虚拟机执行启动时,被标明为启动类的类(包含main的类)。
- 5、使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(关于方法句柄,其功能和反射类似,有点是更灵活简洁,可以参考简单了解下:java7新特性之方法句柄MethodHandle使用_零度的博客专栏-CSDN博客_java methodhandle)
除主动引用(上述5种场景)之外,其他所有引用类的方式都不会触发初始化,这些其他类的引用方式称为被动使用。
比如:
- 1、通过子类引用父类的静态字段,不会导致子类初始化。
- 2、通过数组定义来引用类,不会触发此类的初始化。(举例:ClassA[] aList= new ClassA[10])
- 3、常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发。
- 4、通过类名获取Class对象,不会触发类的初始化。
- 5、通过Class.forName加载指定类时,如果指定参数initialize(这个参数告诉虚拟机,要不要对类进行初始化)为false时,也不会触发类的初始化。
- 6、通过ClassLoader默认的loadClass方法,也不会触发初始化。
加载顺序
JVM自带有三个类加载器,jvm的类加载顺序也是以下三个顺序来的:
- Bootstrap ClassLoader
- 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如
java -Xbootclasspath/a:path将指定的文件追加到默认的bootstrap路径中。
- 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如
- Extention ClassLoader
- 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。可以通过
-D java.ext.dirs指定的加载目录。
- 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。可以通过
- Appclass Loader也称为SystemAppClass
- 加载当前应用classpath下所有类。
加载顺序为什么是这样,直接看源码
Luancher源码
sun.misc.Launcher,是java虚拟机的入口,下面截了部分Launcher的代码:
// This class is used by the system to launch the main application.
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//初始化ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader");
}
try {
//初始化APPCLassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader");
}
//将APPCLassLoader设置为线程上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);
...
}
}
public ClassLoader getClassLoader() {
return this.loader;
}
static class AppClassLoader extends URLClassLoader {...}
static class ExtClassLoader extends URLClassLoader {...}从以上代码可以看到
- Launcher初始化了ExtClassLoader和AppClassLoader。 并将AppClassLoader并设置到当前线程contextClassLoader。每个线程实例都可以设置一个contextClassLoader。
- Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty("sun.boot.class.path"),这里指定了BootstrapClassLoader的类加载路径
ExtClassLoader源码
ExtClassLoader和APPCLassLoader都是Launcher的内部类
// create an ExtClassLoader. The ExtClassLoader is created within a context that limits which files it can read
static class ExtClassLoader extends URLClassLoader {
private File[] dirs;
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return (ExtClassLoader) AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Object run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
void addExtURL(URL url) {
super.addURL(url);
}
/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
// 第二个参数为null,即父加载器为null
super(getExtURLs(dirs), null, factory);
this.dirs = dirs;
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}可以看到
- String s = System.getProperty("java.ext.dirs"); 即ExtClassLoader的类加载路劲。
- super(getExtURLs(dirs), null, factory); /第二个参数为null,即父加载器为null
AppClassLoader源码
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
//设定其父加载器为ExtCLassLoader
return new AppClassLoader(urls, extcl);
}
});
}
......
}可以看到
- AppClassLoader加载的就是java.class.path下的路径
- getAPPCLassLoader中,设定其父加载器为ExtCLassLoader
双亲委托
上面的代码我们可以看到各个类加载是从哪加载类的,但是类的加载顺序是怎么样的。先看结论,下面的简图

整个类加载的流程如下,也就是双亲委托机制:
- 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
- 递归,重复第1部的操作。
- 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
- Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
- ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。找不到则抛异常。
父加载器不是父类
我们写个简答的代码测试下类加载器的结构。
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}运行结果如下:
sun.misc.Launcher$AppClassLoader@28a418fc sun.misc.Launcher$ExtClassLoader@2a84aee7 null
上面贴过APPCLassLoader和ExtClassLoader的源码,它们都继承自URLClassLoader,为什么AppCLassLoader的父加载器是ExtClassLoader。而ExtClassLoader的parent为null。我们先看类图结构:

直接看ClassLoader的源码
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
// @GuardedBy("ClassLoader.class")
private static ClassLoader scl;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
...
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
if (parent == null)
return null;
return parent;
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
return scl;
}
...
}
}可以看到parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:
- 由外部类创建ClassLoader时直接指定一个ClassLoader为parent。
- 由
getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。如果一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
上面已经贴过APPCLassLoader和ExtCLassLoader的代码,这里可以再看下,ExtCLassLoader传入的parent为null,AppClassLoader的parent是一个ExtClassLoader实例。
那为什么说ExtCLassLoader的父加载器是Bootstrap ClassLoader呢?继续往下看
Bootstrap ClassLoader
- 上面提到了AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null,那Bootstrap ClassLoader是什么?
- Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,
- JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件(int.class,String.class都是由它加载)。JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器,ExtClassLoader的parent是null。
- 那为什么说ExtClassLoader的parent是Bootstrap 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) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
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();
//父加载器没有找到,则调用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()
resolveClass(c);
}
return c;
}
}代码里可以直接看到当parent为null,向上委托时,系统会指定Bootstrap ClassLoader去加载类。也就是为什么逻辑上说,ExtClassLoader的parent是Bootstrap ClassLoader。
双亲委托主要就是由LoadClass实现的。
自定义ClassLoader
一般步骤
自定义一个CLassLoader一般按照以下的步骤
- 编写一个类继承自ClassLoader抽象类。
- 复写它的
findClass()方法。(不去重写loadClass,主要是为了不破坏双亲委托)
- 在
findClass()方法中调用defineClass()。
举个栗子
新建两个类A,B,并编译后放到指定目录
ClassA:
public class ClassA {
public void helloClassA(){
System.out.println("hello Class A!");
}
public void printB() {
ClassB b = new ClassB();
b.helloClassB();
System.out.println("ClassB ClassLoader: " + b.getClass().getClassLoader());
}
}、
ClassB:
public class ClassB {
public void helloClassB(){
System.out.println("hello Class B!");
}
}MyCLassLoderA.java
public class MyClassLoaderA extends URLClassLoader {
private String fileDir;
/**
* 注意:
* 未设置parent,默认为APPCLassLoader.
* 如果urls和3个parent有重叠, 或者加载的类name 3个parent的路径里有,
* 如果不重写loadClass,或者findCLass,则会被parent加载.
* 当前应用打的jar包,APPCLassLoader都能找到.
*
* @param urls 类加载器的jar包加载目录.
*/
public MyClassLoaderA(URL[] urls, String fileDir) {
super(urls);
this.fileDir = fileDir;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
String fileName = getFileName(className);
File file = new File(fileDir, fileName);
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(className,data,0,data.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(className);
}
//获取要加载 的class文件名
static String getFileName(String name) {
int index = name.lastIndexOf('.');
if (index == -1) {
return name + ".class";
} else {
return name.substring(index + 1) + ".class";
}
}
// public static void main(String[] args) {
// String className = "com.dzh.javalearn.classLoaderTest.ClassA";
// try {
// findClass0(className);
// } catch (ClassNotFoundException e) {
// e.printStackTrace();
// }
// }
}ClassLoaderTest.java
public class ClassLoaderTest {
public static void main(String[] args) {
String classFileDir = "/Users/dongzihui/mytrunk/modules/java8learn/classTestDir/DIR1";
String className = "com.dzh.javalearn.classLoaderTest.ClassA";
MyClassLoaderA classLoaderA = new MyClassLoaderA(getUrls(classFileDir), classFileDir);
try {
Class<?> clazz = classLoaderA.loadClass(className);
System.out.println("ClassA classLoader: " + clazz.getClassLoader());
Object a = clazz.newInstance();
Method method = clazz.getDeclaredMethod("printB",null);
method.invoke(a, null);
} catch (Exception e) {
}
}
public static URL[] getUrls(String path) {
File file = new File(path);
List<URL> list = new ArrayList<URL>();
if (file.exists() && file.isDirectory()) {
File[] files = file.listFiles();
try {
for (File f : files) {
list.add(f.toURI().toURL());
}
} catch (Exception e) {
e.printStackTrace(err);
}
}
return list.toArray(new URL[0]);
}
}运行后的结果:
com.dzh.classloader.MyClassLoaderA@a09ee92 hello Class B! ClassB ClassLoader: com.dzh.classloader.MyClassLoaderA@a09ee92
可以看到ClassA由MyClassLoaderA加载,A中new了classB,则B也是由MyClassLoaderA加载。
破坏双亲委托
看上面的loadClass
案例
JVM 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个”类”不是由一个 ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是 ClassLoader 隔离的基础原理。
Doom的类隔离
doom的类隔离上一篇已经简单提了下。主要是两点
- 1. 自定义DoomClassLoader,在doom-bootstrap中创建DoomClassLoader对象,并设置其类加载路径为/home/admin/doom/lib/5.7.4 (版本号是根据用户在doom平台上配置的),并通过该doomClassLoader对象去加载DoomBootInitializer,并反射调用其init方法。这样DoomBootInitializer用到的类其加载器均为doomClassLoader。
- 2. 在DoomBootInitializer.init方法中,设定doom核心spring文件的类加载器为DoomClassLoader。
pandora的类隔离
问题&痛点
pandora是什么
pandora是一个“魔盒”,里面装着集团的核心中间件,包括hsf/tair/diamond等等,都在里面。 它更让人熟知的名字是taobao-hsf.sar,起初taobao-hsf.sar 是 HSF 中的一个类隔离模块,后来逐渐独立开来发展为集团的中间件隔离模块,但是因为taobao-hsf.sar这个字符串已经深入到各个启动脚本中,改动起来风险比较大,所以这个名字一直保留了下来。
