JVM系列(一):类的加载机制

什么是类的加载

类的加载指的是程序将此类的.class文件的二进制数据加载到内存中,将其放入运行时的方法区内,并在堆中开辟一个内存区域用来创建和存放java.lang.Class文件对象,这个对象有类的一些说明描述,也为反射技术提供前提条件。

加载.class文件的几种方式:

  • 从本地系统中加载
  • 从.zip、.jar中加载
  • 从网络中下载
  • 从数据库中读取
  • 将Java源码动态编译成.class文件

类的声明周期

类的加载可以是主动的,也可以是被动的(预加载),当程序需要加载一个类的时候,会通过:加载 -> 连接 -> 初始化三个步骤来完成。而连接也分为三步:验证 -> 准备 -> 解析。具体的请听我细细道来。

JVM规范允许类加载器在预料到某个类将要被使用时就预先加载它,如果发现class文件缺失或者异常,类加载器应该在程序首次使用此类时才报告错误

image

加载

查找并加载类的二进制数据是整个类加载的第一步,在整个阶段,JVM需要完成以下三件事情:

  • 通过类的全限定名查找类的二进制数据(查找)
  • 将找到的二进制数据(代表的静态存储结构)转换成方法区的运行时数据结构(转换)
  • 在Java堆中开辟一个内存并创建此类的Class文件对象(载入)

连接

验证

验证是连接阶段的第一步,这一步的目的是为了确保Class文件的数据符合当前JVM的要求,并且不会危害到JVM,验证一个类是否有完整的内部结构。有四个地方需要校验:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

这一步非常重要但并不是必须的,可以通过-Xverifynone参数关闭大部分的类的验证,从而缩短加载的时间

准备

在准备阶段,程序会为这个类的静态变量分配内存并初始化(0,null,false等)。

public static intValue = 3;

在这里,intValue是等于0,而不是等于3。只有在最后的初始化阶段才会赋给intValue开发者指定的值。

public static final intValue = 3;

这里intValue是等于3,而不是等于0,static final修饰的变量在编译的时候就将其结果放在了调用它的类的常量池中,被这两者同时修饰的变量会产生一个ContantValue属性,并且在准备阶段就为其赋予开发者指定的值。

ConstantValue作用是告诉JVM要为次变量自动赋值,变量类型仅仅限于String和基本类型(上面说了将结果放入了常量池中,而只有String和基本类型的字面量才能在常量池中引用到)。

如果变量没有被两者同时修饰,或者又不满足条件的话,就会在构造器中初始化。

static:被static修饰的变量,在准备阶段会被初始化,在初始化阶段会被赋值,并且不能像final一样在构造器中初始化。

final:被final修饰的变量,在运行时被初始化,赋值后不可更改,可以直接赋值,也可以在构造器中赋值。

static final:被两者同时修饰的变量,会产生一个ContantValue属性,此变量在准备阶段直接赋值。

解析

这是连接的最后一步,将此类的符号引用全部转为直接引用。

初始化

在这阶段,JVM会为类的静态变量赋予真正的被指定的值。类的初始化时机:

  • new
  • 接触了该类或该接口的静态变量
  • 接触了该类或该接口的静态方法
  • 反射技术
  • 操作了其子类
  • Java虚拟机启动时被标明为启动类的类(JavaTest),直接使用 java.exe命令来运行某个主类

类的加载器

类的加载器(ClassLoader)是一个抽象类,负责类的加载阶段,它的实例将读入Java字节文件码,并将类装载到JVM中。

从开发人员角度看,类加载大概可以分为三类:

  1. 启动类加载器Bootstrap ClassLoader,负责加载存放在JDK(代表JDK的安装目录,下同)jrelib目录下、或者被-Xbootclasspath参数指定的路径中,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被它所加载),该加载器无法被Java程序直接引用。
  2. 扩展类加载器Extension ClassLoader,负责加载JDKjrelibext下、或者是java.ext.dirs系统变量指定的路径中的类库,该加载器可以在程序中直接引用。
  3. 应用类加载器/系统类加载器App/System ClassLoader,负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果程序中没有定义过类加载器,此加载器则是默认的
  4. 自定义类加载器Custom ClassLoader

