有关Java类加载机制的问题看这一篇就够了,没见过比这写的更细的了

522 阅读12分钟

类的生命周期

1. 加载:在这个阶段jvm主要完成三件事:

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

相对于类加载的其他阶段,加载阶段是可控性最强的阶段,因为我们可以使用系统的类加载器加载,也可以使用自定义的类加载器加载。

2. 验证: 主要作用就是确保被加载的类的正确性,也是连接阶段的第一步。即判断加载好的class文件不能对jvm有危害,它主要完成四个方面的验证

  • 文件格式验证:验证.class文件字节流是否符合class文件的格式规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验。
  • 元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
  • 字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。
  • 符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。

对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。

3. 准备:主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们需要注意两类变量和初始值这两个关键词:

  • 类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
  • 这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值

4. 解析:主要是jvm将常量池中的符号引用转化为直接引用的过程。

  • 符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在工作中,可以用cherry(你的英文名字)代表你,也可用工号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
  • 直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

5. 初始化: 在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。

在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值
  • 使用静态代码块为类变量指定初始值

JVM初始化步骤

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName(“com.pch.Test”))
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类,直接使用 java.exe命令来运行某个主类

注意点说明:

  1. 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

举例:

public class SuperClass {
    static {
        System.out.println("SuperClass init....");
    }

    public static int value = 1;
}

public class SubClass extends SuperClass{
    static {
        System.out.println("SubClass init....");
    }

     public static final int CONST = 2;
}

public class Test {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

输出:
SuperClass init....
1

  1. 通过数组定义来引用类,不会触发此类的初始化
SubClass[] subArrayClasses = new SubClass[2];

没有任何输出

  1. 调用类的常量字段不会触发类的初始化。
public class Test1 {
    public static void main(String[] args) {
        System.out.println(SubClass.CONST);
    }
}

输出:
2

6. 使用

7. 卸载

在类的生命周期过程中:加载、验证、准备、初始化、卸载这5个阶段的顺序是固定的,而解析阶段不一定,它有的时候会在初始化之后开始,这是为了支持java的运行时绑定。其中:类加载的全过程包括:加载、验证、准备、解析、初始化五个阶段。

类加载器

java自带三类类加载器

  • Bootstrap ClassLoader :最顶层的加载器,主要加载核心类库。
  • Extention ClassLoader :扩展的类加载器
  • Appclass Loader:也称为SystemAppClass。 加载当前应用的classpath的所有类。

这个三个类加载器的顺序:Bootstrap ClassLoader > Extention ClassLoader > Appclass Loader

层次关系图

类加载的三种方式

  • 通过命令行启动应用时由JVM初始化加载含有main()方法的主类。
  • 通过Class.forName()方法动态加载,会默认执行初始化块(static{})。
  • 通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。

看一下为什么会这样,通过看源码我们知道:Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader); 第2个boolean参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。 一旦初始化,就会触发目标对象的 static块代码执行。而ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false),由上面介绍可以,不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行。

双亲委派原则

它的工作流程是: 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

看一下classLoader中的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
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

现在对双亲委派的原理是不是很清楚了

那采用双亲委派的好处是:假如加载java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:可以避免重复加载,父类已经加载了,子类就不需要再次加载更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。

自定义类加载器

主要有两种方式

  • 遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
  • 破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。

通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。

我们看一下实现步骤

  • 创建一个类继承ClassLoader抽象类
  • 重写findClass()方法
  • 在findClass()方法中调用defineClass()

代码实现一下:


public class TestFile {
    public void loadFile() {
        System.out.println("加载指定路径下的class文件加载进来的");
    }
}

public class UserClassLoader extends ClassLoader {
        private String classPath;

        public UserClassLoader (String classPath) {
            this.classPath = classPath;
        }

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); 
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
    };
}

public class TestClassLoad {
    public static void main(String args[]) throws Exception {
        //TestFile .class目录在D:/Test/com/pch/test下
        UserClassLoader classLoader = new UserClassLoader ("D:/Test");

        Class clazz = classLoader.loadClass("com.pch.test.TestFile");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("loadFile", null);
        method.invoke(obj, null);
    }
}

破坏双亲委派模型

上面已经提到是双亲委派模型并不是一个强制性约束,我们可以通过自定义类加载器实现的方式来破坏它。到目前为止,双亲委派模型主要出现过三次较大规模的“被破坏”情况。

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前–即JDK1.2发布之前。由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则是JDK1.0时候就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的proceted方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。

双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢? 这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办? 为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!