java类文件格式和类加载机制

Class文件解析

java虚拟机设计之初,就考虑到语言扩展的问题,因此java虚拟机并不是专门为java服务的。确切的说,java虚拟机是为class文件服务的,一切合法的class文件都可以被虚拟机接受执行,而java编译器将java代码编译成对应的class文件,从而被虚拟机执行。类似的,如今groovy和scala文件都可以被各自的编译器编译成class文件运行在java虚拟机中。

为了保证class文件能够被虚拟机正确执行,且保证虚拟机的正确性不被破坏,class文件定义了严格的文件格式,整个文件没有任何分隔符,所有的数据都按照顺序和大小严格排列,精确到字节,我们可以具体分析下这些字节都是什么:

示例代码:

package org.fenixsoft.clazz;

public class Test {
    private int m;
    public int inc() {
        return m + 1;
    }
}

编译出的字节码:

cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 5465 7374 2e6a 6176 610c
0007 0008 0c00 0500 0601 0018 6f72 672f
6665 6e69 7873 6f66 742f 636c 617a 7a2f
5465 7374 0100 106a 6176 612f 6c61 6e67
2f4f 626a 6563 7400 2100 0300 0400 0000
0100 0200 0500 0600 0000 0200 0100 0700
0800 0100 0900 0000 1d00 0100 0100 0000
052a b700 01b1 0000 0001 000a 0000 0006
0001 0000 0003 0001 000b 000c 0001 0009
0000 001f 0002 0001 0000 0007 2ab4 0002
0460 ac00 0000 0100 0a00 0000 0600 0100
0000 0600 0100 0d00 0000 0200 0e

class字节定义:

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count - 1
u2accsee_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_count

字段解析:

Magic nubmer:

cafebabe,8个字节,用于标识此文件是否是一个可以被虚拟机接受的class文件,由于后缀名可以被改,为了防止破坏虚拟机,加了这个标识。

Version:

Minor version(0000) + Major version(0034),用于标识此文件的java版本,高版本虚拟机向下兼容,高于当前虚拟机版本的class文件将被拒绝执行。

常量池大小:

表明有多少个常量,注意此处表示的不是字节数,而是要计算多少个常量,具体每个常量占用多少字节可以通过规则计算出来,0013表示本class共有18个常量。注意常量池常量数量是常量池大小-1,原因是常量池中的常量都是从下标1开始计数,下标0被保留为不指向任何常量。

常量池:

记录所有的常量,主要存放两大类变量:
- 字面常量(Literal):比较接近于java语言层面的常量概念,如文本字符串、声明为final的常量值等
- 符号引用(Symbolic References):属于编译原理的层面,主要包含下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符

java代码在进行javac编译时,不会在每个class文件中保存各个方法、字段的内存布局信息,这些字段、方法在class文件中都是以符号的形式存在,在虚拟机运行时,会从对应的class文件中找到对应的符号引用,然后在类创建运行时解析翻译到具体的内存地址中。

常量池中常量都是计算得到的,感兴趣的可以查看下具体规则,可以使用javap -verbose Test.class命令查看具体的常量:
Classfile /Users/huanhuanjin/code/python/Test.class
  Last modified 2018-1-5; size 285 bytes
  MD5 checksum 94de66f82c58a2eb4a7394d679520fb7
  Compiled from "Test.java"
public class org.fenixsoft.clazz.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // org/fenixsoft/clazz/Test.m:I
   #3 = Class              #17            // org/fenixsoft/clazz/Test
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               org/fenixsoft/clazz/Test
  #18 = Utf8               java/lang/Object
{
  public org.fenixsoft.clazz.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 6: 0
}
SourceFile: "Test.java"
访问权限:

用于标识一些类或者接口层次的访问信息,如这个Class是类还是接口,是否是public类型,是否是abstract类型,如果是类的话,是否被表示为final。具体定义可以搜一下,这里的权限值为0x0021,表示public class。

类索引、父类索引和接口索引集合

这三个字段用来表示一个类的继承关系,类索引确定这个类的全限定名,父类索引确定这个类的父类全限定名,接口索引集合用来确定这个类实现的所有接口。由于java不允许多重继承,所有父类索引只有一个u2字段,而接口确是一个集合。

根据类索引的值,可以确定这个类的全限定名,本文中,0x0021后跟的是0x0003,从常量池中查找0003号的引用值为0x0017,0x0017的值为org/fenixsoft/clazz/Test,即本类的全限定名。同理可以得出父类的全限定名。然后本类没有继承任何接口,即接口集合为空。

字段表集合

