微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

JVM-3 类加载机制上

最近写SDK代码遇到SPI,这其中涉及到类加载器的概念,又重新温习了一下,
考虑到类加载器一直没有系统地梳理一边,趁热总结一下。

1.引言

我们开发的java代码被javac编译为class文件后,需要加载到jvm内存里,才可以发挥作用。
这个加载有点像的概念:虚拟机是个运行的程序,不断地读取class文件,然后输出对应的操作。
说到底,虚拟机就是一个由c/c++写出来的软件,在不同的平台上有不同的实现,但功能都相同–符合jvm规范。
通过jvm约定规范了class文件的结构,即不同的虚拟机实现都可以读懂同一份class文件
从这个角度也能说明java跨平台的本质实现。
这个过程中有个很重要的过程类加载,这个“读”操作除了进行读,还进行了增强;
在读之前先进行了一系列校验,在读之后,又对读取的内容进行按部就班地加工。

未接触到虚拟机关于类加载器部分规范之前,我们可以猜想这样的设计:
方案1:将类加载器放在程序运行开始的时候,程序运行之初就加载所有的类,运行期间不允许加载;
方案2:在运行期间动态加载
方案1在启动时,做了大量的类加载操作,任务太重,占据大量内存和cpu资源;
但是运行时相对较快;
方案2在启动时很慢,保留了大量资源,在需要时才加载,
运行时因为类加载过程会消耗系统资源;
且方案二有个天生的有点,动态加载;如果不限定加载方式,那么可以从任何地方读取class文件(或者说类描述信息的载体)。
事实上jvm规范就是采用的第二种。

2.定义

“JVM把描述类的数据从class文件加载到内存,并对这些数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程被称为jvm的类加载机制” ——from 《JVM规范》。
java天生的动态扩展属性就来源于运行期间对类的动态加载。因此,编写一个面向接口的程序,可以等运行时才确定这个接口的实现类,且这个实现类可以从任何地方加载进Jvm内存。

3.类加载整个过程

类的整个生命周期包括以下几个部分,JVM规范规定了这些步骤大开始时间,但是没有规定必须串行执行,因此在实际的类加载过程中,步骤执行时段可以交叉。

在这里插入图片描述

(1)加载:
1)通过类的全限定名来获取定义此类的二进制字节流
2)将这个二进制流代表的类数据转化为方法区的运行时数据结构;
3)在内存中生成一个代码此类的Class类型对象,作为这个类的访问入口;

对于第一步,虚拟机规范并没有规定必须去哪里加载类,只要求要得到二进制字节流
因此加载路径可以多样化:

1)从zip包中获取jar包,war包;
2)从网络上获取;
3)运行时生成:动态代理
4)从其他文件生成:每个JS文件最终都会生成一个Servlet类的class文件
5)从数据库读取
6)从加密文件获取:通过编译时加密+加载时解密,实现对class文件的反编译保护。

(2) 验证
1)class文件文本格式校验
魔数cafebabe校验,根据主次版本号确定该虚拟机是否可以处理这个class,常量池是否有不被支持的类型等

2)元数据校验
进行的是语义分析,主要涉及类的继承关系:是否继承了不允许被继承的类,
如果该类不是抽象类,是否实现了父类所有的抽象方法

3)字节码验证
进行数据流分析和控制流分析。

4)符号引用验证
校验符号引用能否正确转化为直接引用,例如符号引用能否通过字符串的全限定名找到对应的类等。

(3)准备
给静态字段分配内存,设初始零值

(4) 解析1
将可以确定的符号引用解析为直接引用的过程,即确定方法调用的版本,是个静态分派

(5)初始化
执行编译器生成的类构造器方法()
收集了所有static变量的赋值动作和static代码块(按照在java中的顺序)
注意:静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的只能赋值,而不能访问;
这是代码规范中建议把static块放在文件底部的根本原因。

(6)解析2:
直到运行时才能确定方法调用版本,运行时解析,是个动态分派。

(7)使用:
类型使用的入口是方法区中指向类信息的class对象

