类文件结构
Class文件是一组以8字节为基础单位的二进制流,中间没有分隔符。
类文件结构
魔数:4个字节CA FE BA BE
版本号:2个字节的次版本号
2个字节的主版本号
常量池
image.png接下来2个字节代表常量池的长度
常量池的长度是从1开始的,因此真正的大小要减1,比如0x 00 16 则说明一共是22个,从1开始,索引值是1-21
真内容开始的常量池的偏移量是0x00 00 00 0A
流程就是,从第一个常量池的偏移量开始开始,首先看tag,tag标识常量池的类型
下面是CONSTANT_Class_info型常量的结构,07 00 02,07代表tag(CONSTANT_Class_info),00 02 代表name_index,表示常量池的第二个常量。依次类推进行解析
{
u1 tag,
u2 name_index
}
二进制字节码过于复杂,使用javap -v 类名 进行字节码分析
原始类:
```java
package com.yth.demo.demo02;
public class TestClass {
private int m;
public int inc(){
return m+1;
}
}
javap -v 之后
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/yth/demo/demo02/TestClass.m:I
#3 = Class #20 // com/yth/demo/demo02/TestClass
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/yth/demo/demo02/TestClass;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 TestClass.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/yth/demo/demo02/TestClass
#21 = Utf8 java/lang/Object
字段和方法类似,只有被引用之后才会被编译到常量池中
字段结构:
字段结构真实字段编译后:
image.png
字段的引用格式如下:
image.png
即:
com/jvm/demo01/TestConstant.m:I
方法结构
image.png方法的引用格式:
image.png
具体jvm中
image.png
即:
com/jvm/demo01/TestConstant.getM:()I
访问标志
常量池之后2字节代表访问标识,用于标识类或者接口的访问信息,包括Class是类还是接口、是否是public,是否是abstract,是否是final等
类索引、父类索引、接口索引集合
Class文件用这三项数据来确定该类型的集成关系,类索引、父类索引都是一个u2类型数据,而接口索引集合是一组u2类型的数据集合。
上述代码的类索引部分
03 04 00,03代表常量池的偏移量
字段表集合
用于描述接口或类中声明的变量
表字段结构
access_flags访问标示
name_index:简单名称(字段名称)
description_index:描述字段的类型,如下表
字段类型对应表
后面两项为属性表
方法表集合
描述class中的方法结构,结构与方法表一致。
name_index:方法名称(没有参数,没有修饰符,没有返回值,上面例子中的inc)
description_index:描述符,按照(参数列表)返回值 的格式,上面inc方法的描述符就是()V;方法 java.lnag.String.toString()的描述符就是()Ljava/lang/String;方法void test(int a,int b)的描述符是(II)V
属性表
Class文件、字段表,方法表,都可以携带属性表,下面是部分属性表项
image.png
只介绍Code属性
方法中的代码经过JVM编译之后,变为字节码指令存入Code属性中。
image.png
attribute_name_index:指向常量池偏移量,代表属性名称,此处固定位Code
attribute_length:属性值的长度
max_stack:代表操作数栈的最大深度
max_locals:代表局部变量表所需的存储空间,空间单位是槽,该值不是方法中所有局部变量个数集合,而是各作用域中局部变量数量的最大值
code_length代表字节码长度,code是用于存储字节码指令的一系列字节
流。每个指令大小为u1
code虽然是u4,但其实限制65535条指令
this:过在Javac编译器编译的时候把对this关键字的访问转变为对一个普通方
法参数的访问,因此任何方法都至少有一个局部变量this,该约定只对实例对象有效,类对象不适用(static方法)
类的生命周期
加载、连接(验证、准备、解析)、初始化、使用、卸载
加载
负责以下内容:
- 通过一个类的全限定名获取此类的二进制字节流
- 将字节流代码的静态储存结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类属性的访问入口
类加载的时机
jvm没有规定类加载的时机,但是规定了初始化的时机,类的加载必须在初始化之前。
类初始化时机:
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时
- 使用new关键字实例化对象
- 读取或设置一个类型的静态字段 (编译时静态常量除外)
被 static final 修饰的变量称为常量:编译时常量(static final int A = 1024)、运行时常量(static final int len = "Rhine".length())。
编译期常量会在编译阶段存入调用类的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
- 调用一个类的静态方法时
- 使用java.lang.reflect包的方法对类型进行反射调用时
- 初始化类时,触发父类初始化(接口初始化时,父接口不需要完成初始化)
- 包含main函数的类,虚拟机启动时会初始化这个类
- java.lang.invoke.MethodHandle调用的方法如果是1.那四种
- 接口如果定义了default关键字,如果实现类发生了初始化,则接口也需要
上述六中是有且仅有。其他情况不会触发初始化,比如数组变量、比如静态变量(只会触发直接定义这个字段的类的初始化,子类并不会)
验证
验证阶段保证Class文件的字节流符合要求
- 文件格式校验:基于二级制流校验,保证字节流可以正确解析并存入方法区中
- 元数据校验:对字节码描述的信息进行语义校验,校验类的元数据符合语义(比如类是否有父类、字段是否与父类矛盾等)
- 字节码验证:主要对类的方法进行校验分析(现在很多都移到javac阶段进行)
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候
准备阶段
为类中定义的变量(静态变量,被static修饰的变量)分配内存,并设置类变量初始值(一般是0值,但如果是编译时常量static final,则会直接赋值)。1.8之后类变量会随着Class对象一起放入java堆中。
解析
解析阶段是将常量池内的符号引用转化为直接引用的过程
符号引用:用一组符号来描述引用的目标(见常量池)
直接引用:可以直接指向目标的指针、偏移量或间接指向目标的句柄
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引调用进行
初始化
在,准备阶段,已经给静态变量初始化过一次零值,在初始化阶段进行真正的赋值。初始化阶段就是执行类构造器<clinit>()方法的过程(不是构造函数)。
<clinit>()是所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的
image.png
<clinit>()不是必须的,如果没有静态变量和静态代码块,则没有该函数
父类的<clinit>()函数会在子类执行之前执行
Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,同一个类加载器下,一个类型只会被初始化一次。即:如果多线程环境下初始化类,一个线程退出后,其他线程不会再次进行初始化。
类加载器
类在使用的时候才会去加载,加载的时候会使用双亲委派的方式去加载
类由类本身和加载这个类的类加载器一起确定包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()、instanceof关键字
image.png
·启动类加载器(Bootstrap Class Loader):加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,并且可以被JVM识别的类。比如java、javax、sun等开头的类
启动类加载器获取的是null
扩展类加载器(Extension Class Loader):它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
只要把自己的jar包放到这下面,也可以被加载的。
应用程序类加载器(Application Class Loader):getSystem-ClassLoader()方法的返回值,系统默认的加载器,如果没有自定义类加载器,就是这个类加载的
双亲委派:除了顶层的启动类加载器外,每个类加载器都要有父加载器,父加载器不是继承关系,更像是组合。
每个类,都先由父类进行加载,如果父类抛出ClassNotFoundException 异常,再由自己类加载(调用自己的findClass方法)。这样可以保证,类和加载器一起有了一个优先级关系。比如Object这种基础类由基础类加载器加载。保证不管哪个类加载器加载类似Object这种类,都是同一种类型。
即:
1、保证核心类的安全。防止开发者取了和jdk核心类库中一样的包名和类名,委托给父类加载器能保证JDK类库的类优先加载。
2、防止已经被加载的类多次加载(一待有父类加载过,子类就不会再次加载了)
双亲委派的破坏:SPI、OSGI(星状)
SPI:使用线程线程上下文类加载器解决这个问题(启动类加载JNDI类,然后通过线程上下文类进行加载实现类)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
SPI中ServiceLoader
:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
数据库驱动加载(SPI方式):DriverManager
启动的时候进行SPI加载:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
然后loadInitialDrivers
加载SPI实现类:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
其中driversIterator.next()里面最终通过c = Class.forName(cn, false, loader);
进行类加载
其中loader为loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
正常的情况下,ClassLoader.getSystemClassLoader()
,Thread.currentThread().getContextClassLoader()
都是sun.misc.Launcher$AppClassLoader@18b4aac2
自定义类加载器:
必要性:
- 隔离加载类
- 修改类的加载方式
- 扩展加载源
- 防止源码泄漏
不同的类加载器都去loadClass,那这个返回值会是什么 Class<?> c = findLoadedClass(name)。实验都是null,是和类加载器有关系吧。
image.png如果一个类是由用户类加载器加载的,JVM会将类加载器的一个引用作为类型信息的一部分保存在方法区中
如何判断这个类是有这个类加载器加载的(双亲委派中),还是由AppClassLoader加载的
因为他们都是平级的?
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
用户自定义:
loadClass(String name)
findClass+definClass搭配使用
获取classLoader方式
System.out.println(Thread.currentThread().getContextClassLoader());
System.out.println(Main.class.getClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
网友评论