作业6:Java虚拟机类加载机制

一、概述

1、定义

虚拟机类加载机制:把类的数据从Class文件加载进内存,并对数据作校验、转换解析和初始化,最终形成可被JVM直接使用的Java类型。

2、与C/C++的不同

  • Java不在编译时进行连接工作,Java类型的加载和连接过程在程序运行期间完成。
  • 增加性能开销,但为Java应用程序提供高度的灵活性和编程的易用性。

二、类加载的时机

1、类加载的生命周期

  • 加载
  • 连接
    • 验证
    • 准备
    • 解析
  • 初始化
  • 使用
  • 卸载

2、主动引用:类初始化有且只有的四种情况

(1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令。

  • 使用new关键字实例化对象
  • 读取或设置一个类的静态变量(非final)
  • 调用一个类的静态方法

(2)使用java.lang.reflect包的方法对类进行发射调用的时候,如果类没有进行初始化,则先触发初始化。

(3)初始化一个类时,发现其父类未初始化,先触发父类的初始化,

(4)JVM启动时,用户指定一个执行的主类,虚拟机会先初始化主类。

3、被动引用:不触发初始化

(1)通过子类引用父类的静态字段。

(2)通过数组定义引用类。

(3)常量在编译阶段存入调用类的常量池中,本质上没有直接引用定义常量的类。

4、接口的加载过程与类加载过程

(1)接口没有 “static {}” 来输出初始化过程,但编译器仍然会为接口生成"<clinit>()''类构造器,用于初始化接口中的成员变量。

(2)接口不要求父类接口全部初始化完成,只要在真正使用到父类接口的时候才会初始化。

三、类加载的过程

1、加载

(1)加载过程,虚拟机完成的三件事情
  • 通过类的全限定名获取类的二进制字节流
  • 字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在Java堆中生成代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口
(2)获取类的二进制字节流多种方式
  • 从 ZIP、JAR、EAR、WAR包中获取
  • 从网络获取,如 Applet 应用
  • 运行时计算生成,如:动态代理
  • 由其他文件生成,如:JSP 应用
  • 从DB中获取
(3)注意点:加载阶段与连接阶段部分内容交叉进行,加载未完成,连接阶段就开始了。

2、验证

(1)目的:确保Class文件的字节流中包含的信息符合JVM的要求,并且不会虚拟机自身的安全。
(2)四个阶段的检验过程
  • 文件格式校验:验证字节流是否符合Class文件格式的规范,并且能被当前版本的JVM处理。
  • 元数据验证:对字节码描述的信息进行语义分析
  • 字节码验证:进行数据流和控制流分析,确保类的方法在运行时不会危害JVM的安全
  • 符号引用验证:解析阶段中,在符号引用转化为直接引用的时候。
(3)文件格式验证
  • 是否以魔数0xCAFWBABE开头
  • 主次版本号是否在JVM处理范围
  • 常量池的常量是否由不被支持的常量类型
  • 指向常量的各种索引值中是否指向不存在的常量
  • CONSTANT_Utf8_info类型的常量中是否不符合UTF8编码的数据
  • ......
(4)元数据验证
  • 类是否有父类(除去Object)
  • 类是否继承由final修饰的类
  • 如果类不是抽象类,是否实现了父类或接口的所有方法
  • 类中字段、方法是否与父类产生了矛盾
  • ......
(5)字节码验证
  • 保证任意时刻操作数栈的数据类型与指令代码序列能匹配(操作数栈有int类型的数据,使用却按long类型来加载入本地变量表中)
  • 保证跳转指令不会跳转到方法体意外的字节码指令上
  • 保证方法体中的类型转换是有效的
  • ......
  • 优化措施:StackMapTable属性,描述方法体中的所有基本块开始时本地变量表和操作栈应用的状态。
(6)符号引用验证
  • 符号引用中通过字符串描述的全限定名是否找到对应的类
  • 指定类中是否存在符号方法的字段描述符以及简单名称所描述的方法字段

    扫描二维码关注公众号,回复: 1947130 查看本文章
  • 符号引用中的类、字段和方法的访问性是否可被当前类访问
  • ......
  • 可能抛出的异常:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError

3、准备

目的:为类变量分配内存(方法区)并设置类变量初始值
  • 只对类变量进行内存分配
  • 初始化值为数据类型的零值
数据类型 零值
int 0
long 0L
short (short)0
char '\u0000'
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

4、解析

(1)目的:JVM将常量池中的符号引用替换为直接引用
  • 符号引用:以一组符号所引用的目标,可以是任何类型的字面量;与JVM实现的内存布局无关。
  • 直接引用:可以是直接指向目标的指针、相对偏移量或一个能直接定位到目标的句柄;与JVM实现的内存布局相关。
(2)解析动作的目标对象
  • 接口:对应常量池的CONSTANT_Class_info
  • 字段:对应常量池的CONSTANT_Fieldref_info
  • 类方法:对应常量池的CONSTANT_Methodref_info
  • 接口方法:对应常量池的CONSTANT_InterfaceMethodref_info
(3)解析的时机:下面的字节码指令前
  • anewarray、multianewarray、new
  • checkcast、instanceof
  • invokeinterface、invokestatic、invokevirtual
  • putfield、getfield、putstatic、getstatic
(4)类或接口的解析

类解析

(5)字段解析

字段解析

(6)类方法解析

类方法解析

(7)接口方法解析

接口方法解析

5、初始化

(1)目的:初始化类变量和其他资源(<clinit>()的执行过程)
(2)<clinit>()的执行过程细节和特点
  • <clinit>()由编译期自动收集类中素有类变量的赋值动作静态语句块中的语句合并产生的。
  • <clinit>()与类的构造函数(实例构造器<init>())不同,不需要显示调用父类构造器。
  • 父类的<clinit>()方法先执行。
  • <clinit>()方法非必需,如果静态代码块和类变量赋值,可以不生成。
  • 父接口的<clinit>()不需要比子类先执行
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境下被正确地加锁和同步

四、类加载器

1、类与类加载器

比较两个类是否相等

  • 使用相同类加载器加载
  • Class对象的equals()、isAssignableFrom()、isInstance()、instanceof关键字
public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(filename);
                    if (is == null)
                        return super.loadClass(name);
                    byte[] bytes = new byte[is.available()];
                    is.read(bytes);
                    return defineClass(name, bytes, 0, bytes.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };
        // 由自定义类加载器加载
        Object obj = classLoader.loadClass("ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());
        // 由系统类加载器加载
        System.out.println(obj instanceof ClassLoaderTest);
    }
}

2、双亲委派模型

(1)JVM角度的类加载器的种类
  • 启动类加载器(Bootstrap ClassLoader),由C++语言实现,JVM的一部分
  • 其他类加载器,由Java语言实现,独立于JVM之外,且继承抽象类java.lang.ClassLoader
(2)程序员角度的类加载器种类
  • 启动类加载器(Bootstrap ClassLoader):存放在JAVA_HOME\lib目录或-Xbootclasspath指定路径,由JVM识别类库加载到虚拟机内存,开发者无法干预。
  • 拓展类加载器(Extension ClassLoader):sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA_HOME\lib\ext目录中或被java.ext.dirs系统变量所制定的路径,开发者可以拓展该类加载器。
  • 应用程序类加载器(Application ClassLoader):sum.misc.Laucher$AppClassLoader实现,getSystemClassLoader()返回值,即系统类加载器。负责加载用户路径(ClassPath)上所指定的类库,开发者能直接使用。
(3)类加载之间的关系

1531031337533

  • 采用组合模式,不采用继承方式。
  • 一个类加载器收到类加载请求,先委派给父类加载器去完成,只有父类都无法加载,子加载器才会尝试自己加载。

猜你喜欢

转载自www.cnblogs.com/linzhanfly/p/9279997.html