0%

类的加载与创建

jvm

Class格式

Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。

无符号数

属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。

是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:

jvm

从二进制的数据来看:

jvm

通过javap编译成可视化语言来看:

jvm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

cafe babe 0000 0034 000f 0a00 0300 0c07
000d 0700 0e01 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0004
6d61 696e 0100 1628 5b4c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b29 5601 000a
536f 7572 6365 4669 6c65 0100 134a 766d
436c 6173 7346 6f72 6d61 742e 6a61 7661
0c00 0400 0501 0013 6b75 726f 2f4a 766d
436c 6173 7346 6f72 6d61 7401 0010 6a61
7661 2f6c 616e 672f 4f62 6a65 6374 0021
0002 0003 0000 0000 0002 0001 0004 0005
0001 0006 0000 001d 0001 0001 0000 0005
2ab7 0001 b100 0000 0100 0700 0000 0600
0100 0000 0300 0900 0800 0900 0100 0600
0000 1900 0000 0100 0000 01b1 0000 0001
0007 0000 0006 0001 0000 0006 0001 000a
0000 0002 000b
1
2
3
4
5
6
7
魔法数字: cafe babe
次版本号: 0000
主版本号: 0034 JDK1.8
常量数量: 000f 从1开始
#1常量 : 0a 表示表中第十项(CONSTANT_Methodref_info)
: 00 03 指向#3常量
: 00 0c 指向#13常量

jvm

详情可查阅查阅:

jvm

对象创建方式

​ 使用new关键字创建对象

1
A a = new A();

​ 使用Class类的newInstance方法(反射机制)

1
A a = A.class.newInstance();

​ 使用Constructor类的newInstance方法(反射机制)

1
2
Constructor<A> constructor = A.class.getConstructor();
A a = constructor.newInstance();

​ 使用Clone方法创建对象

​ 使用(反)序列化机制创建对象

对象创建过程

jvm

