1、相应类加载检查过程
Java程序中的“new”操作会转换为Class文件中方法的“new”字节码指令。
JVM(本文特指HotSpot)遇到new指令时,先检查指令参数是否能在常量池中定位到一个类的符号引用:
(A)、如果能定位到,检查这个符号引用代表的类是否已被加载、解析和初始化过;
(B)、如果不能定位到,或没有检查到,就先执行相应的类加载过程;首先通过类加载器将类class文件,加载到内存方法区,并且会创建相应的java.lang.class类对象,虚拟机还必须以某种方式把这个class对象和存储在方法区的类型数据关联起来。
- 验证阶段:检验类的结构是否正确
- 准备阶段:对类的变量进行分配内存,并默认初始化
- 解析阶段:将二进制文件的符号引用(任何形式的字面值)解析为直接引用
2、为对象分配内存
对象所需内存的大小在类加载完成后便完全确定(JVM可以通过普通Java对象的类元数据信息确定对象大小);
为对象分配内存相当于把一块确定大小的内存从Java堆里划分出来;
(A)、分配方式:
(I)、指针碰撞
如果Java堆是绝对规整的:一边是用过的内存,一边是空闲的内存,中间一个指针作为边界指示器;
分配内存只需向空闲那边移动指针,这种分配方式称为"指针碰撞"(Bump the Pointer);
(II)、空闲列表
如果Java堆不是规整的:用过的和空闲的内存相互交错;
需要维护一个列表,记录哪些内存可用;
分配内存时查表找到一个足够大的内存,并更新列表,这种分配方式称为"空闲列表"(Free List);
Java堆是否规整由JVM采用的垃圾收集器是否带有压缩功能决定的;
所以,使用Serial、ParNew等带Compact过程的收集器时,JVM采用指针碰撞方式分配内存;而使用CMS这种基于标记-清除(Mark-Sweep)算法的收集器时,采用空闲列表方式;
后面再介绍垃圾收集算法和垃圾收集器,了解垃圾收集时应注意这里的内容;
(B)、线程安全问题
并发时,上面两种方式分配内存的操作都不是线程安全的,有两种解决方案:
(I)、同步处理
对分配内存的动作进行同步处理:
JVM采用CAS(Compare and Swap)机制加上失败重试的方式,保证更新操作的原子性;
CAS:有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做;
(II)、本地线程分配缓冲区
把分配内存的动作按照线程划分在不同的空间中进行:
在每个线程在Java堆预先分配一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB);
哪个线程需要分配内存就从哪个线程的TLAB上分配;
只有TLAB用完需要分配新的TLAB时,才需要同步处理;
JVM通过"-XX:+/-UseTLAB"指定是否使用TLAB;
3、对象内存初始化为零
对象内存初始化为零,但不包括对象头; 如果使用TLAB,提前至分配TLAB时;
这保证了程序中对象(及实例变量)不显式初始赋零值,程序也能访问到零值;
对象的成员(变量)先进行默认初始化;基本类型为基本类型默认值,引用类型为null,即引用变量的引用地址存放在栈( stack)内存中,对象的成员变量及值存放在堆内存中 ;如果new对象有引用变量指向它,栈内存存放引用变量指向null对象(未初始化,默认为null)
4、对对象进行必要的设置
主要设置对象头信息,包括类元数据引用、对象的哈希码、对象的GC分代年龄等(详见下节);
对象成员初始化,对栈内存中的成员变量指定值;
第一步显式初始化;
第二步构造代码块初始化;
5、执行对象实例方法<init>
该方法把对象(实例变量)按照程序中定义的初始赋值进行初始化;
注明:
在中期时,类加载指的是静态成员变量和静态代码块;
存在继承时:
原则:先静后非,先父后子,先块后器
执行顺序如下:
第一步:父类静态成员变量(方法区)
第二步:父类静态代码块(多个按照顺序执行)
注意:根据静态代码块和变量位置顺序初始化变量
第三步:子类静态成员变量(方法区)
第四步:子类静态代码块
第五步:父类成员变量和子类成员变量栈内存创建一片内存,指向值为null,先父类成员变量显式初始化(如果有的话)
第六步:父类代码块(父类成员变量初始化)
第七步:父类构造器
第八步:子类成员变量显式初始化(如果有的话)
第九步:子类代码块(子类成员变量初始化)
第十步:子类构造器
例子分析:
class Base {
private String name="base" ;
static {
System.out.println("Base static" );
}
public Base() {
System.out.println("base " );
System.out.println("base.name " + name );
tellName();
printName();
}
public void tellName() {
System.out.println("Base tell name: " + name);
}
public void printName() {
System.out.println("Base print name: " + name);
}
}
public class Dervied extends Base {
private String name = "dervied";
static {
System.out.println("Dervied static" );
}
public Dervied() {
System.out.println("Dervied " );
System.out.println("Dervied.name " + name );
tellName();
printName();
}
public void tellName() {
System.out.println("Dervied tell name: " + name);
}
public void printName() {
System.out.println("Dervied print name: " + name);
}
public static void main(String[] args){
new Dervied();
}
}
Base static
Dervied static
base
base.name base
Dervied tell name: null
Dervied print name: null
Dervied
Dervied.name dervied
Dervied tell name: dervied
Dervied print name: dervied
下来解释这程序过程:
声明父类成员变量name父=null,子类成员变量name=null;
显示初始化父类成员变量,name父=“base”;
执行父类构造器,执行tellName();方法,此处省略this关键字,实际是this.tellName(),this指当前对象Dervied子类,子类的name值还没有被初始化,所以默认为null,所以调用子类的tellName(),打印Dervied tell name: null,同理打印Dervied print name: null;
初始化子类成员变量name为dervied,所以打印
Dervied tell name: dervied
Dervied print name: dervied
这里单独分析类加载机制:APC
All 全盘负责:当一个类加载器加载某个类时,全盘负责将其类依赖的类一并加载;
Parent 父类委托:先父类加载器去试图加载该class,当父类加载器无法加载该class时,才从自己的类路径中加载该类;
Cache 缓存机制:缓存机制会将所有曾经类加载器加载过的类存入缓存中,当程序中需要某个类先去缓存中查找,如果查不到,则系统会读取该类的class文件继续缓存。这样算是为什么每次修改类后要重启JVM的原因
参考文献:
《new一个对象,java虚拟机做了什么》
https://blog.csdn.net/mrb1289798400/article/details/75637158
《Java对象与JVM(一) Java对象在Java虚拟机中的创建过程》