JavaClass文件结构解析、JVM类的加载过程、类加载器(双亲委派模型)

JavaClass文件结构解析

Class文件

概念

  1. Class文件是JVM执行引擎的数据入口,也是Java技术体系的基础构成之一;
  2. class文件不同于,C/C++直接将程序代码编译成"01…"二进制格式的机器码形式,直接运行在操作系统上;而class文件是将程序代码,编译成与操作系统和机器指令集无关的格式,运行在隔离硬件平台的虚拟机之上;
  3. Class文件是经过前端编译(如javac编译)后生成的文件(以.class为后缀),一个Class文件对应一个类或一个接口。它的存储内容称为字节码,它包含了JVM指令集和符号表以及其他辅助信息。
  4. Class文件是一组以8位字节(8-bit bytes)为基础的二进制字节流构成,8位以上的数据以大端(Big-Endian)的高位在前的顺序进行存储,中间没有添加任何分隔符。
  5. JVM加载的Class文件不一定来磁盘,还可以来自网络数据,甚至在运行时直接编译代码字符串生成 。
  6. 优点:
    1. 平台无关性一次编写,到处运行。它以虚拟机作为执行引擎,与操作系统平台无关。
    2. 语言无关性虚拟机只与字节码关联,不与任何语言绑定。任何语言只要可以编译成JVM规范规定的字节码格式,就都可以在虚拟机上运行,如:Kotlin,Clojure,Groovy,Scala,Jython,JRuby,JavaScript等可以通过不同的编译器编译成字节码文件。

Class文件的结构

Class文件结构根据《Java虚拟机规范》定义的结构进行存储,类似于C语言的结构体的伪结构。
伪结构中各个数据都有相应的含义,并且各个数据项必须严格按规定的先后顺序排列,它们之间没有任何分隔符和空隙。结构及说明如下:
在这里插入图片描述

Idea中字节码分析插件:jclasslib Bytecode Viewer

文件结构的数据类型:
  1. 无符号数: 属于基本的数据类型,以u1、u2、u4、u8来表示一个字节、两个字节…的无符号数; 无符号数用来描述数字、索引引用、数量值或UTF-8编码构成的字符串值
  2. 表: 表是由多个无符号数或其他表作为数据项构成的复合数据类型,一般以"_info"结尾;表用来描述有层次关系的复合结构的数据;表中的项长度不固定;整个Class文件本质上就是一个表。
全限定名称、非全限定名称、描述符以及签名:

它们在Class文中结构中的字段、方法、类、接口都可能用到的表示,都存储在常量池

  1. 全限定名称:全限定名是在整个JVM中的绝对名称,可以表示Class文件结构中的类或接口的名称,如:类 Thread 的正常的二进制名是"java.lang.Thread",在 Class 文件面,对该类的引用是通过来一个代表字符串"java/lang/Thread"的CONSTANT_Utf8_info 结构来实现的。
  2. 非全限定名称方法名、字段名和局部变量名都被使用非全限定名进行存储, 如,“java.lang.Object"表示为"Object”。;
  3. 描述符:描述符是一个描述字段或方法的类型的字符串。描述符中基本类型表示字符如下
    在这里插入图片描述

3.1. 字段描述符:表示类、实例或局部实例变量的语法符号;
代码: private static int age = 10;
在这里插入图片描述
3.2. 方法描述符:描述一个方法所需的参数和返回值信息,即包括参数描述符和返回描值述符
代码:private static String test1(int a)
在这里插入图片描述

  1. 签名:用于描述字段、方法和类型定义中的泛型信息的字符串,这应该是JDK1.5引入泛型(类型变量或参数化类型)后的而出现的。 在Java语言中,任何类、接口、初始化方法或成员的泛型签名,如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则该字段在字段表集合中(field_info fields[fields_count])对应的字段信息(field_info),或该方法在方法表集合(method_info methods[methods_count])对应的方法信息(method_info),存在Signature属性会为它记录泛型签名信息,Signature属性存在指向CONSTANT_Utf8_info常量类型数据的索引,这样就可以找到相应的签名字符串。
    1. 类签名:把Class申明的类型信息编译成对应的签名信息;描述当前类可能包含的所有的(泛型类型的)形式类型参数,包括直接父类和父接口;代码:public class Test1<T> implements Serializable
      在这里插入图片描述

    2. 字段类型签名: 作用是将字段、参数或局部变量的类型编译成对应的签名信息, 由 JavaTypeSignature定义,包括基本类型和引用类型的签名:;

    3. 方法签名:作用是将方法中所有的形式参数的类型编译成相应的签名信息(或将它们参数化); 由 MethodTypeSignature 定义:

