类的结构

CA FE BA BE(魔数) Minor Version(次版本号) Major Version(主版本号)
Constant Pool Count(常量池数量) Constant Pool(常量池)
Access Flags(访问标记) This Class(当前类) Super Class(父类)
Interface Count(接口数量) Interfaces
Field Count(字段集合) Fields
Method Count(方法集合) Methods
Attribute Count(属性集合) Attributes

魔数

Class 文件的头4个字节被称为魔数,用来标识此文件是否是一个能被虚拟机接收的Class文件

Class 版本号

魔数后面的4个字节表示Class文件的版本号,第5、6位是次版本号,第7、8位是主版本号,用来表示此Class文件是被哪个版本的java编译器编辑的
注意:JVM是向下兼容的,高版本的JVM能运行低版本的字节码

常量池

版本号之后的是常量池,常量池的数量是 constant_pool_count-1(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”
主要存放着字面量符号引用两种常量,包括:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

    访问标志

    访问标志占用两个字节,用来表示类或者接口层次的访问信息,如 public、abstract、final 等

    当前类&父类&接口索引集合

    用于描述当前类、其父类和其实现的所有接口的全限定名(全类名,非限定名即短类名)

    字段表集合

    用于描述当前类或接口中的全局变量,不包括方法内部的局部变量

    方法表集合

    和字段表集合类似,用来描述类中方法的信息

    属性表集合

    字段表和方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息(如字段或者方法上面的注解,方法抛出的异常,code属性等)

    类加载过程

    编译好的 class 文件需要加载到JVM中才能够被运行和使用
    JVM加载class文件主要分为三步:加载、连接、初始化
    其中连接又分为三步:验证、准备、解析
    注意:数组由JVM直接创建,不需要被加载;用户可以通过重写loadClass()自定义类加载器

    加载

    主要完成三件事情
  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流中的静态存储结构转换为方法区中的运行时数据结构(内部采用 C++ 的instanceKlass 来描述 java 类)
  3. 在内存中生成一个代表该类 class 的类对象(java_mirror,也叫 java镜像类),作为方法区这些数据的入口(java_mirror和instanceKlass互相保存的对方的地址,堆中的实例对象的对象头中则会保存 java_mirror 的地址,方便实例对象通过类对象找到方法区中的 instanceKlass,以此获取类的各种信息(常量池、方法、类加载器、静态常量等))

注意:若这个类的父类还没有被加载,则会先加载父类;加载和链接可能是交替执行的

链接

验证

验证class文件的内容是否符合JVM规范,并进行一些安全性检查和初始化零值等操作

准备

为 static 变量分配空间,设置默认值(初始化零值)

  • static变量 在jdk1.7以前存储在instanceKlass中(方法区),1.7及之后存在了java_mirror类对象中(堆)
  • 如果 static 是 final 的基本数据类型或是字符串常量,在编译期阶段值就确定了,那么赋值操作在准备阶段完成
  • 如果 static 是 final 的引用数据类型,那么赋值操作在初始化阶段完成

    解析

    将方法区运行时常量池中的符号引用解析成直接引用
    符号引用:符号引用就是用一组符号描述目标,通过解析符号引用可以定位到目标方法或类
    直接引用:直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄
    解析阶段其实是JVM将运行时常量池中的符号引用转换成直接引用的过程,从而能够获取想要的字段、类或方法在内存中的指针或者偏移量

    初始化

    初始化阶段会执行<cinit>()方法(编译器自动生成的方法),是类加载的最后一部;这个方法是带锁线程安全的,在多线程环境下进行类初始化可能会引起阻塞问题
    只有5种情况,必须对类进行初始化:

    使用

    初始化完成后就可以使用静态变量、创建对象等操作

    卸载

    卸载即将堆空间中的类对象进行GC回收,需要满足三个要求:
  1. 该类的所有对象都收回
  2. 该类在其它地方没有被引用
  3. 该类加载器的实例(ClassLoader)已经被回收

    类加载器

    所有的类都需要通过类加载器进行加载,作用只有一个,将class文件内容加载到运行时数据区

    类加载器种类

    除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader

  4. BootstrapClassLoader:启动类加载器,由C++实现,负责加载JAVA_HOME/lib 目录下的jar包或者被-Xbootclasspath参数指定路径的所有类

  5. ExtensionClassLoader:扩展类加载器,主要负责加载JRE_HOME/lib/ext 目录下的jar包和类,或者被java.ext.dirs 系统变量所指定的路径下的jar包
  6. AppClassLoader:应用程序类加载器,面向用户的加载器,负责加载当前应用 classpath 下的jar包或类

ExtensionClassLoader 是 AppClassLoader 的父类加载器,BootstrapClassLoader 是 ExtensionClassLoader 的父类加载器(在代码中 ExtensionClassLoader 的父类加载器为 null)

双亲委派模型

每个类都有一个对应的加载器,ClassLoader 在工作时会默认使用 双亲委派机制

工作流程

在类加载的时候,首先会判断当前类是否被加载,若被加载则会直接返回;若没有被加载,则会首先尝试将加载请求委派给父类加载器的 loadClass();正常来说所有的加载请求最终都会传递到BootstrapClassLoader中,当父类加载器无法处理时,才会由自己的类加载器处理

作用

  1. 可以避免类的重复加载,提高运行效率
  2. 保证了Java的核心API不被篡改(若我们写了一个和核心API同名的类,如java.lang.Object,就会导致系统出现不同的Object类)

    如何不用双亲委派模型

    自定义加载器,需要继承 ClassLoader 类

  3. 如果不想打破双亲委派模型,重写 findClass() 方法即可,当所有父类加载器都无法加载时,则会使用自定义加载器

  4. 如果想打破双亲委派模型直接使用自定义加载器,则重写 loadClass() 即可

注意:除了BootstrapClassLoader以外,其它类加载器均继承自 java.lang.ClassLoader