JVM_02 类加载机制

  • Post author:
  • Post category:其他





JVM

类加载步骤


  • loading

    – 装载

  • linking

    – 链接


    • verification

      – 验证

    • preparation

      – 准备

    • resolution

      – 解析

  • intializing

    – 初始化
  1. 装载

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

  3. 准备: 给类的静态变量分配存储空间 -> 此时会给类的全局变量赋默认值;

    int : 0;
    long : 0L;
    String : null;
    
  4. 解析:将符号引用转成直接引用;

    符号引用: 例如String的符号引用 –

    Ljava/lang/String

    , class 文件的常量池中,

    fields



    descriptor_index

    代表了这个属性的类型,以一个符号引用的形式存储在 常量池中。

    直接引用:指向实际值的地址;

  5. 初始化: 对变量赋初始值

​ 类的初始化过程是类加载的最后一个过程。虚拟机没有规定一个类何时开始加载,但是有规定几种情形必须对类进行初始化操作:

  1. 遇到 new、getstatic、putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。



ClassLoader

​ 类被加载后

  1. 将类的二进制内容放进内存(

    metaSpace

    )中
  2. 生成了一个

    java.lang.Class

    类的对象
  • MethodArea (方法区) 在 jdk 1.7 及之前是 perm(永久代),在 jdk 1.8 及之后是 metaSpace(元数据区)



双亲委派机制

类加载器有一个层次关系:

在这里插入图片描述

​ 注意这里的层次关系并不是继承关系,而加载器的父加载器是通过

ClassLoader

中的一个成员变量

parent

来指定的;



父加载器 – 不是类加载器的加载器,也不是加载器的父类

  1. 这里的最底层加载器是

    BootStrap

    加载器,这个加载器是由

    C++

    实现的,负责加载

    lib/rt.jar

    ,

    charset.jar

    等的内容;
sys_paths = initializePath("sun.boot.library.path");

  1. ExtClassLoader

    用于加载

    jre/lib/ext/*jar

    等扩展包中的类,通过以下配置指定。并且可以通过


    -Djava.ext.dirs

    配置

String var0 = System.getProperty("java.ext.dirs");

  1. AppClassLoader

    加载

    classPath

    的类
final String var1 = System.getProperty("java.class.path");

  1. CustomClassLoader

    是可以自定义指定的类加载器,也就是说,我们自己写的类加载器,其父加载器都是

    AppClassLoader

​ 获取某个类是由哪个加载器加载的

System.out.println(Object.class.getClassLoader());
System.out.println(User.class.getClassLoader().getParent());
/* ---------------------------------------
output:
null
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
*/

1. 由于 Object 是由 BootStrapClassLoader 加载器加载的,但由于这个加载器是由 C++ 实现的,在java层级并没有与之对应的类。所以Object 的 ClassLoader 为null;
2. 程序中的某个类,其加载器是 Launcher 类的子类 -> AppClassLoader
3. AppClassLoader 的父加载器是 ExtClassLoader



类的加载过程:

// java.lang.ClassLoader abstract
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;
}
}

​ 以上代码可以看出,在加载之前,先去自己的缓存里面找一下这个类有没有被自己加载过。如果已经加载过则直接返回这个类之前加载的结果。

​ 如果没有加载过,则调用父加载器进行加载。其中判断

parent == null

即为判断其父加载器是不是

BootStrapClassLoader

,调用这个加载器需要进行 native 方法调用。这是个类似递归的调用过程。

​ 父加载器也要在自己的缓存中检测这个类有没有被自己加载过,如果父加载器没有加载成功,就会调用自己的加载逻辑进行加载。



为什么要使用双亲委派机制进行类的加载?

  1. 主要原因:为了安全

    防止类进行重复加载,如果发生了重复加载,则之前加载的类便会被覆盖掉,不仅不能保证类的单例性,而且如果自己写了一个名叫

    java.lang.String

    的类替换掉了 Java 原来的 String ? 将会造成不可描述的问题。

  2. 次要原因:为了资源节约

    类是单例的,无需进行重复加载。



自己写一个类加载器

什么时候需要加载类?

​ 某些 热部署/热替换 的场景,需要手动编译新的 Class ,并将其替换时。比如 Spring 中大量使用的代理。

  1. 需要继承

    java.lang.ClassLoader

    CLassLoader 类中使用

    模板模式

    定义了 loadClass 方法作为加载类的入口,写死了双亲委派机制的同时,提供了一个具体加载类的模板 ->

    findClass

    方法。

  2. 重写

    findClass

    方法

  3. 准备好类的 二进制字节流后,使用

    defineClass

    方法生成

    java.lang.Class

    对象并返回

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println(name);
        File f = new File("/Users/.../java/main/",
                name.replaceAll(".", "/").concat(".class"));
        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            int b;
            while ((b = fis.read()) != 0) {
                baos.write(b);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();

            // 转换为 class 对象
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    public static void main(String[] args) throws Exception {
        ClassLoader l = new MyClassLoader();
        Class clz = l.loadClass("com.improve.jvm.classloader.Hello");
        Hello h = (Hello) clz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());
        System.out.println(h.getClass().getClassLoader());
    }
}
  • 自定义的

    ClassLoader

    由于在项目中,所以它是由

    AppClassLoader

    加载进来的。



其他

  1. Java 使用一种

    lazy loading

    (懒加载) 的模式,即需要用才加载。

    在引用一个 jar 包时,并不会将整个 jar 包中都的 Class 都加载进来。

  2. 通过自定义的 ClassLoader 可以实现对 Class文件的加密,首先对一个 Class 文件进行一定规则的加密,生成另一个 文件 , 在类加载时,对这个类进行解密。

// 一个简单的基于异或运算的加解密样例
public class EncriptionClassLoader extends ClassLoader {

    public static int seed = 0B10110110;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("c:/test/", name.replace('.', '/').concat(".pbclass"));

        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) {
                baos.write(b ^ seed);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    public static void main(String[] args) throws Exception {

        encFile("com.improve.jvm.hello");

        ClassLoader l = new EncriptionClassLoader();
        Class clazz = l.loadClass("com.improve.jvm.Hello");
        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());
    }

    private static void encFile(String name) throws Exception {
        File f = new File("c:/test/", name.replace('.', '/').concat(".class"));
        FileInputStream fis = new FileInputStream(f);
        FileOutputStream fos = new FileOutputStream(new File("c:/test/", name.replaceAll(".", "/").concat(".pbclass")));
        int b = 0;

        while((b = fis.read()) != -1) {
            fos.write(b ^ seed);
        }

        fis.close();
        fos.close();
    }
}
  1. jVM 没有规定一个类什么时候被加载,但是它规定了一个类什么时候必须进行初始化(初始化肯定要先加载了啊,憨憨)
1. 在对类进行 new, getstatic, putstatic, invokestatic 时 (访问 final 的变量除外,因为 final 的变量无需进行初始化已经可以拿来用了。 换句话说,访问 final 的变量,并不会导致这个类被初始化);
2. reflect 反射访问时;
3. 子类初始化,父类必须先初始化;
4. 虚拟机启动时,含有 main 方法的那个主类要初始化;
5. 动态语言支持中,method_handle 方法句柄解析的结果为:
	REF_getstatic;
	REF_putstatic;
	REF_invokestatic;
以及 在 Class.forname();



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