Class文件概述
我们知道,java代码是可以一次编译,到处运行的,其主要是因为运行在java虚拟机上,不同的操作系统,只要安装了java虚拟机,就能运行编译之后的java代码。
将编译之后的class文件解析出来,其实里面就是一堆的16进制字节。下面的图是class文件的结构。
类的加载过程
1、加载
①、通过全限定名获取此类的二进制字节流。
- 生成class文件字节码的方式有很多种,从网络中读取,将jsp文件转换为class文件等等。
②、将该二进制字节流代表的静态存储结构转化为方法区运行时的数据结构。
③、生成该类的java.lang.Class,作为访问入口。
- 对于 hotSpot 而言,Class对象比较特殊,它虽然是对象,但是它并没有存储在堆中,而是在存储在方法区中。
2、验证
该阶段主要是验证加载的二进制字节流文件是否符合Class文件的规范。
是否能被当前版本的虚拟机所理解。
①.文件格式的验证
- 是否以魔数 0xAEDADSDF开头
- 常量池中的常量类型是否有不被支持的常量类型
…
②.元数据的验证
- 这个类是否有父类(除了Object类之外,其余的类都应该有父类)
…
③.字节码验证
- 字节码指令跳转是否会有问题
④.符号引用验证
3、准备
这个阶段是正式仅为类变量(static修饰的变量)进行内存分配的过程。而不包括
实例变量,实例变量是在对象实例化的时候随着对象一起分配至java堆中的。
public static int value = 123;
这个变量 value 在准备阶段后的初始值为0,而不是 123。
因为这时候尚未开始执行任何java方法,把123赋值给value是在程序初始化之后进行的。
public static final int value = 123;
添加了final之后就会不一样,添加了final之后,在字段属性表中是属于常量,在准备阶段就会将value赋值为123
4、解析
解析阶段是虚拟机将常量池内的符号引用转换为直接引用的过程。
4、初始化
类初始化阶段是执行类构造器clinit<>()的过程。
clinit<>() , 类构造器方法,在jvm第一次加载class文件时调用,因为是类级别的,所以只加载一次,是编译器自动收集类中所有类变量(static修饰的变量)和静态语句块(static{}),中的语句合并产生的,编译器收集的顺序,是由程序员在写在源文件中的代码的顺序决定的。
init<>(),实例构造器方法,在实例创建出来的时候调用,包括调用new操作符;调用Class或java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。
- 如果代码中存在继承关系,则优先加载父类的类变量,然后在加载子类的类变量
- 只有父接口中存在变量的时候,clinit<>()才会被初始化。
类与类加载器
双亲委派模型
对于java虚拟机来说,存在2种不同的类加载器。
一种是启动类加载器(bootStrap ClassLoader),是虚拟机自身的一部分。
另外一种是 其他类加载器,是由java实现的。独立与虚拟机外部,全部都继承与抽象类ClassLoader
上图展示的是类加载器的双亲委派模型
- 启动类加载器
它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar),是用原生代码来实现的,并不继承自java.lang.Classloader。 - 扩展类加载器
用来加载Java的扩展库(JAVA_HOME/JRE/EXT/*.jar),Java虚拟机的实现会提供一个扩展库目录。该加载器在此目录里面查找并加载Java类。 - 应用类加载器
它根据Java应用程序的类路径(classpath,java.class.path)路径类。一般来说,系统(应用程序)类都是由它来加载。
双亲委派模型的工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是委派给父类加载器去完成加载。只有父类加载器反馈自己无法加载这个请求的时候,子类加载器才会尝试自己去加载。
使用双亲委派模型的好处:
1、保证java核心库的类型安全。
2、Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。