它们之间的关系:

image

工作机制

默认的是双亲模式,上面的四个加载器,自底向上检查类是否已经加载,也就是先会在App ClassLoader检查是否已经加载,如果未检查到,第一时间不是自己去加载,而是把这个请求给父类Extension ClassLoader,让Extension ClassLoader来继续做检查工作。同样,如果Extension ClassLoader也没检查到,那继续抛给BootStrap ClassLoader,找到的话就返回,如果没有找到,那就BootStrap ClassLoader先来尝试加载,如果加载成功就返回这个Class,如果没有加载成功就往下推。往上走上层是父亲下层是儿子,往下走下层是父亲,上层是儿子,所以是双亲。

image

ClassLoader 源码解析

// 加载一个类
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
// resolve - 是否解析该类
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 {
                    // 否则调用本地的方法:private native Class<?> findBootstrapClass(String name);没有找到则返回null
                    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;
    }
}

关于双亲模式

双亲的意义:

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

双亲的问题:
顶层ClassLoader无法加载底层ClassLoader中的类,也就无法生成相关的类的实例,在加载的过程中,一楼的人可以看到顶楼的人,但是因为是自底向上,不能往下看,那么顶楼的人看不到一楼的人,万一顶楼人的其中一个亲人在一楼了呢,那就永远也找不到,这就是双亲模式的缺陷。

image

问题的解决:

Thread.setContextClassLoader() //  它是一个上下文加载器,用以解决顶层加载器无法访问底层加载器的问题,只要在顶层加载器传入底层加载器的实例就行,类似于一个通道。

自定义加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

public class MyClassLoader extends ClassLoader {
    private String root; // 文件路径

    public void setRoot(String root) {
        this.root = root;
    }

    /**
     * 自定义类需要重写的方法
     * 
     * @param name 类名
     * @return 返回此类的Class文件对象
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = loadClassData(name);
        if (bytes == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, bytes, 0, bytes.length);
    }

    /**
     * 加载文件二进制数据
     *
     * @param className 类名(包名.类名)
     * @return 这个类的字节数组
     */
    private byte[] loadClassData(String className) {
        /* 得到类的全限定名 */
        String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class";

        InputStream ins = null;
        ByteArrayOutputStream baos = null;
        try {
            ins = new FileInputStream(fileName);
            baos = new ByteArrayOutputStream();
            int bufferSize = 1024, length;
            byte[] buffer = new byte[bufferSize];
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                ins.close();
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public static void main(String[] args) {
        MyClassLoader myClassLoader = new MyClassLoader();
        myClassLoader.setRoot("E:\\Project\\Play\\Timers\\out\\production");

        Class<?> loadClass;
        try {
            loadClass = myClassLoader.findClass("net.imain.MyClassLoader");
            Object instance = loadClass.newInstance();
            System.out.println(instance.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

自定义加载器主要是对传入进来的类的字节码文件的读取(无加解密步骤)。需要注意:

  • 因为defineClass()的格式要求,需要传入类的全限定名:net.imain.ClassLoader
  • 最好不要重写loadClass方法,因为会破坏双亲模式
  • 这个类本身可以是被App ClassLoader加载,因此不能把net\imain\Test.class放在类路径下

ClassLoader API

public Class<?> loadClass(String name) throws ClassNotFoundException // 载入并返回一个Class

protected final Class<?> defineClass(Stirng name, byte[] b, int off, int len) // 定义一个类,不公开调用

protected Class<?> findClass(String name) throws ClassNotFoundException // loadClass 回调该方法,自定义ClassLoader的推荐做法

protected final Class<?> findLoadedClass(String name) // 寻找已经加载的类
Last modification:January 30th, 2018 at 06:07 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment