JVM第一弹--类加载子系统

JVM与Java体系结构

Jvm的架构模型

区分栈的指令集架构和寄存器的指令集架构

Java编译器的输入的指令流基本上是基于栈的指令集架构

另一种的指令集架构则是基于寄存器的指令集架构

两种架构之间的区别

  • 基于栈的架构:

  1. 设计和实现更加的简单,适用于资源受限的系统

  2. 避开了寄存器分配的难题,使用零地址指令方式分配

  3. 指令流中指令大部分是零地址(即不依赖地址)的指令,执行过程流程依赖栈结构。指令集小,编译器容易实现

  4. 不需要硬件的支持,可移植性好,更好的实现了跨平台

  • 基于寄存器的架构:

  1. 典型x86二进制指令集,比如传统的PC以及Android的Davlik虚拟机

  2. 指令集的架构完全依赖硬件,可移植性较差

  3. 性能优秀,执行效率更高

  4. 花费更少的指令去完成操作

  5. 基于寄存器的指令集架构,依赖地址指令来完成指令操作

总结

由于跨平台的设计,Java的指令都是根据栈来设计。不同的CPU架构不同,不能设计为基于寄存器的。有点事。跨平台,指令集小,编译器容易实现。缺点,指令多,不能直接和CPU进行交互性能下降。

Jvm的生命周期


虚拟机的启动

  1. Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建了一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。

虚拟机的执行

  1. 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序

  2. 程序开始执行时他才执行,程序结束时他就停止

  3. 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程

虚拟机的退出

  1. 程序正常的执行结束

  2. 程序在执行过程中遇到异常或错误而终止

  3. 操作系统的错误导致Java虚拟机的进程终止

  4. 线程调用Runtime类或者System类的exit方法,或者Runtime的halt方法。

下图是JVM的架构图:

image

类加载子系统

类加载器子系统的作用

  • 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件的开头有特定的文件标识。

  • ClassLoader只负责class文件的加载,至于它是否可以运行呢,是由Execution Engine即执行引擎进行决定的。

  • 加载完成的类信息,通常存放在一块称为方法区的内存空间内。方法区中除了类信息,还会存放运行时的常量信息,可能包括字符串的字面量和数字常量。(这部分常量信息是Class文件中常量池部分的内存映射)

类加载过程

  • Loading:

       加载.class文件的方式:

    • 从本地系统直接加载

    • 通过网络进行获取,典型场景:Web Applet

    • 从压缩包中进行读取,例如jar/war

    • 运行时计算生成,例如:动态代理

    • 由其他文件生成,例如:jsp

    • 等等

    1. 通过一个类的全限定列名获取定义此类的二进制字节流

    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的数据访问入口

  • Linking

    验证(Verify):

    1- 目的在于确保Class文件中的字节流包含的信息符合当前虚拟机的要求,保证被加载类的正确性。
    2- 主要包含四大验证:文件格式/元数据/字节码/符号引用

    准备(Prepare):

    1- 为类变量分配内存,并且设置该变量的默认初始值。
    2- 这里不包含用final修饰的static,即常量值,因为final修饰的在编译期间就会进行分配,准备阶段会显示的进行初始化。
    3- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。

    解析(resolve):

    1- 将常量池中的符号引用转换为直接引用的过程
    2- 事实上,解析操作往往伴随着JVM在执行完初始化之后,再执行
    3- 符号引用:一组符号来描述所引用的目标。 符号引用的字面量形式--明确的定义在《Java虚拟机规范》的Class文件格式中。
    4 直接引用:直接指向目标的指针/相对偏移量或一个间接定位到目标的句柄。
    5- 解析的动作主要针对类或者接口/字段/类方法/接口方法/方法类型等。  对应常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info/CONSTANT_Methodref_info等。
  • Initialization

    1.初始化阶段就是执行类构造器clinit()的过程。此方法不需要定义,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来的。构造器方法中指令按照语句在源文件中出现的顺序进行执行

    2.clinit()不同于类的构造器,构造器是虚拟机视角下的init().若被初始化的类有父类,JVM会保证子类clinit()执行前,父类clinit()已经执行完毕。同时虚拟机保证一个类clinit()方法在多线程下被同步加锁。

