20200405——java之jvm JVM执行的子系统 三

Class类的本质

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class 文件是一组以8位字节为基础单位的二进制流。

Class文件格式

数据项目严格按照固定顺序存储在Class文件中,数据之间无空隙。Class文件格式采用一种类似于的伪结构来C语言结构体存储数据,这种伪结构中只有两种数据类型:无符号数和表,整个Class文件本质上就是一张表。

无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,

ClassFile { 
    u4 magic;  // 魔法数字,表明当前文件是.class文件,固定0xCAFEBABE
    u2 minor_version; // 分别为Class文件的副版本和主版本
    u2 major_version; 
    u2 constant_pool_count; // 常量池计数 常量池内容不一样的要计数
    cp_info constant_pool[constant_pool_count-1];  // 常量池内容
    u2 access_flags; // 类访问标识
    u2 this_class; // 当前类
    u2 super_class; // 父类
    u2 interfaces_count; // 实现的接口数
    u2 interfaces[interfaces_count]; // 实现接口信息
    u2 fields_count; // 字段数量
    field_info fields[fields_count]; // 包含的字段信息 
    u2 methods_count; // 方法数量
    method_info methods[methods_count]; // 包含的方法信息
    u2 attributes_count;  // 属性数量
    attribute_info attributes[attributes_count]; // 各种属性
}

魔数

大多数情况下,我们都是通过扩展名来识别一个文件的类型的,比如我们看到一个.txt类型的文件我们就知道他是一个纯文本文件。但是,扩展名是可以修改的,那一旦一个文件的扩展名被修改过,那么怎么识别一个文件的类型呢。这就用到了我们提到的“魔数”。在Java中我们用前四个字节来表明文件的格式Class文件格式必须为0xCAFEBABE。

主次版本号

第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

类的加载机制

在这里插入图片描述
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。

其中验证、准备、解析3个部分统称为连接(Linking)。

在初始化阶段,虚拟机严格规定了有且只有5种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):

遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候

使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

当使用JDK
1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化,跟1有点类似。

加载
要判断文件格式是否OK,是否可以找到文件。
虚拟机需要完成以下3件事情:

通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在堆内存中生成一个代表这个类的java.lang.Class对象(也就是反射),作为方法区这个类的各种数据的访问入口。
平常我们认识到的是一个class 然后new 出object。但是class 也是一个object。 这个object由JVM通过java.lang.Class来统一给我们生成。

验证
是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。大致的工作如下:

验证Java加载进内存的二进制文件是否符合JVM以及Java规范,并且不会危害虚拟机的自身安全。比如说符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问、类中的字段、方法是否与父类产生矛盾……

准备

准备阶段是指准备要执行的制定的类,这包含了给这个类的静态变量数据分配内存空间,并分配初始值(仅仅是分配内存空间,具体初始化在最后一步)。
public static int age = 14这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 age 赋值为 14 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,在初始化阶段才会对 value 进行赋值。但是如果添加了final就会在这个阶段直接赋值为14。

解析

这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用转换为直接引用就发生在解析阶段,解析阶段可能在初始化前,也可能在初始化之后。

为什么要用符号引用呢?这是因为类加载之前,javac会将源代码编译成.class文件,这个时候javac是不知道被编译的类中所引用的类、方法或者变量他们的引用地址在哪里,所以只能用符号引用来表示。在解析阶段又需要根据关联上数据。

初始化

是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
虚拟机会保证一个**类的<clinit>()方法在多线程环境中被正确地加锁、同步,**如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。

例如,如果一个类中包含声明public static int age=14;那么变量age被赋值为14的过程将在初始化阶段进行,另外倘若静态变量并没有指定初值,那么JVM会自动给静态变量赋予一个初值,下表给出Java基本类型和引用变量的缺省值。

在这里插入图片描述
类加载器

前面我们说到了类加载分为7个部分,而在链接阶段我们一般是无法干预的,我们大部分干预的阶段类加载阶段(ClassLoder)。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
这里所指的相等,包括代表类的Class对象的 isAssignableFrom()方法,equals()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

ClassLoader 里面有三个重要的方法 loadClass()、findClass()和 defineClass(),平常用到的主要函数如下:

loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,找到直接返回。
如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。

getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。

双亲委派机制
在这里插入图片描述
定义:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

作用

防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载(自己写个java.lang.String试试),即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

BootstrapClassLoader(启动类加载器)

c++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

ExtClassLoader (标准扩展类加载器)

java编写,加载扩展库,如classpath中的jre ,javax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

AppClassLoader(系统类加载器)

java编写,加载程序所在的目录,如user.dir所在的位置的class

CustomClassLoader(用户自定义类加载器)

java编写,用户自定义的类加载器,可加载指定路径的class文件

发布了955 篇原创文章 · 获赞 43 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_36344771/article/details/105335330