(8)卸载:
类的卸载的条件比较苛刻;

1)类型A所有的对象已经被gc,
2)加载类型A的类加载器已经被gc,

因此大部分类被加载进内存后,在整个jvm进程结束前都不会被GC,因为引导、扩展、系统类加载器在整个生命周期中都不会被卸载。

4.类的初始化时机

JVM对于什么时候开始“加载”或者“连接”没有规定,但是严格规定了必须立刻进行类的"初始化"的六种情况:

1)遇到 new,getstatic,putstatic,invokestatic指令时:即创建对象时,读写静态变量或者调用静态方法时。
2)使用反射时
3)初始化类时,如果父类没有初始化,先初始化父类
4)虚拟机启动的main方法所在的类(主类)
5)jdk7提供的动态语言中:MethodHandle实例解析出的方法句柄涉及静态方法时,需要先初始化这个方法句柄对应的类。
6) jdk8中接口方法(被default修饰的接口方法),如果这个接口的实现类要初始化时,这个接口要先初始化。

5.类加载器介绍

JVM启动过程中涉及类加载器类的加载和初始化过程,以windows下的JVM为例:

(1) java.exe启动,找到JRE,加载jvm.dll,启动JVM并进行初始化;
(2) jvm初始化Bootstrap类加载器,加载核心类
(3) Bootstrap类加载器加载Extension类加载器,Extension类加载器加载扩展类。
(4) Extension类加载器加载System类加载器,System类加载器在classpath中加载主类App.class
(5) 执行App.class的main方法,整个程序启动成功;
(6) 在运行的过程中使用System类加载器加载类;也可使用System类加载器加载自定义类+通过自定义加载器加载类进JVM;

在上述流程中涉及到3个JVM提供的类加载器,

(1)引导类加载器(Bootstrap ClassLoader)
负责加载JDK中的核心类库,
<JAVA_HOME>/lib 以及 -Xbootclasspath参数指定的路径中存放的jar包
且必须时可识别的:按照文件名识别,不能识别的不会被加载。
如:rt.jar等,String、Integer等类型都是有引导类加载器加载的。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

(2)扩展类加载器(Extension ClassLoader)
主要负责加载Java的扩展类库,
认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包
由java.ext.dirs系统属性指定的jar包.
允许用户将具有通用性的类库放到ext目录下,以扩展Java SE的功能;

(3)系统类加载器(App ClassLoader)
主要负责加载用户类路径classpath
或者java.class.path系统属性
或者CLAsspATH操作系统属性所指定的JAR类包和类路径.
如果开发者没有使用自定义加载器,自己开发的类都由这个类加载器加载;

6.双亲委派机制

双亲委派机制时在jdk1.2以后引入的。

6.1 父子关系

其中引导类加载器是jvm提供的,使用c++语言实现;
其他类加载器都是继承于java.lang.classloader,因此都有一个parent属性,用来指向父加载器(组合关系而不是继承关系)
其中:

启动类加载器,由C++实现,没有父类
拓展类加载器,父类加载器为null,表示父加载器是引导类;
系统类加载器,父类加载器为ExtClassLoader
自定义类加载器,父类加载器为系统类加载器。

6.2 双亲委派模型

(1) 当一个类加载器A收到一个加载请求时,自己先不加载,直接委托父类B去加载,B也会委托自己的父类C,一层层上传,直到引导类加载器。
(2) 如果父类可以加载,替代子类加载,返回成功;如果父类不可以加载,子类自己加载。

涉及的代码如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 检查这个classsh是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果有父类的加载器委托父类加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //如果父类的加载器为空,则当前加载器是bootStrap
                    //bootStrapClassloader比较特殊无法通过get获取
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {}
            if (c == null) {
                //如果bootstrap没有加载过,则递归回来,尝试自己去加载
                long t1 = System.nanoTime();
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClasstime().addelapsedtimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

线程上下文类加载器,命名空间,自定义类加载器,tomcat对类加载器的使用与热部署等内容放在JVM-3 类加载机制(下)中整理。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