用来描述class的字段信息,包括字段名称,访问权限,类型,是否是final,常量值等信息。

方法表集合

和字段表基本一致,包含方法的名称,访问权限,返回值类型等信息,注意编译器会自动加入一些方法,最常见的是

属性表集合
  • code属性:主要用来存放由javac编译器处理后的字节码指令,方法如何执行在此处定义。

java类加载机制

与编译期进行连接的语言不同,java语言中,类型的加载、连接和初始化都是在程序运行期间完成,用户甚至可以从网络或者其他地方加载一个二进制流作为代码的一部分,只要保证class二进制流正确,而此时已经加载过的class能够正确的
使用该class。

java类加载分为7个阶段:
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载

其中验证、准备和解析三个阶段统称为连接阶段。

加载、验证、准备、初始化和卸载这5个阶段开始的顺序是确定的,类的加载过程必须按照这种方式开始,但是这些阶段通常都是互相交叉地混合进行,一个阶段执行中就会触发另一个阶段,执行结束没有依赖关系。

解析阶段可以在初始化阶段之后进行,主要是为了支持java语言的运行时绑定特性。

java虚拟机规范中并没有对何时加载一个类做规定,不同的虚拟机可以有自己的策略。但是对于初始化阶段,虚拟机强制规定了==有且只有==5种情况需要立即对类进行初始化(此时加载、验证、准备阶段已经开始,解析不一定)。
- 遇到new、getstatic(读static)、putstatic(写static)或者invokestatic(调用static方法)这4条字节码指令时,如果类没有进行过初始化,则必须先进行初始化。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则必须初始化。
- 如果初始化一个类的时候,其父类没有初始化,则必须要触发父类的初始化。
- 虚拟机启动的时候,用户需要制定一个要执行的主类,虚拟机会先初始化这个主类。
- 当使用jdk1.7的动态语言支持的时候,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,则需要先出发起初始化。

这5种场景称为对一个类的主动引用,除此之外的引用方式,都不会触发初始化,称为被动引用,如下所示:

package org.fenixsoft.clazz;

public class SuperClass {
    public static int value = 123;
    public static final int HELLOWORLD = "hello world";
    static {
        System.out.println("SuperClass init!");
    }
}

public class SubClass {
    static {
        System.out.println("SubClass init!");
    }
}

public class NotInitialization {
    public static void main(String[] args) {
        //1
        System.out.println(SubClass.value);
        //2
        SuperClass[] sca = new SuperClass[10];
        //3
        System.out.println(SuperClass.HELLOWORLD);
    }
}