类加载器

分类:

  1. JVM支持两种类型的类加载器,引导类加载器(Bootstrap Classloader)/自定义类加载器(User-Defined ClassLoader)

  2. Java虚拟机规范将所有派生于ClassLoader的类加载器全部划分为自定义类加载器

常见的类加载器:

Bootstrap ClassLoader

Extension ClassLoader

System ClassLoader

代码理解:

package com.jvm;

/**
 * @ClassName ClassLoaderTest
 * @Description TODO:测试类加载器
 * @Author 86133
 * @Date 2020/10/3 22:02
 * @Version 1.0
 **/
public class ClassLoaderTest {
    public static void main(String[] args) {
        // 获取系统的类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
        // 通过系统类加载器获取上层扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@1b6d3586
        // 再通过扩展类加载器获取上层
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader); // null
        // 获取自定义类ClassLoaderTest的类加载器
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
        // 获取String类的类加载器
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1); // null
        /**
         * 得出结论:Java的核心类库都是通过引导类加载器进行加载的,自定义类默认是通过系统类加载器进行加载
         * 而引导类加载器是通过c/c++实现
         */

    }
}

自定义类加载器

Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要的时候,我们还可以自定义类加载器,来定制类的加载方式

为什么需要自定义类加载器?
  1. 隔离加载类

  2. 修改类加载的方式

  3. 扩展加载源

  4. 防止源码泄露

自定义类加载器的实现步骤:
  1. 继承ClassLoader

  2. jdk1.2之前需要重写loadClass()方法/之后则建议将自定义类加载的实现逻辑写在findClass()方法中

  3. 在编写的时候,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,可以避免自己实现findClass()逻辑,更加简洁的实现。

关于ClassLoader:

它是一个抽象类,除了引导类加载器,其余所有的类加载器都继承自它

常用方法详解:

1. public Class<?> loadClass(String name) throws ClassNotFoundException:
公有调用加载类的入口,内部是调用了loadClass(String name,boolean resolve)方法
2. protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException:
通过双亲委托机制实现的加载类的实现。
3. protected Class<?> findClass(String name) throws ClassNotFoundException
查找类的方法,自定义类加载器推荐重写此方法。
4. protected final Class<?> defineClass(String name, byte[] b, int off, int len):
将二进制字节码转换成Class的一个实例。

获取ClassLoader的途径:

双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说,当需要使用该类的时候呢,才会将其class文件加载到内存生成class对象。

而加载某个类的class文件时,虚拟机采用的是双亲委派模型,即把请求交由父类处理,它是一种任务委派的模式。

工作原理

优势
  1. 避免类的重复加载

  2. 保护程序安全,防止核心的API被篡改

沙箱安全机制

双亲委派机制以及其实现了对Java核心源代码的保护,这就是沙箱安全机制

其他

JVM中标识两个class对象是否为同一个类 的必要条件:
  1. 全限定类名必须一致

  2. 加载该类的ClassLoader必须相同

对类加载器的一个引用:

JVM必须知道一个类是由引导类加载器加载的还是用户类加载器进行加载的。

如果是由用户类加载器进行加载的,那么JVM会将这个类加载器的引用作为类型信息的一部分保存在方法区。

当解析一个类型到另一个类型的引用的时候,JVM要保证这两个类加载器的引用是相同的。

类的主动使用和被动使用:

Java类主动使用情况,会导致类的初始化:

  1. 创建类的实例

  2. 访问某个类或接口的静态变量,或者对该静态变量赋值

  3. 调用类的静态方法

  4. 反射(如Class.forName(“com.bunny.Test”))

  5. 初始化一个类的子类

  6. Java虚拟机启动时被表明为启动类的类(JavaTest)

其他使用java类方式,都可以被看作是被动使用,都不会导致类的初始化。

啊哈~ 至此,有关于类加载的相关知识介绍完毕,我们会在后续的章节继续加深巩固。

posted on 2020-10-04 23:41  王泱钧的Java小站  阅读(126)  评论(0)    收藏  举报

导航