Java类的加载和实例化全过程解析
本篇解析中,默认首次new时类还未加载,并使用的是应用程序类加载器。
1.类加载
1.1 类加载器选择
根据双亲委派模型,选择出该类的类加载器。
1.2 类装载
- 类加载器通过全限定名获取类的.class文件。
- 解析二进制数据流中的数据转换为方法区运行时的数据结构,类似于封装的一个过程。
- 堆中创建该类的Class对象,作为方法区数据访问的入口。
(数组类型不通过类加载器创建,而是JVM直接创建)
1.3 类验证
验证是为了保证加载的字节码是合法并符合规范的。
主要验证的有格式检查
(版本号,长度,魔数)、语意检查
(检查规定的规则,例如是否继承了final,抽象方法是否有实现这类的)、字节码验证
(参数的赋值类型是否一致,函数调用是否传递正确参数这类)、符号引用验证
(检查引用的类、方法是否存在)。
1.4 分配内存
类加载检查通过后,JVM为新对象分配内存,对象所需的内存大小在类加载完成后就可以确定了。
分配内存的方式有指针碰撞
和空闲列表
两种,选择哪一种分配方式由堆内存是否规整在决定,堆内存是否规整由采用的垃圾回收器决定(会不会产生内存碎片)。
指针碰撞:
- 适用场合:适用于
没有内存碎片
的情况下。- 原理:垃圾清理时,生存的对象全部整合到一边连续的内存中,中间有个分界指针,只要从指针开始向未分配的方向移动指定大小的空间即可。
- GC回收算法:使用标记压缩法、复制算法等整理内存的算法。
空闲列表:
- 适用场合:适用于堆内存不规则的情况下
- 原理:JVM会维护一个列表,列表中记录哪些内存块是可用的和大小,在分配的时候,从列表中寻找足够大的内存块划分给对象实例,然后更新列表
内存分配时主要是使用CAS乐观锁来解决线程安全问题。
1.5初始化零值
分配完内存后,为对象中的成员变量设置“零值”,例如int类型零值为0,布尔类型零值为false。
1.6设置对象头
初始化零值完毕后,虚拟机需要对对象头中的信息进行设置,例如元数据信息,对象的哈希码,对象的GC分带年龄信息等等。
1.7执行init方法
完成上述方法后,对象在虚拟机中算是创建完成了,接着就需要按照程序的编写进行初始化,例如对成员变量的赋值,构造方法的执行等。
特例
String类型
String类型的创建比较特殊,因为它内部维护了一个常量池,常见的创建String的方式有:
String str = "abc";
String str2 = "abc";
String strObj = new String("abc");
String strObj2 = new String("abc");
System.out.println(str == str2); // true
System.out.println(str == strObj); // false
System.out.println(strObj == strObj2); // false
- 使用
双引号
声明的String类型是在常量池
中创建的,所以str和str2对比的是常量池中的地址 - 使用new关键字创建的都是堆内存中的新对象,所以后面两个的答案都是false。
- intern() 这是一个native方法,它的作用是如果常量池中含有此String对象的值,则返回常量池中该字符串中的引用;如果没有则在常量池中创建一个与此String内容相同的字符串,并返回常量池中创建的字符串的引用。
String str = new String("abc");
String str2 = str.intern();
String str3 = "abc";
System.out.println(str2 == str3); // true