Class格式
Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。
无符号数
属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。
表
是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:
从二进制的数据来看:
通过javap
编译成可视化语言来看:
1 |
|
1 | 魔法数字: cafe babe |
详情可查阅查阅:
对象创建方式
使用new关键字创建对象
1 | A a = new A(); |
使用Class类的newInstance方法(反射机制)
1 | A a = A.class.newInstance(); |
使用Constructor类的newInstance方法(反射机制)
1 | Constructor<A> constructor = A.class.getConstructor(); |
使用Clone方法创建对象
使用(反)序列化机制创建对象
对象创建过程
加载(class loading)
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的的静态存储结构转化成访问区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证(class verification)
文件格式验证、元数据验证、字节码验证、符号引用验证等等。
如验证是否以0xCAFEBABE开头
准备(class preparation)
为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配到Java堆中。
正常情况下,这里初始化的值是静态变量的数据类型的默认值,而不是属性指定的值,如果它还被final修饰了,那么将会在这个阶段直接初始化成属性指定的值。
解析(class resolution)
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
类初始化(class initalizing)
类初始化就是执行<clinit>()
方法,<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,静态语句块中只能访问到定义在静态语句之前的变量。
也就是说,静态属性和静态代码块的赋值和调用在初始化过程中执行,先执行父类的,再执行子类的。
实例初始化
实例初始化过程,就是执行<init>()
方法
<init>()
方法可能重载有多个,有几个构造器就有几个<init>
方法
<init>()
方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成
非静态实例变量显示赋值代码和非静态代码块代码从上到下顺序执行,而对应构造器的代码最后执行
每次创建实例对象,调用对应构造器,执行的就是对应的<init>
方法
<init>()
方法的首行是surper(或super(实参列表),即对应父类的<init>
方法
类初始化与实例初始化,在子父类内部执行顺序
1 | 父类静态成员 |
样例:

上图执行结果:
1 | 5、1、10、6、9、3、2、9、8、7 |
1 | public class T001_ClassLoadingProcedure { |
1 | public class T001_ClassLoadingProcedure { |
如果是
Object o = new Object()
,有以下几步:1、申请内存空间,这时候成员变量均是默认值
2、调用构造方法,初始化成员变量值
3、建立栈上和堆内存对象的关联关系
1 | //当我们调用构造方法时,java的底层的字节码指令如下: |
类加载器
如果一个类加载器收到了类加载的请求,它不会先尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己加载。
这里的父-子是通过使用组合关系,成员变量有个叫做parent的属性记录上层的类加载器,而不是继承关系。
父类加载器不是类加载器的加载器,也不是类加载器的父类加载器。双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程。
为什么用双亲委派机制?
安全,保证了Java程序的稳定运行。避免核心类库被用户覆盖。
查看各个类加载器加载的路径及信息可以查阅Launcher.java
1 | BootStrap ClassLoader:sun.boot.class.path |
JDK破坏双亲委派机制的历史
双亲委派模型的第一次被破坏发生在双亲委派模型出现之前,由于双亲委派模型在JDK1.2之后才被引入,为了向前兼容,JDK1.2之后添加了一个findClass()方法。
双亲委派模型的第二次被破坏是由于模型自身的缺陷导致的,有些标准服务是由启动类加载器(Bootstrap)去加载的,但它又需要调用独立厂商实现并部署在应用程序的ClassPath下的代码,为了解决这个问题,引入了线程上下文类加载器,如果有了线程上下文类加载器,父类加载器将会请求子类加载器去完成类加载动作。
双亲委派模型的第三次被破坏是由于用户对程序动态性的追求导致的。如热替换、热部署。
假设每个程序都有一个自己的类加载器,当需要更换一个代码片段时,就把这个代码片段连同类加载器一起换掉实现代码的热替换。
当我们需要定义自己的类加载器时,继承ClassLoad,重写findClass()方法,当调用loadClass()加载class,找不到时会调用我们自定义的findClass(),读取要加载的文件流,调用defineClass()去真正加载,保证了双亲委派机制
若要破坏双亲委派模型,我们可以直接重写loadClass()方法,直接加载指定的class,没有的情况下再走parent的loadClass()。
1 |
|
编译器和解释器
Java默认采用混合模式,初期通过编译器编译Class文件的代码,当出现热点代码时,会通过JIT
解释器把热点代码解释成本地代码,提高运行效率。
1 | # 热点代码的阈值频次 |