JAVA类隔离机制

JAVA类隔离机制

目录

类隔离机制

类加载概述

什么是类加载

类加载的好处

类加载流程

类加载时机

加载顺序

Luancher源码

ExtClassLoader源码

AppClassLoader源码

双亲委托

父加载器不是父类

Bootstrap ClassLoader

自定义ClassLoader

一般步骤

举个栗子

破坏双亲委托

案例

Doom的类隔离

pandora的类隔离

问题&痛点

pandora是什么

JVM-Sandbox的隔离


类加载概述

什么是类加载

  • 我们写的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种场景)之外,其他所有引用类的方式都不会触发初始化,这些其他类的引用方式称为被动使用。

比如:

  • 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路径中。
  • Extention ClassLoader
    • 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。可以通过-D java.ext.dirs指定的加载目录。 
  • 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一般按照以下的步骤

  1. 编写一个类继承自ClassLoader抽象类。
  1. 复写它的findClass()方法。(不去重写loadClass,主要是为了不破坏双亲委托)
  1. 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这个字符串已经深入到各个启动脚本中,改动起来风险比较大,所以这个名字一直保留了下来。


版权声明:本文为qq_34376868原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。