java 类加载原理分析

来源:互联网 发布:node全栈工程师招聘 编辑:程序博客网 时间:2024/05/17 02:31

java 类加载原理分析

    java 所有的代码都是要经过编辑成 class 文件,也就是class文件才能被虚拟机识别,在被虚拟机加载的过程 需要完成下面3步

  1. 通过一个类的全限定名来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。

下面可以看代码1验证一下,然后在分析代码

        ClassLoader classsLoader = Main.class.getClassLoader();        while (classsLoader != null) {            System.out.println(classsLoader.toString());            classsLoader = classsLoader.getParent();        }

运行结果

sun.misc.Launcher$AppClassLoader@12a3a380

sun.misc.Launcher$ExtClassLoader@511d50c0

可以这个类是由Launcher的内部类AppClassLoader加载的,AppClassLoader的父类为同样的内部类ExtClassLoader,其实 还有一个加载类为Bootstrap ClassLoader ,这三个类加载器,加载不同目录和功能

  1. 启动类加载器:Bootstrap ClassLoader,它负责加载存放在JDK\jre\li(JDK 代表 JDK 的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如 rt.jar,所有的java.*开头的类均被 Bootstrap ClassLoader 加载)。启动类加载器是无法被 Java 程序直接引用的,这个加载器不是java写的。
  2. 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器:Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,一般可以直接使用,一般情况下开发中的类的加载器就是他。

ExtClassLoader和AppClassLoader都是继承URLClassLoader 继承于 ClassLoader ,ClassLoader是类加载器的基类,除了启动类加载器是由java实现的,以上运行结果没有这个类,是由于他没有父类。

加载入口loadClass

    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;        }    }

这个是ClassLoader的类加载入口,其中类加载前首先判断该类有没有被加载过,如果被加载过直接返回,否则调用父类加载器,以此类推,如果没有加载最终调用自身加载。

下面是AppClassLoader的类加载方法

        public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {            int var3 = var1.lastIndexOf(46);            if(var3 != -1) {                SecurityManager var4 = System.getSecurityManager();                if(var4 != null) {                    var4.checkPackageAccess(var1.substring(0, var3));                }            }            if(this.ucp.knownToNotExist(var1)) {                Class var5 = this.findLoadedClass(var1);                if(var5 != null) {                    if(var2) {                        this.resolveClass(var5);                    }                    return var5;                } else {                    throw new ClassNotFoundException(var1);                }            } else {                return super.loadClass(var1, var2);            }        }

其中this.ucp.knowToNotExist(var1)这个方法也是用来判断该类有没有被加载过。类似于父类的加载流程。

在看ClassLoader的加载方法当所有类没有完成类的字节码加载的时,会调用findClass方法,

    protected Class<?> findClass(final String name)        throws ClassNotFoundException    {        final Class<?> result;        try {            result = AccessController.doPrivileged(                new PrivilegedExceptionAction<Class<?>>() {                    public Class<?> run() throws ClassNotFoundException {                        String path = name.replace('.', '/').concat(".class");                        Resource res = ucp.getResource(path, false);                        if (res != null) {                            try {                                return defineClass(name, res);                            } catch (IOException e) {                                throw new ClassNotFoundException(name, e);                            }                        } else {                            return null;                        }                    }                }, acc);        } catch (java.security.PrivilegedActionException pae) {            throw (ClassNotFoundException) pae.getException();        }        if (result == null) {            throw new ClassNotFoundException(name);        }        return result;    }

