您的位置:首页 > 其它

JVM解读(二):JVM类加载器ClassLoader

2015-06-26 19:04 274 查看
JVM全称是java Virtual Machine(java虚拟机),JVM屏蔽了与各个计算机平台相关的软件和硬件差异

在接下来的日子里,我要通过写博客的形式学习JVM,让自己更懂得Java

本系列文章是对《深入分析javaweb技术内幕》和《深入理解java虚拟机》的总结,欢迎大家一起吐槽,一起进步。

《JVM解读》第一篇:JVM体系结构

《JVM解读》第二篇:JVM类加载器ClassLoader

《JVM解读》第三篇:JVM内存区域

《JVM解读》第四篇:JVM内存溢出异常分析

《JVM解读》第五篇:JVM垃圾收集

ClassLoader的作用

(1)加载class文件进入JVM

(2)审查每个类应该由谁加载,采用双亲委托机制

(3)将class字节码重新解析成JVM要求的对象格式

ClassLoader结构分析

protected final Class<?> defineClass(byte[] b, int off, int len)throws ClassFormatError{
     return defineClass(null, b, off, len, null);
  }
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
protected final void resolveClass(Class<?> c) {
        resolveClass0(c);
    }


(1)defineClass方法用来将byte字节流解析成JVM能够识别的Class对象。如果直接用此方法创建类的class对象,这个类的class对象还没有resolve,这个resolve将会在这个对象真正实例化时才能进行,通常这个方法要和findClass()方法一起使用,通过直接覆盖ClassLoader父类的findClass方法来实现类的加载规则,从而取得要加载类的字节码。如果想在类被加载到JVM时就被链接,可以调用resolveClass方法

(2)ClassLoader类是一个抽象类,如果我们要实现自己的加载器一般会继承URLClassLoader这个子类。

ClassLoader的双亲委派加载机制

双亲委派加载机制即如果一类的加载器收到一个加载请求,它首先会将这个请求委托给父类加载器去加载,如果父类判断没有加载过这个类,在继续向其上一层加载器委派,每一层次的类加载器都是如此,则所有的类加载请求最终都会到达顶层的类加载器,只有父类加载器无法加载这个请求,子类才会去加载此类,这样可以避免重复加载同时防止用户自定义的类加载器替代java核心加载器。

这里我们可以看看ClassLoader的源码就知道ClassLoader的双亲委派机加载机制了

protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    // 首先,检查请求的类是否已经被加载过了
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClass0(name);
        }
        } catch (ClassNotFoundException e) {
      // If still not found, then invoke findClass in order
      //如果父类加载器抛出ClassNotFoundException ,说明父类加载器无法完成加载请求,则调用自身的findClass方法来进行类加载
            // to find the class.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
    }


JVM中提供的三个ClassLoader类,下图示层次





(1)Bootstrap ClassLoader:主要加载JVM自身工作需要的类,完全由JVM自己控制,这个类不遵守双亲委派加载机制,它仅仅是一个类的加载工具,既没有父加载器也没有子加载器。

(2)ExtClassLoader:这个类本身是JVM自身的一部分,但不是由JVM自身实现的,服务的特定目标在java.ext.dirs目录下的类

(3)AppClassLoader:这个类服务java.class.path目录下的类,即classpath路径。

如果我们要实现自己的类加载器,不管是直接继承ClassLoader还是继承URLclassLoaderlei ,它的父加载器都是AppClassLoader,因为不管调用哪个父类构造器,创建的对象都必须最终调用getSystemClassLoader()作为父类加载器,而该方法获取的正是AppClassLoader。

如果应用中没有定义其他的类加载器,那么除了java.ext.dirs下的类是由ExtClassLoader来加载,其他的都是由AppClassLoader来加载。

JVM加载Class文件到内存的两种方式

(1)隐式加载:在代码中不通过调用ClassLoader来加载需要的的类,而是通过JVM自动加载需要的类到内存方式。如我们类继承或类中引用其他类的时候,JVM在解析当前这个类的时候发现引用类不在内存中就会去自动的将这些类加载进入到内存。

(2)显示加载:在代码中通过调用ClassLoader类来加载类。如this.getClass().getClassLoader().loadClass()或者Class.forName()或者我们自己实现的findClass()方法。

类加载过程

类的生命周期




下面是类加载的过程




(1)第一阶段:找到class文件并把这个文件包含的字节码加载到内存

(2)第二阶段:字节码验证,class类数据结构分析以及相应内存分配,符号表的链接。

(3)第三阶段:类中静态属性初始化赋值以及静态代码块的执行

加载字节码到内存

在加载阶段JVM需要做的三件事:

(1)通过一个类的全限定名来获取定义此类的二进制字节流

(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

(3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

在抽象类ClassLoader中没有定义如何去加载,如何去找到指定类并加载到内存中需要子类中去实现,即实现findClass()方法。如在URLClassLoader中通过一个URLClassPath类帮组取得要加载的class文件字节流,而这个URLClassPath定义了到哪里去找这个class文件,如果找到这个class文件,再读取byte字节流通过调用defineClass方法来创建类对象。

protected Class<?> findClass(final String name)
         throws ClassNotFoundException
    {
        try {
            return 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 {
                            throw new ClassNotFoundException(name);
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
    }


验证阶段

验证是连接的第一步,这一阶段是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段对虚拟机非常重要,但不一定是必要阶段。如果所运行的全部代码都已经反复验证过了,在实施阶段可以关闭大部分类的验证,以缩短虚拟机类加载的时间。

(1)第一步:文件格式验证,要验证字节流是否符合Class文件格式规范,并且能被虚拟机处理。例如,是否以魔数0xCAFEBABE开头

(2)第二步:元数据验证,对字节码描述的信息进行语义分析,以保证其描述信息符合java语言规范要求。例如这个类是否有父类,这个类的父类是否继承了不允许被继承的类。

(3)第三步:字节码验证,整个验证阶段最复杂的阶段,主要是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。

(4)第四步:符号引用验证, 这一步发生在虚拟机将符号引用转换为直接引用阶段,即在解析中发生。

准备阶段

准备阶段是正式为类变量(被static修饰的变量)分配内存并设置变量初始值的阶段,这些内存都将在方法区中进行分配。这里仅仅包括类变量的内存分配,不包括实例变量,实例变量的内存分配将会在对象实例化时随着对象一起分配在java堆中。这里的初始值都是零值,如下代码中flag的初始值是false。

public static boolean flag=true;


但是如果上述代码变成下面这样,那么在准备阶段变量flag就会被初始化为ConstantValue属性所指定的值,也就是true。


public static final boolean flag=true;


解析阶段

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对的是类或接口,字段,类方法,接口方法四类符号引用。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与内存布局无关。引用的目标并不一定已经加载到内存中。

直接引用:直接引用可以使直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局有关的,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同的。如果有了直接引用,那引用的目标必定已经子啊内存中存在。

初始化阶段

初始化化阶段是类加载过程中最后一个阶段,前面的阶段除了类加载阶段用户可以通过自定义的加载器参与外,其余动作完全由虚拟机来主导和控制的,到了初始化阶段才真正开始执行类中定义的java程序代码。

这个阶段是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,类变量被初始化为默认值,静态代码块获得执行。

声明:本文很多是对《深入分析javaweb技术内幕》和《深入理解java虚拟机》的总结,希望大家多多提出意见,共同进步
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: