类加载
类加载的过程
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
这 7 个阶段的顺序如下图所示:

加载(Loading):
类加载的第一阶段是加载。在加载阶段,Java虚拟机会通过类加载器查找并加载类的字节码。加载阶段的任务包括从文件系统、网络或其他来源获取类的字节码,并将字节码转换为内部数据结构,用来在Java虚拟机内部表示类。
主要做了三件事:
通过全类名获取定义此类的二进制字节流。
将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口。
加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;JDK1.8及之后:元空间),类被加载到方法区中后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类将 .class文件 加载至元空间后,会在堆中创建一个 Java.lang.Class 对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个 Class类型 的对象。

补充
虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:”通过全类名获取定义此类的二进制字节流” 并没有指明具体从哪里获取( ZIP
、 JAR
、EAR
、WAR
、网络、动态代理技术运行时动态生成、其他文件生成比如 JSP
…)、怎样获取。
加载这一步主要是通过我们后面要讲到的 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破由双亲委派模型)。
每个 Java 类都有一个引用指向加载它的 ClassLoader
。不过,数组类不是通过 ClassLoader
创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()
方法获取 ClassLoader
的时候和该数组的元素类型的 ClassLoader
是一致的。
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass()
方法)。
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
验证(Verification):
- 在验证阶段,Java虚拟机对加载的字节码进行验证,以确保其符合Java虚拟机规范。验证阶段包括类型检查、字节码验证、符号引用验证等步骤,以防止恶意代码或不规范的字节码被加载和执行。
验证阶段主要由四个检验阶段组成:
- 文件格式验证(Class 文件格式检查)
- 元数据验证(字节码语义检查)
- 字节码验证(程序语义检查)
- 符号引用验证(类的正确性检查)

准备(Preparation):
- 在准备阶段,Java虚拟机为类的静态变量分配内存并设置默认初始值。这些静态变量包括类级别的静态变量和类的常量。这些内存都将在方法区中分配。注意此阶段仅仅是为类变量即静态变量分配内存,并将其初始化为默认值,举个例子
1 |
|
注意: valFin
是被final static修饰的常量
在 **编译 **的时候已分配好了,所以在准备阶段 此时的值为5,所以在这个阶段也不会初始化。
这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static
关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。相关阅读:《深入理解 Java 虚拟机(第 3 版)》勘误#75
这里所设置的初始值”通常情况”下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了public static int value=111
,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111
,那么准备阶段 value 的值就被赋值为 111。
解析(Resolution):
- 解析阶段是可选的,它会将类、接口、字段和方法的符号引用解析为直接引用。解析阶段的目标是将类加载器在加载阶段创建的符号引用替换为直接引用,以便在运行期间能够直接定位到目标。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
- 符号引用就是一组符号来描述目标,可以是任何字面量。
- 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
符号引用验证
符号引用验证发生在类加载过程中的解析阶段,具体点说是 JVM 将符号引用转化为直接引用的时候
符号引用验证的主要目的是确保解析阶段能正常执行,如果无法通过符号引用验证,JVM 会抛出异常,比如:
java.lang.IllegalAccessError
:当类试图访问或修改它没有权限访问的字段,或调用它没有权限访问的方法时,抛出该异常。java.lang.NoSuchFieldError
:当类试图访问或修改一个指定的对象字段,而该对象不再包含该字段时,抛出该异常。java.lang.NoSuchMethodError
:当类试图访问一个指定的方法,而该方法不存在时,抛出该异常。
初始化(Initialization):
初始化阶段,简言之,为类的静态变量赋予正确的初始值,执行静态代码块。类的初始化是类装载的最后一个阶段。此时,类才会开始执行Java字节码。
初始化阶段的重要工作是执行类的初始化方法< clinit >()方法。
- 该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成。
- 它是由类静态成员的赋值语句以及static语句块合并产生的。
类初始化的时机
对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
- 当遇到 new、 getstatic、putstatic、invokestatic 这 4 条字节码指令时,比如 new一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当 jvm 执行
new
指令时会初始化类。即当程序创建一个类的实例对象。 - 当 jvm 执行
getstatic
指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 - 当 jvm 执行
putstatic
指令时会初始化类。即程序给类的静态变量赋值。 - 当 jvm 执行
invokestatic
指令时会初始化类。即程序调用类的静态方法。
- 当 jvm 执行
使用 java.lang.reflect
包的方法对类进行反射调用时如 Class.forName("...")
, newInstance()
等等。如果类没初始化,需要触发其初始化。
初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main
方法的那个类),虚拟机会先初始化这个类。
MethodHandle
和 VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,
就必须先使用 findStaticVarHandle
来初始化要调用的类。
「补充,来自issue745」 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类卸载的时机
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
只要想通一点就好了,JDK 自带的 BootstrapClassLoader
, ExtClassLoader
, AppClassLoader
负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
双亲委派模型
类加载器
那什么是类加载器?
通过一个类的全限定名来获取描述此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例
Java虚拟机支持类加载器的种类:主要包括3中: 引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用类加载器(系统类加载器,AppClassLoader)
,另外我们还可以自定义加载器-用户自定义类加载器

引导类加载器(Bootstrap ClassLoader):BootStrapClassLoader
是由c++实现的。引导类加载器加载java运行过程中的核心类库jre\lib\rt.jar,sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar
以及存放 在JRE\classes
里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List
等
扩展类加载器(Extension ClassLoader):它用来加载/jre/lib/ext
目录以及java.ext.dirs
系统变量指定的类路径下的类。
应用类加载器(AppClassLoader):它主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器。其实就是加载我们一般开发使用的类
用户自定义类加载器: 用户根据自定义需求,自由的定制加载的逻辑,只需继承应用类加载器AppClassLoader,负责加载用户自定义路径下的class字节码文件
线程上下文类加载器:除了以上列举的三种类加载器,其实还有一种比较特殊的类型就是线程上下文类加载器ThreadContextClassLoader可以是上述类加载器的任意一种,它允许在运行时为特定线程设置一个类加载器。
双亲委派模型
双亲委派模型(又称双亲委托模型)是Java类加载器(ClassLoader)的一种工作机制。这个模型的核心思想是在类加载的过程中,类加载器之间形成一种层次关系,每个类加载器都有一个父类加载器。类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。当一个类加载器接收到类加载请求时,它首先将这个请求委派给它的父类加载器去完成。只有在父类加载器无法完成加载任务(即在它的搜索范围内找不到所需的类),子类加载器才会尝试加载。
1 |
|
双亲委派模型的执行步骤
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,相关代码如下所示。
1 |
|
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()
方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()
方法来加载类)。 - 如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException
异常。
双亲委派模型的好处
避免类重复加载:双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),并且通过层级关系可以实现一定程度的隔离和安全性,也保证了 Java 的核心 API 不被篡改。
如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现两个不同的 Object
类。双亲委派模型可以保证加载的是 JRE 里的那个 Object
类,而不是你写的 Object
类。这是因为 AppClassLoader
在加载你的 Object
类时,会委托给 ExtClassLoader
去加载,而 ExtClassLoader
又会委托给 BootstrapClassLoader
,BootstrapClassLoader
发现自己已经加载过了 Object
类,会直接返回,不会去加载你写的 Object
类。
在类加载的过程中,当一个类被加载时,首先由最顶层的Bootstrap ClassLoader尝试加载,如果找不到类,则由Extension ClassLoader尝试加载,最后由Application ClassLoader尝试加载。这种层次结构确保了类的加载是有序的,避免了类的冲突和混乱。
打破双亲委派模型方法
为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 loadClass()
即可。
修正(参见:issue871 ):自定义加载器的话,需要继承 ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
为什么是重写 loadClass()
方法打破双亲委派模型呢?双亲委派模型的执行流程已经解释了:
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()
方法来加载类)
重写 loadClass()
方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化
Tomcat中的委派模型
我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader
来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。
Tomcat是web容器,我们把war包放到 tomcat 的webapp目录下,这意味着一个tomcat可以部署多个应用程序。
不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。防止出现一个应用中加载的类库会影响另一个应用的情况。如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
Tomcat 的类加载器的层次结构如下