调用

    private Class<?> defineClass(String name, Resource res) throws IOException {        long t0 = System.nanoTime();        int i = name.lastIndexOf('.');        URL url = res.getCodeSourceURL();        if (i != -1) {            String pkgname = name.substring(0, i);            // Check if package already loaded.            Manifest man = res.getManifest();            definePackageInternal(pkgname, man, url);        }        // Now read the class bytes and define the class        java.nio.ByteBuffer bb = res.getByteBuffer();        if (bb != null) {            // Use (direct) ByteBuffer:            CodeSigner[] signers = res.getCodeSigners();            CodeSource cs = new CodeSource(url, signers);            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);            return defineClass(name, bb, cs);        } else {            byte[] b = res.getBytes();            // must read certificates AFTER reading bytes.            CodeSigner[] signers = res.getCodeSigners();            CodeSource cs = new CodeSource(url, signers);            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);            return defineClass(name, b, 0, b.length, cs);        }    }

最终调用ClassLoader的 defineClass 方法,这个方法被定义为final 是不能够被复写的,尽管参数不同但是都是通过加载byte数据,通过底层实现Class 的构建,其中里面的由于涉及涉及JVM内存区等这里先不讨论。

下图是系统加载器流程图


先验证一下上述流程图的问题,主要涉及到自定义加载的问题,细看ClassLoader可以发现,classloder的加载机制是在基类里面已经实现好的,如果需要自定义类加载器,没有必要重写loadClass方法,只需要重写findClass方法就可以

public class MyClassLoader extends ClassLoader {    private String rootPath;    public MyClassLoader(String rootPath) {        this.rootPath = rootPath;    }    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        //check if the class have been loaded        Class<?> c = findLoadedClass(name);        if (c != null) {            return c;        }        //load the class        byte[] classData = getClassData(name);        if (classData == null) {            throw new ClassNotFoundException();        } else {            c = defineClass(name, classData, 0, classData.length);            return c;        }    }    private byte[] getClassData(String className) {        String path = rootPath + "/" + className.replace('.', '/') + ".class";        InputStream is = null;        ByteArrayOutputStream bos = null;        try {            is = new FileInputStream(path);            bos = new ByteArrayOutputStream();            byte[] buffer = new byte[1024];            int temp = 0;            while ((temp = is.read(buffer)) != -1) {                bos.write(buffer, 0, temp);            }            return bos.toByteArray();        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                is.close();                bos.close();            } catch (Exception e) {                e.printStackTrace();            }        }        return null;    }}

上面是自定义的类加载器

public class Test1 {    private Test1 instance;    public void setSample(Object instance) {        this.instance = (Test1) instance;    }    public String getName() {        return "test1";    }}

上图是随便写的测试类.

验证流程问题

            String root = "/Users/xinggenguo/Desktop";            String name = "Test1";            ClassLoader classLoader = new MyClassLoader(root);            Class classC = classLoader.loadClass(name);            while (classLoader != null) {                System.out.println(classLoader.toString());                classLoader = classLoader.getParent();            }

输出结果

MyClassLoader@60e53b93

sun.misc.Launcher$AppClassLoader@12a3a380

sun.misc.Launcher$ExtClassLoader@1d44bcfa

这就验证了上述问题。

关于类加载为什么这么设计,需要看一下一个类是否相同是怎么判断的,类是否相同不单于类的全名相关还和类加载器相关。

        try {            String root = "/Users/xinggenguo/Desktop";            String name = "Test1";            ClassLoader classLoader = new MyClassLoader(root);            Class classC = classLoader.loadClass(name);            Object obj1 = classC.newInstance();            MyClassLoader classLoader1 = new MyClassLoader(root);            Class classC1 = classLoader1.loadClass(name);            Object obj2 = classC1.newInstance();            System.out.println(obj1.getClass().getClassLoader().toString());            System.out.println(obj2.getClass().getClassLoader().toString());            Method setSampleMethod = classC.getMethod("setSample", java.lang.Object.class);            setSampleMethod.invoke(obj1, obj2);        } catch (Exception e) {            e.printStackTrace();        }

运行这段代码会出现Caused by: java.lang.ClassCastException: Test1 cannot be cast to Test1 异常也就验证了上述问题。

在回过来分析上面为什么要这么设计类加载,java下所有的类都是继承于Object类的,这就可以保证java核心类库在同一个虚拟机下,只有同一个版本,这些都是可以兼容的,反过来,又可以通过不同的类加载器为开发者提供相对对立的空间。

同时类加载器不单单可以加载内部的class类,java包,也可以加载外部的class类。

还是使用上面的测试类,调用getName方法

        try {            String root = "/Users/xinggenguo/Desktop";            String name = "Test1";            ClassLoader classLoader = new MyClassLoader(root);            Class classC = classLoader.loadClass(name);            Object object = classC.newInstance();            Method setSampleMethod = classC.getMethod("getName");            System.out.println(setSampleMethod.invoke(object));        } catch (Exception e) {            e.printStackTrace();        }

打印结果:test1 

更改Test1类的getName方法重新调用。

    public String getName() {        return "test2";    }

打成class文件后重新运行

打印结果:test2

也就是说明,直接替换是生效的,但是在java程序在运行期间来替换,要涉及到资源的动态释放和或者替换,这里不讨论。


1 0
原创粉丝点击