描述符、签名以及特征签名的区别:
  1. 描述符和签名的区别
    1. 范围不同
      1. 描述符不能描述类,所以没有类描述符;
    2. 对方法来说:
      1. 签名 = 描述符 + FormalTypeParametersopt(类型变量或参数化类型编译未擦除类型前的信息) + ThrowsSignature(抛出的异常信息);
      2. 描述符 = 参数类型 + 参数顺序 + 返回值类型;
      3. 当一个方法没有类型变量或参数化类型,也没有抛出异常时,签名和描述符是一样的,不过这时候也就没有签名信息存在了;
  2. 方法的描述符(签名)和特征签名的区别:
    1. 方法特征签名:用于区分两个不同方法的语法符号
      1. Java语言层面的方法特征签名特征签名 = 方法名 + 参数类型 + 参数顺序;
      2. JVM层面的方法特征签名特征签名 = 方法名 + 参数类型 + 参数顺序 + 返回值类型 = 方法名 + 描述符 ; 如果存在类型变量或参数化类型,还包括类型变量或参数化类型编译未擦除类型前的信息(FormalTypeParametersopt),和抛出的异常信息(ThrowsSignature),那么:特征签名 =方法名+签名
  3. Java语言重载(Overload)一个方法,需要java语言层面的方法特征签名不同,即不包括方法返回值;而Class文件中有两个同名同参数(类型、顺序都相同),但返回值类型不一样,也是允许的,可以正常运行,因为JVM层面的方法特征签名包括返回值类型
    同样的,对字段来说,Java语言规定字段无法重载,名称必须不一样;但对Class文件来说,只要两个字段描述(类型)不一样,名称一样也是可以的

Class文件结构分析

  1. 一个Java代码通过:javac -parameters -d . HellowWorld.java编译后,Linux下通过:od -t xC HelloWorld.class命令来查看Class的文件结构,也可以使用带16进制查看器的文档软件进行查看;
package com.xrl.classstructure;

// HelloWorld示例
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

程序编译后以16进制查看,它是以16进制表示的。
在这里插入图片描述
它的解析可以参考:https://blog.csdn.net/Fine____/article/details/120476629

类的加载过程

相应类加载检查过程

  1. JVM遇到new指令时,先检查指令参数是否能在常量池中定位到一个类的符号引用
    1. 如果能定位到,检查这个符号引用代表的类是否已被加载、解析和初始化过;
    2. 如果不能定位到,或没有检查到,就先执行相应的类加载过程

加载

  1. 通过一个类的全限定名获取定义此类的二进制字节流,并从各个位置(网络、磁盘等)加载到内存中,接着会为这个类在 JVM 的方法区(1.8后为元空间,在本地内存中)创建一个对应的 Class 对象中,内部采用 C++ 的 instanceKlass对象 描述 java 类;

连接

验证

验证字节码文件是否符合 JVM规范,安全性检查,它分为两个类型:
JVM规范校验:JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范;魔数、版本等验证;
代码逻辑校验::JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数等情况。

准备

对象所需内存的大小在类加载完成后便完全确定(JVM可以通过普通Java对象的类元数据信息确定对象大小);JVM 开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型

  1. 内存分配的对象:JVM为类变量(类中被static修饰的静态变量)分配内存空间,并附默认值;
  2. 初始化的类型:为类变量赋予 Java 语言中该数据类型的零值,如果类变量是被final 修饰的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成,如果是属于引用类型,那么赋值会在初始化阶段完成将常量池中的符号引用解析为直接引用;
解析

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

