JVM(三)类加载

编写的 Java 代码需要经过编译器编译为 class 文件(从本地机器码转变为字节码的过程),class 文件是一组以 8 位字节为基础的二进制流,这些二进制流分别以一定形式表示着魔数(用于标识是否是一个能被虚拟机接收的 Class 文件)、版本号、字段表、访问标识等内容。代码编译为 class 文件后,需要通过类加载器把 class 文件加载到虚拟机中才能运行和使用。

1、类加载步骤

类从被加载到内存到使用完成被卸载出内存,需要经历加载、连接、初始化、使用、卸载这几个过程,其中连接又可以细分为验证、准备、解析
在这里插入图片描述
(1)加载
在加载阶段,虚拟机主要完成三件事情:
① 通过一个类的全限定名(比如 com.danny.framework.t)来获取定义该类的二进制流;
② 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构;
③ 在内存中生成一个代表这个类的 java.lang.Class 对象,作为程序访问方法区中这个类的外部接口。
(2)验证
验证的目的是为了确保 class 文件的字节流包含的内容符合虚拟机的要求,且不会危害虚拟机的安全。
文件格式验证:主要验证 class 文件中二进制字节流的格式,比如魔数是否已 0xCAFEBABY 开头、版本号是否正确等。
元数据验证:主要对字节码描述的信息进行语义分析,保证其符合 Java 语言规范,比如验证这个类是否有父类(java.lang.Object 除外),如果这个类不是抽象类,是否实现了父类或接口中没有实现的方法,等等。
字节码验证:字节码验证更为高级,通过数据流和控制流分析,确保程序是合法的、符合逻辑的。
符号引用验证:对类自身以外的信息进行匹配性校验,举个例子,比如通过类的全限定名能否找到对应类、在类中能否找到字段名 / 方法名对应的字段 / 方法,如果符号引用验证失败,将抛出异常:
java.lang.NoSuchFieldError``java.lang.NoSuchMethodError
(3)准备
正式为【类变量】分配内存并设置类变量【初始值】,这些变量所使用的内存都分配在方法区。注意分配内存的对象是“类变量”而不是实例变量,而且为其分配的是“初始值”,一般数值类型的初始值都为0,char类型的初始值为’\u0000’(常量池中一个表示Nul的字符串),boolean类型初始值为false,引用类型初始值为null。
但是加上final关键字比如public static final int value=123;在准备阶段会初始化value的值为123;
(4)解析
解析是将常量池中【符号引用】替换为【直接引用】的过程。
符号引用是以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。比如在com.danny.framework.LoggerFactory 类引用了 com.danny.framework.Logger,但在编译期间是不知道 Logger 类的内存地址的,所以只能先用 com.danny.framework.Logger(假设是这个,实际上是由类似于 CONSTANT_Class_info 的常量来表示的)来表示 Logger 类的地址,这就是符号引用。
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局有关,如果有了直接引用,那引用的目标一定在内存中存在。
解析的时候 class 已经被加载到方法区的内存中,因此要把符号引用转化为直接引用,也就是能直接找到该类实际内存地址的引用。
(5)初始化
在准备阶段,已经为类变量赋了初始值,在初始化阶段,则根据程序员通过程序定制的主观计划去初始化类变量的和其他资源,也可以从另一个角度来理解:初始化阶段是执行类构造器 () 方法的过程,那 () 到底是什么呢?
我的理解是,java 在生成字节码时,如果类中有静态代码块或静态变量的赋值操作,会将类构造器 () 方法和实例构造器 () 方法添加到语法树中(可以理解为在编译阶段自动为类添加了两个隐藏的方法:类构造器 ——() 方法和实例构造器 ——() 方法,可以用 javap 命令查看),() 主要用来构造类,比如初始化类变量(静态变量),执行静态代码块 (statis{}) 等,该方法只执行一次;() 方法主要用来构造实例,在构造实例的过程中,会首先执行 (),这时对象中的所有成员变量都会被设置为默认值(每种数据类型的默认值和类加载准备阶段描述的一样),然后才会执行实例的构造函数(会先执行父类的构造方法,再执行非静态代码块,最后执行构造函数)。

public class Parent {
    static {
        System.out.println("Parent-静态代码块执行");
    }

    public Parent() {
        System.out.println("Parent-构造方法执行");
    }

    {
        System.out.println("Parent-非静态代码块执行");
    }
}

public class Child extends Parent{
    private static int staticValue = 123;
    private int noStaticValue=456;

    static {
        System.out.println("Child-静态代码块执行");
    }

    public Child() {
        System.out.println("Child-构造方法执行");
    }

    {
        System.out.println("Child-非静态代码块执行");
    }

    public static void main(String[] args) {
        Child child = new Child();
    }
}

运行结果如下:

Parent-静态代码块执行
Child-静态代码块执行
Parent-非静态代码块执行
Parent-构造方法执行
Child-非静态代码块执行
Child-构造方法执行

上面的例子中可以看到一个类从加载到实例化的过程中,静态代码块、构造方法、非静态代码块的加载顺序。无法看到静态变量和非静态变量初始化的时间,静态变量的初始化和静态代码块的执行都是在类的初始化阶段 (()) 完成,非静态变量和非静态代码块都是在实例的初始化阶段 (()) 完成。

2、类加载器

