java类加载器机制

参考

https://blog.csdn.net/zhangjg_blog/article/details/16102131

https://www.jianshu.com/p/b6547abd0706

https://www.jianshu.com/p/8c8d6cba1f8e

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

类的加载机制简介

JVM除了比较类是否相等还要比较加载这两个类的类加载器是否相等,只有同时满足条件,两个类才能被认定是相等的。

JVM就是按照下面的顺序一步一步的将字节码文件加载到内存中并生成相应的对象的。

  1. 加载
  2. 连接(验证-准备-解析)
  3. 初始化

首先将字节码加载到内存中,然后对字节码进行连接,连接阶段包括了验证准备解析这3个步骤,连接完毕之后再进行初始化工作;下面我们一一了解。

获取Class对象的方式

获取Class对象的方式有3种:

  1. Class.forName(包名.类名);
  2. 类名.class
  3. 对象.getClass()

这几种获取Class对象的不同

  1. 类名.class : JVM将使用类装载器, 将类装入内存(前提是:类还没有装入内存),不做类的静态初始化工作(不执行静态代码块),返回Class的对象。
  2. 实例对象.getClass() :既然都有对象了,那么是在创建对象时就把静态代码块和构造代码块都执行了,返回引用运行时真正所指的对象(因为:子对象的引用可能会赋给父对象的引用变量中)所属的类的Class的对象。
  3. Class.forName(包名.类名) :装入类,并执行静态初始化工作(执行静态代码块),返回Class的对象。

Class.forName(String className)源码:

public static Class<?> forName(String className) throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

private static native Class<?> forName0(String name, boolean initialize, ClassLoader loader, Class<?> caller)

就是说第二个参数initialize是控制是否对类进行静态初始化。而Class.forName(String className)内部是true,所以会执行静态初始化工作。

什么是类的加载?

虚拟机加载类有两种方式,一种方式ClassLoader.loadClass()方法,另一种是使用反射API,Class.forName()方法,其实Class.forName()方法内部也是使用的ClassLoader。

类的加载指的是将类的.class文件中的二进制数据读入内存中,将其放在运行时数据区域的方法区内,然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构.只有java虚拟机才会创建class对象,并且是一一对应关系,这样才能通过反射找到相应的类信息。

我们上面提到过Class这个类,这个类我们并没有new过,这个类是由java虚拟机创建的。通过它可以找到类的信息,我们来看下源码:

/*
 * Constructor. Only the Java Virtual Machine creates Class
 * objects.
 */
private Class() {...}

从上面贴出的Class类的构造方法源码中,我们知道这个构造器是私有的,并且只有虚拟机才能创建这个类的对象。

讲到类加载,我们不得不了解类加载器。

类加载器及各类加载器关系

java中(指的是javase)有几种类加载器。每个类加载器在创建的时候已经指定他们对应的目录, 也就是说每个类加载器去哪里加载类是确定的。

  • BootStrap        --   加载JRE/lib/rt.jar中的类
  • ExtClassLoader   --   加载jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下的类
  • AppClassLoader  --   加载classpath指定的路径中的类
  • 自定义的ClassLoader

这几种ClassLoader并不是继承的关系,而是一种委托关系。

那么类加载器是如何工作的呢?可以参看jdk中ClassLoader类的源码。

private final ClassLoader parent;
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
            }
            return c;
        }
    }
    ...

从源码可以总结出三个特性,分别为委派,可见性和单一性,其他文章上对这三个特性的介绍如下:

  • 委托机制是指将加载一个类的请求交给父加载器,如果这个父加载器不能够找到或者加载这个类,那么再加载它。
  • 可见性的原理是子加载器可以看见所有的父加载器加载的类,而父加载器看不到子加载器加载的类。
  • 单一性原理是指仅加载一个类一次,这是由委托机制确保子加载器不会再次加载父加载器加载过的类。

其中,委派机制是基础,在其他资料中也把这种机制叫做类加载器的双亲委派模型,其实说的是同一个意思。可加性和单一性是依赖于委派机制的。

他们之间的关系可以通过例子展示:

ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@19821f

ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println(extClassLoader);  //sun.misc.Launcher$ExtClassLoader@addbf1
//AppClassLoader的父加载器是ExtClassLoader

System.out.println(extClassLoader.getParent()); //null
//ExtClassLoader的父加载器是null, 也就是BootStrap,这是由c语言实现的

由打印结果可知,

  • 加载我们自己编写的类它的加载器是AppClassLoader,
  • AppClassLoader的父加载器是ExtClassLoader,
  • 在而ExtClassLoader的父加载器返回结果为null,这说明他的附加载器是BootStrap,这个加载器是和虚拟机紧密联系在一起的,在虚拟机启动时,就会加载jdk中的类,它是由C实现的,没有对应的java对象,所以返回null。但是在逻辑上,BootStrap仍是ExtClassLoader的父加载器。也就是说每当ExtClassLoader加载一个类时,总会委托给BootStrap加载。
  • 另外我们自己实现的ClassLoader的parent是AppClassLoader。

自定义ClassLoader验证

package com.puppet.test;
import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader {
    public static void main(String args[]) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        ClassLoader loader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                if (inputStream == null) {
                    return super.loadClass(name);

                } else {
                    try {
                        byte[] bytes = new byte[inputStream.available()];
                        inputStream.read(bytes);
                        return defineClass(name, bytes, 0, bytes.length);

                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new ClassNotFoundException(name);
                    }
                }
            }
        };
        Object object = loader.loadClass("jvm.classloader.MyClassLoader").newInstance();
        System.out.println(object instanceof com.puppet.test.MyClassLoader);
    }
}

结果为false。

所以,我们如果开发自己的类加载器,只需要继承jdk中的ClassLoader类,并覆盖findClass方法就可以了,剩下的而工作,父类会完成。其他java平台有的根据自己的需求,实现了自己特定的类加载器,例如javaee平台中的tomcat服务器,android平台中的dalvik虚拟机也定义了自己的类加载器。

系统类加载器和线程上下文类加载器

在java中,还存在两个概念,分别是系统类加载器和线程上下文类加载器。

其实系统类加载器就是AppClassLoader应用程序类加载器,它两个指的是同一个加载器,以下代码可以验证:

    ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
    System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@19821f
    
    ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println(sysClassLoader);  //sun.misc.Launcher$AppClassLoader@19821f
    //由上面的验证可知, 应用程序类加载器和系统类加载器是相同的, 因为地址是一样的

这两个类加载器对应的输出,不仅类名相同,连对象的哈希值都是一样的,这充分说明系统类加载器和应用程序类加载器不仅是同一个类,更是同一个类的同一个对象。

每个线程都会有一个上下文类加载器,由于在线程执行时加载用到的类,默认情况下是父线程的上下文类加载器, 也就是AppClassLoader。

    new Thread(new Runnable() {
        @Override
        public void run() {
            ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
            System.out.println(threadcontextClassLosder); //sun.misc.Launcher$AppClassLoader@19821f
        }
    }).start();

这个子线程在执行时打印的信息为sun.misc.Launcher$AppClassLoader@19821f,可以看到和主线程中的AppClassLoader是同一个对象(哈希值相同)。

也可以为线程设置特定的类加载器,这样的话,线程在执行时就会使用这个特定的类加载器来加载使用到的类。如下代码:

    Thread th = new Thread(new Runnable() {
        @Override
        public void run() {
            ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
            System.out.println(threadcontextClassLosder); //jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74
        }
    });
    th.setContextClassLoader(new ClassLoader() {});
    th.start();

在线程运行之前,为它设置了一个匿名内部类的类加载器对象,线程运行时,输出的信息为:jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74,也就是我们设置的那个类加载器对象。

类的连接

讲完了类的加载之后,我们需要了解一下类的连接。类的连接有三步,分别是验证,准备,解析。下面让我们一一了解

首先我们看看验证阶段。

验证阶段主要做了以下工作

  • 将已经读入到内存类的二进制数据合并到虚拟机运行时环境中去。
  • 类文件结构检查:格式符合jvm规范
  • 语义检查:符合java语言规范,final类没有子类,final类型方法没有被覆盖
  • 字节码验证:确保字节码可以安全的被java虚拟机执行.
  • 二进制兼容性检查:确保互相引用的类的一致性。如A类的a方法会调用B类的b方法,那么java虚拟机在验证A类的时候会检查B类的b方法是否存在并检查版本兼容性,因为有可能A类是由jdk1.7编译的,而B类是由1.8编译的。那根据向下兼容的性质,A类引用B类可能会出错,注意是可能。

准备阶段