Tomcat 这四个自定义的类加载器对应的目录如下:
CommonClassLoader
对应<Tomcat>/common/*
CatalinaClassLoader
对应<Tomcat >/server/*
SharedClassLoader
对应<Tomcat >/shared/*
WebAppClassloader
对应<Tomcat >/webapps/<app>/WEB-INF/*
从图中的委派关系中可以看出:
- CommonClassLoader:
CommonClassLoader
作为CatalinaClassLoader
和SharedClassLoader
的父加载器。CommonClassLoader
能加载的类都可以被CatalinaClassLoader
和SharedClassLoader
使用。因此,CommonClassLoader
是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。 CatalinaClassLoader
和SharedClassLoader
能加载的类则与对方相互隔离。CatalinaClassLoader
用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。SharedClassLoader
作为WebAppClassLoader
的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。- WebAppClassLoader:每个 Web 应用都会创建一个单独的
WebAppClassLoader
,并在启动 Web 应用的线程里设置线程线程上下文类加载器为WebAppClassLoader
。各个WebAppClassLoader
实例之间相互隔离(比如不同的应用有不同的User),进而实现 Web 应用之间的类隔。 为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器(多个应用程序,就有多个WebAppClassLoader),负责优先加载本身的目录下的class文件,加载不到时再交给CommonClassLoader
以及上层的ClassLoader
进行加载,这破坏了双亲委派机制。 - Jsp类加载器(JasperLoader):实现热部署的功能,修改文件不用重启就自动重新装载类库。
JasperLoader
的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader
的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap
功能。
线程上下文类加载器
JDBC中的委派模型
单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。
SPI 中,SPI 的接口(如 java.sql.Driver
)是由 Java 核心库提供的,由BootstrapClassLoader
加载。而 SPI 的实现(如com.mysql.cj.jdbc.Driver
)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(BootstrapClassLoader
)也会用来加载 SPI 的实现。按照双亲委派模型,**BootstrapClassLoader
是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。**
Java 中的 SPI 机制( ServiceLoader 工具类)就是在每次类加载的时候会先去找到 class 相对目录下的 META-INF /services 的文件夹,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。
Spring的委派模型
假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 SharedClassLoader
加载(Web 服务器是 Tomcat)。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以, Spring 的类加载器(也就是 SharedClassLoader
)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 SharedClassLoader
的加载路径下,所以 SharedClassLoader
无法找到业务类,也就无法加载它们
如何解决这个问题呢?
这个时候就需要用到 线程上下文类加载器(ThreadContextClassLoader
)
拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 WebAppClassLoader
,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader
。这样就可以让高层的类加载器(SharedClassLoader
)借助子类加载器( WebAppClassLoader
)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。
线程线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的。也就是让父类加载器有了使用子类加载器的能力。
Java.lang.Thread
中的getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
分别用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)
进行设置的话,线程将继承其父线程的上下文类加载器。
Spring 获取线程线程上下文类加载器的代码如下:
1 |
|
Class 文件的结构
根据 Java 虚拟机规范,Class 文件通过 ClassFile
定义,有点类似 C 语言的结构体。
ClassFile
的结构如下:
1 |
|

魔数(Magic Number)
1 |
|
每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。Java 规范规定魔数为固定值:0xCAFEBABE。如果读取的文件不是以这个魔数开头,Java 虚拟机将拒绝加载它.
Class 文件版本号(Minor&Major Version)
1 |
|
紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本号,第 7 和第 8 个字节是主版本号。
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 javap -v
命令来快速查看 Class 文件的版本号信息。
高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。
常量池(Constant Pool)
1 |
|
紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1
(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)。
常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。
类型 | 标志(tag) | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_FieldRef_info | 9 | 字段的符号引用 |
CONSTANT_MethodRef_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodRef_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
.class
文件可以通过javap -v class类名
指令来看一下其常量池中的信息(javap -v class类名-> temp.txt
:将结果输出到 temp.txt 文件)。
访问标志(Access Flags)
1 |
|
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public
或者 abstract
类型,如果是类的话是否声明为 final
等等。
类访问和属性修饰符:
我们定义了一个 Employee
类
1 |
|
通过javap -v class类名
指令来看一下类的访问标志。

当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
1 |
|
Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引、父类索引和接口索引集合按照顺序排在访问标志之后,
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object
之外,所有的 Java 类都有父类,因此除了 java.lang.Object
外,所有 Java 类的父类索引都不为 0。
接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是extends
) 后的接口顺序从左到右排列在接口索引集合中。
字段表集合(Fields)
1 |
|
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
field info(字段表) 的结构:
- access_flags: 字段的作用域(
public
,private
,protected
修饰符),是实例变量还是类变量(static
修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 - name_index: 对常量池的引用,表示的字段的名称;
- descriptor_index: 对常量池的引用,表示字段和方法的描述符;
- attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;
- attributes[attributes_count]: 存放具体属性具体内容。
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。
字段的 access_flag 的取值

方法表集合(Methods)
1 |
|
methods_count 表示方法的数量,而 method_info 表示方法表
Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项
**method_info(方法表的) 结构: **

方法表的 access_flag 取值:

注意:因为volatile
修饰符和transient
修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized
、native
、abstract
等关键字修饰方法,所以也就多了这些关键字对应的标志。
属性表集合(Attributes)
1 |
|
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。