(1)类加载器的作用
•加载 class:类加载的加载阶段的第一个步骤,就是通过类加载器来完成的,类加载器的主要任务就是 “ 通过一个类的全限定名来获取描述此类的二进制字节流 ”,在这里,类加载器加载的二进制流并不一定要从 class 文件中获取,还可以从其他格式如zip文件中读取、从网络或数据库中读取、运行时动态生成、由其他文件生成(比如 jsp 生成 class 类文件)等。
从程序员的角度来看,类加载器动态加载class文件到虚拟机中,并生成一个 java.lang.Class 实例,每个实例都代表一个 java 类,可以根据该实例得到该类的信息,还可以通过newInstance()方法生成该类的一个对象。
•确定类的唯一性:类加载器除了有加载类的作用,还有一个举足轻重的作用,对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在 Java 虚拟机中的唯一性。也就是说,两个相同的类,只有是在同一个加载器加载的情况下才 “ 相等 ”,这里的 “ 相等 ” 是指代表类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括 instanceof 关键字对对象所属关系的判定结果。
(2)类加载器的分类
以开发人员的角度来看,类加载器分为如下几种:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)和自定义类加载器(User ClassLoader),其中启动类加载器属于 JVM 的一部分,其他类加载器都用 java 实现,并且最终都继承自 java.lang.ClassLoader。
① 启动类加载器(Bootstrap ClassLoader)是由 C/C++ 编译而来的,看不到源码,所以在 java.lang.ClassLoader 源码中看到的 Bootstrap ClassLoader 的定义是 native 的 private native Class findBootstrapClass(String name);。启动类加载器主要负责加载 JAVA_HOME\lib 目录或者被 -Xbootclasspath 参数指定目录中的部分类,具体加载哪些类可以通过 System.getProperty(“sun.boot.class.path”) 来查看。
② 扩展类加载器(Extension ClassLoader)由 sun.misc.Launcher.ExtClassLoader 实现,负责加载 JAVA_HOME\lib\ext 目录或者被 java.ext.dirs 系统变量指定的路径中的所有类库,可以用通过 System.getProperty(“java.ext.dirs”) 来查看具体都加载哪些类。
③ 应用程序类加载器(Application ClassLoader)由sun.misc.Launcher.AppClassLoader 实现,负责加载用户类路径(我们通常指定的 classpath)上的类,如果程序中没有自定义类加载器,应用程序类加载器就是程序默认的类加载器。
④ 自定义类加载器(User ClassLoader),JVM 提供的类加载器只能加载指定目录的类(jar 和 class),如果我们想从其他地方甚至网络上获取 class 文件,就需要自定义类加载器来实现,自定义类加载器主要都是通过继承 ClassLoader 或者它的子类来实现,但无论是通过继承 ClassLoader 还是它的子类,最终自定义类加载器的父加载器都是应用程序类加载器,因为不管调用哪个父类加载器,创建的对象都必须最终调用 java.lang.ClassLoader.getSystemClassLoader() 作为父加载器,getSystemClassLoader() 方法的返回值是 sun.misc.Launcher.AppClassLoader 即应用程序类加载器。
(3)ClassLoader 与双亲委派模型
下面看一下类加载器 java.lang.ClassLoader 中的核心逻辑 loadClass() 方法:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 检查该类是否已经加载过
            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) {
                }

                if (c == null) {//如果上面用父加载器还没加载到类,就自己尝试加载
                    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;
        }
    }

这段代码的主要意思就是当一个类加载器加载类的时候,如果有父加载器就先尝试让父加载器加载,如果父加载器还有父加载器就一直往上抛,一直把类加载的任务交给启动类加载器,然后启动类加载器如果加载不到类就会抛出 ClassNotFoundException 异常,之后把类加载的任务往下抛,如下图:
在这里插入图片描述
通过上图的类加载过程,就引出了一个比较重要的概念——双亲委派模型,如下图展示的层次关系,双亲委派模型要求除了顶层的启动类加载器之外,其他的类加载器都应该有一个父类加载器,但是这种父子关系并不是继承关系,而是像上面代码所示的组合关系。
在这里插入图片描述
双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会加载类,而是把这个请求委派给它上一层的父加载器,每层都如此,所以最终请求会传到启动类加载器,然后从启动类加载器开始尝试加载类,如果加载不到(要加载的类不在当前类加载器的加载范围),就让它的子类尝试加载,每层都是如此。
那么双亲委派模型有什么好处呢?最大的好处就是它让 Java 中的类跟类加载器一样有了 “ 优先级 ”。前面说到了对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在 Java 虚拟机中的唯一性,比如 java.lang.Object 类(存放在 JAVA_HOME\lib\rt.jar 中),如果用户自己写了一个 java.lang.Object 类并且由自定义类加载器加载,那么在程序中是不是就是两个类?所以双亲委派模型对保证 Java 稳定运行至关重要。

转自:https://gitbook.cn/books/5b6ee92eb8469638412c8b35/index.html

基于bert实现关系元组抽取python源码+数据集+项目说明.zip基于bert实现关系元组抽取python源码+数据集+项目说明.zip基于bert实现关系元组抽取python源码+数据集+项目说明.zip基于bert实现关系元组抽取python源码+数据集+项目说明.zip基于bert实现关系元组抽取python源码+数据集+项目说明.zip 个人大四的毕业设计、课程设计、作业、经导师指导并认可通过的高分设计项目,评审平均分达96.5分。主要针对计算机相关专业的正在做毕设的学生和需要项目实战练习的学习者,也可作为课程设计、期末大作业。 [资源说明] 不懂运行,下载完可以私聊问,可远程教学 该资源内项目源码是个人的毕设或者课设、作业,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96.5分,放心下载使用! 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),供学习参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值