java虚拟机为类的静态变量分配内存并赋予默认的初始值。如int分配4个字节并赋值为0,long分配8字节并赋值为0;

解析阶段

解析阶段主要是将符号引用转化为直接引用的过程。比如 A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)。

初始化

类加载进内存后不一定会初始化,下边是几种触发类的主动初始化(类的静态初始化)的方式:

  1. 创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。
  2. 调用类的静态属性或者为静态属性赋值
  3. 调用类的静态方法
  4. 通过class文件反射创建对象
  5. 初始化一个类的子类:使用子类的时候先初始化父类
  6. java虚拟机启动时被标记为启动类的类:就是我们的main方法所在的类

只有上面6种情况才是主动使用,也只有上面六种情况的发生才会引发类的初始化。

同时我们需要注意下面几个Tips:

  1. 在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了.
  1. 调用 编译常量(在编译的时候能确定下来的),不会对类进行初始化;
  2. 调用 运行时常量(在编译时无法确定下来的),会对类进行初始化;

这里都是指的final static修饰的属性,而只有static修饰的属性就属于触发初始化的第二条。

  1. 如果这个类没有被加载和连接的话,那就需要进行加载和连接
  2. 如果这个类有父类并且这个父类没有被初始化,则先初始化父类.
  3. 如果类中存在初始化语句,依次执行初始化语句.
  4. 静态代码块和静态属性是同级别加载,就是说按照代码的编写顺序,谁在前谁就先执行

例子:

public class Test1 {
    public static void main(String args[]){
        System.out.println(FinalTest.x);
    }
}

class FinalTest{
    public static final int x =6/3;
    static {
        System.out.println("FinalTest static block");
    }
}

上面和下面的例子大家对比下,然后自己看看输出的是什么?

public class Test2 {
    public static void main(String args[]){
        System.out.println(FinalTest2.x);
    }
}
class FinalTest2{
    public static final int x =new Random().nextInt(100);
    static {
        System.out.println("FinalTest2 static block");
    }
}

第一个输出的是

2

第二个输出的是

FinalTest2 static block

61(随机数)

那么将第一个例子的final去掉之后呢?输出又是什么呢?

这就是对类的首次主动使用,引用类的静态变量,输出的当然是:

FinalTest static block

2

类的初始化步骤

讲到这里我们应该对类的加载-连接-初始化有一个全局概念了,那么接下来我们看看类具体初始化执行步骤。

我们分两种情况讨论,一种是类有父类,一种是类没有父类。(当然所有类的顶级父类都是Object)

没有父类的情况:

  1. 类的静态属性
  2. 类的静态代码块
  3. 类的非静态属性
  4. 类的非静态代码块
  5. 构造方法

有父类的情况:

  1. 父类的静态属性
  2. 父类的静态代码块
  3. 子类的静态属性
  4. 子类的静态代码块
  5. 父类的非静态属性
  6. 父类的非静态代码块
  7. 父类构造方法
  8. 子类非静态属性
  9. 子类非静态代码块
  10. 子类构造方法

在这要说明下,静态代码块和静态属性是等价的,他们是按照代码顺序执行的。

例子

public class Singleton {
    private static Singleton singleton = new Singleton();
    public static int counter1;
    public static int counter2 = 0;

    private Singleton() {
        counter1++;
        counter2++;
    }

    public static Singleton getSingleton() {
        return singleton;
    }
}

public class TestSingleton {
    public static void main(String args[]){
        Singleton singleton = Singleton.getSingleton();
        System.out.println("counter1="+singleton.counter1);
        System.out.println("counter2="+singleton.counter2);

    }
}

输出是:
counter1=1
counter2=0

 

注:调整counter1,counter2和singleton的顺序会得到不同结果。

 

总结

在Android中,QQZone团队提出的基于Dex分包的热修复解决方案就属于加载外部的类,本来应当由开发者自己实现classloader来实现加载过程,但是Android本身已经为我们封装好了一个classloader,就是DexClassLoader。

事实上,如今Java中很多插件化开发,动态部署,热修复等动态技术都是基于Java的类加载器来展开的。因此,我才会想专门用一篇文章总结Java的类加载器和加载机制。后面我会找时间基于HotFix详细的分析其中的类加载过程。毕竟理论总要落实到代码才会让人印象深刻。

猜你喜欢

转载自www.cnblogs.com/muouren/p/11706519.html