class类文件结构
任何一个Class文件都对应着唯一 一个类或者接口的定义信息,但是类或者接口不一定都定义在文件里。类或者接口可以直接通过类加载器生成。Class文件是一组以8位字节为基础单位的二进制流,按照严格的顺序排列在Class文件里。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号和表
- 无符号属于基本数据类型,以u1,u2,u4,u8,代表1个字节,2个字节,4个字节,8个字节的无符号数。
- 表是由多个无符号数或者其他表作为数据项构成复合类型的数据类型,Class文件本质上就是一张表。
魔数和Class文件的版本
每个Class文件的头4个字节称为魔数,它的作用就是确定这个文件是否是一个能被虚拟机接受的Class文件。接着魔数的4个字节存储的是Class文件的版本号。
常量池
版本号之后的是常量池的入口,常量池是Class文件的资源仓库,共有21项常量,主要存放两大类常量:
- 字面量:如字符串,声明为final的常量值等
- 符号引用:类和接口的全限定类名,字段的名称和描述符,方法的名称和描述符
当虚拟机运行的时候需要从常量池中获得对应的符号引用,在类创建时或者运行时解析,翻译到具体的内存地址当中,进行动态连接。
访问标志
在常量池结束之后,接着的两个字节代表访问标志,这个标志用来识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstruct类型,是类的话是否被声明为final,是否是一个注解,枚举等。
类索引,父类索引,接口索引
Class文件由这三项数据来确定这个类的继承关系,类索引用于确定这个类的全限定类名,父类索引用于确定这个类的父类的全限定类名,除了java.lang.Object之外所有的java类都有父类,父类索引都不为0,接口索引集合用于描述这个类实现了哪些接口,如果没有则为0.
字段表集合
字段表用于描述接口或者类中声明的变量,字段可以包括类级变量(static修饰)及实例级变量,但是不能包括在在方法内部声明的局部变量。字段包含的信息有被那个关键字修饰,字段的数据类型。但是这些信息无法固定只能引用常量池的常量描述
方法表集合
和字段表集合类似
属性表集合
Java程序方法体的代码经过javac编译器处理后,变为字节码指令存放在code属性中。但是接口和抽象方法不存在code属性。如果把一个java程序分为代码(code)和元数据(类,字段,方法定义等)两部分那么在class文件中Code用于描述代码,所有其他数据项目都用于描述元数据。
ConstantValue属性
只有被static关键字修饰的变量才能使用这项属性,例如”int x=1”“static int x=1”虚拟机对这两种变量的赋值方式和时机都有所不同。对于实例变量即未被static修饰,它的赋值是在实例构造器< init >()方法中赋值的。而对于类变量,如果同时用static和final修饰一个变量并且这个变量是基本数据类型或者字符串类型使用ConstantValue赋值,如果这个变量不是基本类型及字符串或者没有被final修饰就会在< clinit >()方法中初始化。
类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
加载,验证,准备,初始化和卸载这五个阶段的顺序是确定的,在类加载过程中必须严格按照这种顺序开始。而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始。这是为了支持java的动态绑定。
加载
加载需要虚拟机完成以下3件事情:
1 . 通过一个类的全限定类名获取定义类的二进制流。但是不一定是从Class文件中获取。例如从jar,ear,war包中,网络中,运行时计算生成(动态代理),jsp文件等
2 . 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3 . 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载阶段完成之后,虚拟机将外部的二进制流按照所需的格式存储在方法区中,然后实例化一个java.lang.Class对象,Class对象比较特殊,它虽然是对象但是存放在方法区中,这个对象作为程序访问方法区中这些类型数据的外部接口。
数组类本身不通过类加载器创建,由java虚拟机直接创建。但是数组类和类加载器有着密切的关系,它的组件最终要靠类加载器创建。如果数组类的组件类型是引用类型,那么数组类的可见性和它的组件类型一致,如果组件类型不是引用类型,那么他的默认可见性是public.
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件字节流包含的信息是否符合虚拟机的要求。
1. 文件格式验证
2. 元数据验证
3. 字节码验证
4. 符号引用验证
验证阶段是一个非常重要的,但不是一定必要的(因为对程序的运行期没有影响),如果代码已经被反复使用和验证过,可以考虑关闭验证的过程,缩短虚拟机类加载时间。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,类变量使用的内存都将在方法区分配,强调的是这个时候进行内存分配的只是类变量(static修饰的),而不包括实例变量,实例对象会在对象实例化的时候随着对象分配在堆内存中。
public static int i=123;
变量在准备阶段后的i初始值是0而不是123 ,而赋值动作在初始化阶段才会执行。
public static final int i=123;
如果i被final修饰,在编译的时候javac将会为i生成ConstantValue属性,在准备阶段就会将i赋值为123;
解析
解析阶段是虚拟机将常量池内的符号引用转化为直接引用的过程。符号引用的目标不一定已经加载到内存中但是直接引用的目标一定是已经在内存中存在的,直接引用可以是直接指向目标的指针,相对偏移量或者能间接定位到目标的句柄。
- 类或者接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
初始化是类加载过程的最后一步,前面的过程除了在加载阶段用户可以通过自定义的类加载器参与外,其他动作完全由虚拟机主导和控制,到了初始化阶段才真正开始执行类中定义的java代码。
在准备阶段,类变量已经复过一次系统要求的初始值,再初始化阶段再根据java代码去初始化类变量和其他资源,或者说初始化阶段是执行类构造器< clinit >()方法的过程。
< clinit >()方法
< clinit >()方法:是由编译器自动收集类中所有的类变量赋值动作和静态代码块中语句合并而成的,收集的顺序是语句在源文件中出现的顺序决定的。
- 静态代码块只能访问到在静态代码块前定义的变量,定义在静态代码块后的变量,它可以对其赋值但是不能访问。
public class Test{
static {
i=0; //给变量赋值正常编译
System.out.println(i);//无法通过,非法向前引用
}
static int i=1;
}
- < clinit >()方法和类构造函数(< init >()方法)不同,他不需要显式调用父类构造器,虚拟机会保证< clinit >()方法在子类之前执行。意味着父类定义的静态语句块要优先于子类的赋值操作。
- < clinit >()方法对于类或者接口来说并不是必须的。< clinit >()方法中如果有耗时时间很长的操作可能会导致线程阻塞。
触发初始化的场景
- 最常见的场景就是使用new关键字实例化对象的时候,读取或者设置一个类的静态字段(被final修饰,放入常量池的静态字段除外),调用一个类的静态方法的时候。
- 使用java.lang.reflect包对类进行反射调用的时候
- 当初始化一个类,其父类还没有初始化的时候,先初始化其父类
- 当虚拟机启动,用户指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
- 使用Jdk1.7的动态支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果REF_getStatic, REF_putStatic, REF_invoketStatic,的方法句柄,并且这个方法对应的类没有初始化,先初始化这个类。
触发类的初始化的方式有且只有这5种场景,称为对一个类的主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
当一个类在初始化的时候,要求其父类全部被初始化。但是一个接口在初始化的时候,并不要求其父接口全部完成初始化,只有在真正使用父接口的时候才会初始化。初始化是类加载过程的最后一步,前面的过程除了在加载阶段用户可以通过自定义的类加载器参与外,其他动作完全由虚拟机主导和控制,到了初始化阶段才真正开始执行类中定义的java代码。
被动引用示例:
class SuperClass
{
static {
System.out.print("SuperClass init");
}
static int value=123;
}
class SubClass extends SuperClass
{
static {
System.out.print("SuperClass init");
}
}
class Test
{
public static void main(String[] args){
System.out.print(SubClass.value);
}
}
上述代码只输出了”SuperClass init” 对于静态字段,只有直接定义这个字段的类才会被初始化,通过子类引用父类定义的静态字段,只会触发父类的初始化。
class Test
{
public static void main(String[] args){
SuperClass [] sc= new SuperClass [10];
}
}
没有输出”SuperClass init”,通过数组定义引用类不会触发类的初始化
class SuperClass
{
static {
System.out.print("SuperClass init");
}
static final String HELLOWORLD ="hello world";
}
class Test
{
public static void main(String[] args){
System.out.print(SuperClass.HELLOWORLD);
}
}
常量在编译阶段经过常量的传播优化存入到Test类的常量池,SuperClass.HELLOWORLD的引用被转化为对自身常量池的引用,也就是说在Test的Class文件中没有SuperClass的符号引用入口。无法触发初始化SuperClass,不能输出”SuperClass init”
类加载器的双亲委派机制
启动类加载器:
这个类负责加载< JAVA_HOME >\bin目录的,或者被-Xbootclasspath参数指定路径的,并且可以被虚拟机执识别的类库,如rt.jar,启动类加载器无法被java程序直接引用,如果需要把加载请求委托给引导类加载器,直接使用null代替。
扩展类加载器:
负责加载< JAVA_HOME >\bin\ext目录的,开发者可以直接使用
应用程序类加载器:
负责加载ClassPath路径下的类库,如果程序没有指定自定以的类加载器一般情况下就使用这个类加载器。
双亲委派模型
类加载器之间的父子关系一般不会以继承关系来实现,而是以组合关系来复用父加载器的代码,双亲委派模型在jdk1.2被引入,但是不是一个强制行性的约束模型。
双亲委派模型的工作过程:
一个类加载器收到了类加载的过程,它首先不会自己去尝试加载这个类,而是把这个类委托给父加载器完成,每一层都是如此,因此所有的加载请求都会被传送到顶层的启动类加载器,只有父类加载器反馈无法完成这个加载请求时,子类加载器才会尝试自己加载。双亲委派模型保证了java程序的稳定性,程序的一致性,安全性。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先检查这个类是否被加载过了
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
// 父类加载器无法完成加载
}
if (c == null) {
// If still not found, then invoke findClass in order
// 调用本身的findClass(name)
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;
}
示例(双亲委派)
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class ClassLoaderTest {
public static void main(String[] args)
throws ClassNotFoundException, InstantiationException, IllegalAccessException {
myClassLoader mcl = new myClassLoader();
Class<?> loadClass = mcl.loadClass("com.refect.demo.Demo");
System.out.println(loadClass.newInstance());
System.out.println(loadClass.getClassLoader());
System.out.println(loadClass.getClassLoader().getParent());
System.out.println(loadClass.getClassLoader().getParent().getParent());
System.out.println(loadClass.getClassLoader().getParent().getParent().getParent());
}
}
class myClassLoader extends ClassLoader {
FileInputStream fis = null;
ByteArrayOutputStream baos = null;
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
System.out.println(name);
System.out.println(name.startsWith("java"));
byte[] data = null;
if (name.startsWith("java")) {
return super.loadClass(name);
}
try {
data = this.loadClassFile(name);
} catch (IOException e) {
e.printStackTrace();
}
// return super.loadClass(name);
return defineClass(name, data, 0, data.length);
}
private byte[] loadClassFile(String name) throws IOException {
String fileName = name.substring(name.lastIndexOf(".") + 1);
String filePath = "G:" + File.separator + fileName + ".class";
File file = new File(filePath);
fis = new FileInputStream(file);
baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while ((len = fis.read(buf)) != -1) {
baos.write(buf, 0, len);
}
fis.close();
baos.close();
return baos.toByteArray();
}
}
public class Demo {
@Override
public String toString() {
return "这是自己的类加载器";
}
}
加载的时候会先加载父类,如果是父类交给父类加载处理。
但是为什么Bootstrap ClassLoader 为null?因为它是用c++实现的啊,没有办法获得他的实例但是他确实是存在的。
破坏双亲委派模型
在JdK1.2已经不提倡用户再去覆盖loadClass()方法了而是增加了一个findClass(),直接加载本类,不再委派父类加载。
线程上下文类加载器(Thread Context ClassLoader) ,JNDI,JDBC
等都采用的是这种方式。