内存分配方式
  1. 指针碰撞:如果java中的内存是规整的,一边为用过的内存一边为空闲内存,就以中间一个指针作为边界指示器;分配内存只需向空闲那边移动指针;
  2. 空闲列表如果Java内存不是规整的:用过的和空闲的内存相互交错;需要维护一个列表,记录哪些内存可用;分配内存时查表找到一个足够大的内存,并更新列表;
  3. 应用: Java堆是否规整,由JVM采用的垃圾收集器是否带有压缩功能决定的; 所以,使用Serial、ParNew等带Compact过程的收集器时,JVM采用指针碰撞方式分配内存;而使用CMS这种基于标记-清除(Mark-Sweep)算法的收集器时,采用空闲列表方式;
线程安全问题
  1. 同步处理分配内存的动作进行同步处理:JVM采用CAS机制加上失败重试的方式,保证更新操作的原子性
  2. 本地线程分配缓冲区(TLAB): 把分配内存的动作按照线程划分在不同的空间中进行;在每个线程在Java堆预先分配一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB);哪个线程需要分配内存就从哪个线程的TLAB上分配; 只有TLAB用完需要分配新的TLAB时,才需要同步处理;
    JVM通过"-XX:+/-UseTLAB"指定是否使用TLAB;

初始化

初始化阶段就是**执行类构造器方法< clinit >()**的过程。在这个阶段会为类变量赋上我们自己设置的值,此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。构造器方法中指令按语句在源文件中出现的顺序执行。
若该类具有父类,JVM会保证子类的< clinit>()执行前,父类的< clinit>()已经执行完毕。
虚拟机必须保证一个类的< clinit>()方法在多线程下被同步加锁。

发生的时机
  1. 会导致类初始化的情况
    1. main 方法所在的类,总会被首先初始化
    2. 首次访问这个类的静态变量或静态方法
    3. 子类初始化,如果父类还没初始化,会引发父类的初始化;
    4. 子类访问父类的静态变量,只会触发父类的初始化;
    5. Class.forName
    6. new 会导致初始化
  2. 不会导致类初始化的情况
    1. 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
    2. 类对象.class 不会触发初始化
    3. 创建该类的数组不会触发初始化
public class Load1 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        // 1. 静态常量(基本类型和字符串)不会触发初始化
//         System.out.println(B.b);
        // 2. 类对象.class 不会触发初始化
//         System.out.println(B.class);
        // 3. 创建该类的数组不会触发初始化
//         System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
//         ClassLoader cl = Thread.currentThread().getContextClassLoader();
//         cl.loadClass("cn.ali.jvm.test.classload.B");
        // 5. 不会初始化类 B,但会加载 B、A
//         ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//         Class.forName("cn.ali.jvm.test.classload.B", false, c2);


        // 1. 首次访问这个类的静态变量或静态方法时
//         System.out.println(A.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
//         System.out.println(B.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
//         System.out.println(B.a);
        // 4. 会初始化类 B,并先初始化类 A
//         Class.forName("cn.ali.jvm.test.classload.B");
    }

}


class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}
class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

< clinit>():

  1. 定义clinit是类构造器方法
  2. 调用时机在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法
  3. 作用对象:clinit是class类构造器对静态变量,静态代码块进行初始化
  4. < clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器自动生成的; < clinit>()方法是由编译器自动收集类中的所有 类变量的赋值动作 和 静态语句块(static{}块)中的语句 合并产生的,所以当我们代码中包含static变量的时候,就会有clinit方法;
  5. 不生成clinit方法的情况:对应非静态的字段,不管是否进行了显式赋值,都不会生成clinit()方法;静态的字段,没有显式的赋值;比如对于声明为 static final 的基本数据类型或字符串常量的字段,不管是否进行了显式赋值,都不会生成clinit()方法;
< init>()
  1. 定义init是对象构造器方法
  2. 调用时机:在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法
  3. 作用对象:init是instance实例构造器,对非静态变量解析初始化
  4. 如果程序代码中定义有构造函数,它在解析的语法分析阶段被重命名为< init>(); 如果没有定义构造函数,则实例构造器< init>()是作为默认构造函数,是在填充符号表时添加的;
  5. < init>()中的super()调用父类< init>(),如果没有显示调用则会在语义分析的标注检查在方法检查时添加。