加载(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
父类静态成员
父类静态代码块
->子类静态成员
->子类静态代码块
->父类代码块
->父类成员变量
->父类构造器
->子类代码块
->子类成员变量
->子类构造器
类初始化
静态成员赋值的代码、静态代码块(两者先后看顺序)
实例初始化
super()
非静态赋值的代码、非静态代码块(两者先后看顺序)
无参构造

​ 样例:

上图执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
5、1、10、6、9、3、2、9、8、7
9、3、2、9、8、7

类初始化与实例初始化
5 父类静态成员j调用父类的静态方法method(),静态方法不可重写,所以不会调用子类的
1 父类静态代码块
10 子类静态成员j调用子类的静态方法method()
6 子类静态代码块
9 父类成员变量i调用子类的test(),非静态方法默认的调用对象是this,this对象在构造器或者说<init>方法中就是正在创建的对象
3 父类代码块,与成员变量之前的执行先后取决于位置的前后
2 父类构造器
9 子类成员变量i调用子类的test(),非静态方法默认的调用对象是this,this对象在构造器或者说<init>方法中就是正在创建的对象
8 子类代码块,与成员变量之前的执行先后取决于位置的前后
7 子类构造器

再次实例初始化
9 父类成员变量i调用子类的test(),非静态方法默认的调用对象是this,this对象在构造器或者说<init>方法中就是正在创建的对象
3 父类代码块,与成员变量之前的执行先后取决于位置的前后
2 父类构造器
9 子类成员变量i调用子类的test(),非静态方法默认的调用对象是this,this对象在构造器或者说<init>方法中就是正在创建的对象
8 子类代码块,与成员变量之前的执行先后取决于位置的前后
7 子类构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class T001_ClassLoadingProcedure {
public static void main(String[] args) {
//ClassLoader加载T对象:
//1.加载(loading)
//2.验证(verification)
//3.准备(preparation)--初始化静态变量默认值count = 0; T t = null
//4.解析(resolution)
//5.初始化(initalizing)--按照顺序赋值静态变量 count = 2; T t = new T();--->调用构造方法-->count++;
System.out.println(T.count); //输出3
}
}

class T {
public static int count = 2; //0
public static T t = new T(); // null


private T() {
count ++;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class T001_ClassLoadingProcedure {
public static void main(String[] args) {
//ClassLoader加载T对象:
//1.加载(loading)
//2.验证(verification)
//3.准备(preparation)--初始化静态变量默认值count = 0; T t = null
//4.解析(resolution)
//5.初始化(initalizing)--按照顺序赋值静态变量 T t = new T();--->调用构造方法-->count++; count = 1;
//--按照顺序赋值静态变量 count = 2;(覆盖掉之前的值)
System.out.println(T.count); //输出2
}
}

class T {

public static T t = new T(); // null
public static int count = 2; //0

private T() {
count ++;
}
}

如果是Object o = new Object(),有以下几步:

1、申请内存空间,这时候成员变量均是默认值

2、调用构造方法,初始化成员变量值

3、建立栈上和堆内存对象的关联关系

1
2
3
4
5
6
//当我们调用构造方法时,java的底层的字节码指令如下:
0: new #2 // class java/lang/Object 申请内存空间
3: dup // 复制内存空间地址,供以调用构造方法时出栈使用
4: invokespecial #1 // Method java/lang/Object."<init>":()V 调用构造方法
7: astore_1 // Object o 指向开辟的内存地址
8: return

类加载器

jvm

jvm

如果一个类加载器收到了类加载的请求,它不会先尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己加载。

这里的父-子是通过使用组合关系,成员变量有个叫做parent的属性记录上层的类加载器,而不是继承关系。

父类加载器不是类加载器的加载器,也不是类加载器的父类加载器。双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程。

为什么用双亲委派机制?

安全,保证了Java程序的稳定运行。避免核心类库被用户覆盖。

查看各个类加载器加载的路径及信息可以查阅Launcher.java

1
2
3
BootStrap ClassLoader:sun.boot.class.path
ExtClassLoader:java.ext.dirs
AppClassLoader:java.class.path

JDK破坏双亲委派机制的历史

双亲委派模型的第一次被破坏发生在双亲委派模型出现之前,由于双亲委派模型在JDK1.2之后才被引入,为了向前兼容,JDK1.2之后添加了一个findClass()方法。

双亲委派模型的第二次被破坏是由于模型自身的缺陷导致的,有些标准服务是由启动类加载器(Bootstrap)去加载的,但它又需要调用独立厂商实现并部署在应用程序的ClassPath下的代码,为了解决这个问题,引入了线程上下文类加载器,如果有了线程上下文类加载器,父类加载器将会请求子类加载器去完成类加载动作。

双亲委派模型的第三次被破坏是由于用户对程序动态性的追求导致的。如热替换、热部署。

假设每个程序都有一个自己的类加载器,当需要更换一个代码片段时,就把这个代码片段连同类加载器一起换掉实现代码的热替换。

当我们需要定义自己的类加载器时,继承ClassLoad,重写findClass()方法,当调用loadClass()加载class,找不到时会调用我们自定义的findClass(),读取要加载的文件流,调用defineClass()去真正加载,保证了双亲委派机制

若要破坏双亲委派模型,我们可以直接重写loadClass()方法,直接加载指定的class,没有的情况下再走parent的loadClass()。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//File To byte[]
byte[] bytes = FileUtils.readFileToByteArray(new File("xxx"));
//调用父类的defineClass装载
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name); //throws ClassNotFoundException
}

编译器和解释器

Java默认采用混合模式,初期通过编译器编译Class文件的代码,当出现热点代码时,会通过JIT解释器把热点代码解释成本地代码,提高运行效率。

1
2
3
4
5
6
# 热点代码的阈值频次
-XX:CompileThreshold = 10000
# 使用编译器运行
-Xcomp
#使用解释器运行
-Xint