分析上述三条语句单独执行的情况:
- 单独执行1,只会输出”SuperClass init!”,由于对于static字段,只会触发直接定义这个字段的类的初始化,因此通过子类来引用父类的static字段,只会触发父类的初始化,而父类的初始化是否会触发子类的加载验证初始化,虚拟机规范并没有给出具体规定。
- 单独执行2,会发现什么都没有输出,原因是new的对象并不是SuperClass,而是SuperClass[10],却发现触发了一个名为[Lorg.fenixsoft.clazz.SuperClass的初始化,这是一个由java虚拟机自动生成的、直接集成与java.lang.object的子类,创建动作由字节码指令newarray触发。这个类代表了SuperClass的一维数组,数组中所有的属性和方法都实现在这个类里,因此C/C++语言数组越界会访问非法内存,而java数组越界会抛出越界异常。
- 单独执行3,也会发现没有任何输出,这是因为final字段在编译阶段经过优化,已经直接存在NotInitialization的常量池中,实际上两个和SuperClass并不存在任何引用关系。

对于接口的加载,只有一点与类不同,在第三点,当一个类初始化的时候,要求其父类必须全部初始化,而接口并没有这个要求。

那么,类加载的各个阶段都做了什么事情呢?做个简要总结:

加载

加载阶段主要做了三件事:
- 通过全限定类名获取此类的二进制字节流。

具体从哪里读取java虚拟机规范并没有给出要求,开发人员可以从任何地方获取这个字节流,如从网络获取,动态生成等。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

具体的数据结构格式虚拟机规范也并没有规定,虚拟机可以自行实现。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口。

这个对象的位置没有明确规定,可以在堆或者方法去区中。

验证

验证阶段是连接三步中的第一步,这一阶段的目的是为了确保Class文件字节流符合虚拟机要求,且不会对虚拟机造成危害。
- 文件格式验证(符合虚拟机要求)

验证字节流是否符合class文件格式规范,并且能够被当前虚拟机理解。

  • 元数据验证(符合java语言规范)

从基本层面验证class文件描述信息是否符合java语言规范,如除了java.lang.Object每个类都应有父类,是否继承了final修饰的类等,保证不存在不符合java语言规范的元数据信息。

  • 字节码验证(程序语义验证)

通过对数据流和控制流做分析,确保程序语义合法合逻辑,确保逻辑不会危害虚拟机。

  • 符号引用验证(验证符号引用正确性)

此阶段发生在连接第三步解析中,将符号引用转化为直接引用时,保证class字节流中的所有符号能够正确解析,找到其引用。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。需要注意两点:
- 此处是为类变量分配内存,即static修饰的变量,而不包括对象变量。
- 此处设置的初始值是类型级的初始值(零值),如int型是0,boolean型是false,而非用户制定的初始值。

解析

解析阶段是将虚拟机常量池内的符号引用替换成直接引用的过程,关于符号引用和直接引用:
- 符号引用以一组符号来描述引用的目标,只要使用时能找到正确的目标即可。符号引用与内存布局无关,符号引用的目标并不一定已经加载到内存中,虚拟机可以自己实现自己的内存布局,但是虚拟机能接受的符号引用必须都是一致的。
- 直接引用可以是直接指向目标的指针、相对偏移量或者其他能定位的东西。直接引用是与虚拟机内存布局相关的,同一个符号引用在不同虚拟机上翻译出的直接引用一般不同。如果有了直接引用,则引用的目标必定已经在内存中。

初始化

类初始化阶段是类加载的最后一步,之前所有阶段,除了加载时用户能够自定义类的加载器外,全部由虚拟机所实现,而初始化会开始执行类定义中的字节码。

虚拟机会为每个类生成()方法,这个方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定,静态变量只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

()方法与类的构造函数()不同,它不需要显示调用父类构造器,虚拟机会保证子类在()方法执行前,父类的()方法已经执行(解析的时候,子类中有父类的符号引用,父类一定会被加载,否则解析阶段就会出错)。

由于父类的()方法先执行,即父类的static语句块先执行,故以下代码会输出2.

package org.fenixsoft.clazz;

public class SuperClass {
    public static int A = 1;
    static {
        A = 2;
    }
}

public class SubClass {
    public static int B = A;
}

public static void main(String[] args) {
    System.out.println(SubClass.B);
}

虚拟机会保证()在多线程中被正确的加锁同步,有且仅有一个线程执行()方法。如果在一个类的()方法中耗时太久,有可能造成线程阻塞。

类加载器和双亲委派模型

类加载器

对于任意一个类,都需要加载它的类加载起和这个类被试一桶确定其在java虚拟中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,通俗点说,两个类是否相等,必须建立在两个类被同一个类加载器加载的前提下。用户可以自定义自己的类加载器。

从开发人员角度讲,绝大多数的java程序都会使用到以下几种类加载器:
- 启动类加载器(Bootstrap ClassLoader)

主要负责加载\lib目录中的,或者被-Xbootclasspath参数所制定的路径中的,并且能够被虚拟机识别的(名称识别)类库加载到虚拟机内存中,开发者不能直接使用,可以使用null来触发引导类加载器。

  • 扩展类加载器(Extension ClassLoader)

由sun.misc.Launcher$ExtClassLoader实现,负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所制定的路径中的所有类库,开发者可以直接使用。

  • 应用程序类加载器(Application ClassLoader)

由sun.misc.Launcher$AppClassLoader实现,这个类是ClassLoader中的getSystemClassLoader()方法的返回值,负责加载用户类路径(classpath)下的所致定的类库,开发者可以直接使用。

  • 开发者自定义类加载器
    自定义

双亲委派模型

为了保证类能够被正确的类加载器所加载,java提供了双亲委派模型
- 除了顶层的启动类加载器外,所有的类加载器都应有自己的父类加载器
- 当一个类加载器收到类加载的请求,会先将请求委派给父类加载器去加载,如果父类加载器不能加载,再有自己加载。

这个模型使得java类的加载有了一种带优先级的层级关系,保证了类能够被正确的加载器所加载。

破坏双亲委派模型

  • java1.0出现了ClassLoader,java1.2出现了双亲委派模型,在此1.2之前覆盖loadClass方法能够避开双亲委派模型,1.2之后的类加载器,推荐覆盖findClass方法。
  • 基础类要调用用户类的代码,通过使用线程类加载器来实现。
  • 为了支持程序的动态性热插拔,将双亲委派模型修改为复杂的网状结构,类和类加载器能够一起被热插拔替换。





  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值