< init>()和 < clinit>()的执行

  1. 先执行类构造器< clinit>():
    1. 父类静态成员变量初始化、静态语句块(static{})执行;
    2. 子类静态成员变量初始化、静态语句块(static{})执行;
    3. 静态成员变量与静态语句块不区分,按照在代码中的位置顺序执行;
  2. 而后执行实例构造器< init>():
    1. 父类实例成员变量初始化、实例语句块({})执行;
    2. 父类实例构造器调用;
    3. 实例成员变量初始化、实例语句块({})执行;
    4. 实例成员变量、实例语句块不区分,按照在代码中的位置顺序执行;
  3. < init>()无论如何(自定义或编译器添加)都有父类< init>()(super())调用;实例成员变量初始化、实例语句块({})执行输出要先于构造器原来的代码执行输出;
  4. 即按照先父类,后子类;先静态、后实例的原则;
  5. < clinit>()是在Class文件被类加载器加载的时候(初始化阶段)执行,并且只执行一次(加锁 )而< init>()在每次实例化对象时都会执行

类加载器

类加载器的类型

双亲委派

当需要使用某个类时才会将他的class文件加载到内存生成class对象。加载某个类的class文件时,jvm采用双亲委派模式,即把请求交由给父类处理;
在这里插入图片描述

启动类、扩展类与系统类加载器之间的父子关系,并不是通过继承来实现的,而是通过组合,即使用 parent 变量来保存父加载器的引用。

java.lang.ClassLoader 中的 loadClass(String name)方法

public Class<?> loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}  
  
protected synchronized Class<?> loadClass(String name, boolean resolve)  
        throws ClassNotFoundException {  
  
    // 首先判断该类型是否已经被加载  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载  
        try {  
            if (parent != null) {  
                //如果存在父类加载器,就委派给父类加载器加载  
                c = parent.loadClass(name, false);  
            } else {    // 递归终止条件
                // 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
                // parent == null就意味着由启动类加载器尝试加载该类,  
                // 即通过调用 native方法 findBootstrapClass0(String name)加载  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值
            // 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出
            c = findClass(name);  
        }  
    }  
    //是否需要连接该类
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}

意义

  1. 防止加载同一个.class。通过委托去询问上级是否已经加载过该.class,如果加载过了,则不需要重新加载。保证了数据安全
  2. 保证核心.class不被篡改。通过委托的方式,保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。这样则保证了Class的执行安全

打破双亲委派模型的情况

参考:Tomcat、JDBC 如何打破双亲委派机制的

JDBC加载

在这里插入图片描述

Tomcat加载
  1. 先是Bootstrap.initClassLoaders()设置类加载器CommonClassLoader 、ServerClassLoader 、SharedClassLoader,分别对应的加载目录为(根目录下):/common(存放Tomcat与所有Web应用程序共用的类库)、/server(只Tomcat使用、而所有Web应用程序不可见的)、/shared(Tomcat不可见、而所有Web应用程序共用)。 这是通过/conf/catalina.properties设置,默认只为设置了CommonLoader,所以只会创建CommonLoader,而Server.loader 、SharedLoader都使用CommonLoader,而对应的三个目录合并为/lib(这是Tomcat6的简化改进)。
  2. 在后面初始化每个Web应用程序解析web.xml时,会创建WebappClassLoader,只有对应的Web应用程序可见,加载对应Web应用程序的/WEB-INF/lib里的类库。
    所以默认情况下,Tomcat类加载器架构如下:

在这里插入图片描述
其中Bootstrap(启动类)类加载器为Java虚拟机提供,包含JDK基本运行时类,而System类加载器用于Tomcat启动初始化(通常忽略);另外,Tomcat类加载器架构是按照经典的"双亲委派模型"来实现的,即:当类加载器被要求加载特定的类或资源时,它首先将请求委托给父类加载器,然后只有当父类加载器找不到请求的类或资源时,它才在自己的存储库中查找。
Tomcat的类加载器架构的好处是可以按需要实现Tomcat与Web应用程序、以及不现Web应用程序之间的类库共享与隔离,如常用的Spring等类库可以放到共享目录,为多个Web应用程序共用;而"双亲委派模型"也是JDK类加载器的架构,可以有效组织类库的层次结构,避免一个类被不同加载器加载多次(注意,同一个类文件被不同加载器加载表示不同的类)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值