- 普通java对象创建(不包括数组及Class对象)
对象的创建对应于虚拟机执行一条new指令这一过程。在执行new指令时,虚拟机会做出如下操作:
1. 检查参数是否可以被定位到常量池中的一个类的符号引用C。
2. 将此C所代表的类是否已被加载(此步可以确认对象所需内存大小),解析,初始化。如果没有则先进行类加载;如果已经完成,则转到步骤三。
3. 虚拟机为新生对象分配内存(有指针碰撞、空闲列表两种方式)并将分配到的内存空间都初始化为零值。
4. 虚拟机根据对象头中的信息对对象进行设置,确定对象属于哪个类,如何才能找到类元信息,对象的哈希码,对象的GC分代年龄等信息。至此,虚拟机角度一个新的对象已经产生。但是此时所有字段还都为零值。在new指令之后会接着执行<init>方法,把对象按找程序员设定的值进行初始化,至此才算产生一个完整的java对象。
类从被加载到虚拟机开始,到卸载出内存,主要包括:加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备、解析三个阶段统称为连接。而类加载的整个过程具体包括:加载、验证、准备、解析、初始化这五个阶段的动作。
- 加载
加载是类加载的一个阶段,具体完成以下动作:
1. 通过一个类的全限定名来获取该类的二进制字节流。
2. 将这个字节流所代表的静态存储结构转换为方法区运行时数据结构。
3. 在内存中生存一个java.lang.Class对象,作为方法区这个类的各种数据访问入口。 - 验证
检查二进制流的合法性 - 准备
正式为类变量分配内存并设置初始值(一般为数据类型的零值)。注意只有类变量,实例变量会在对象实例化时一起被分配到堆中。而这些类变量都位于方法区。
public static int value = 1; 准备阶段过后value的值为0。 - 解析
虚拟机将常量池中的符号引用替换为直接引用。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定点符7类符号引用进行。 - 初始化
类加载过程的最后一步,对应于执行类构造器<clinit>()方法的过程, 该方法包括所有编译器自动收集的类中静态变量的赋值及静态代码块中的语句。在子类的<clinit>()方法调用之前,虚拟机栈保证一定会先调用父类的<clinit>()。同一个类加载器,一个类型只会初始化一次。
- 虚拟机规范规定仅在五种情形下必须立即对类进行初始化:
- 遇到new、getstatic、putstatic、invokestatic字节指令时。
生成这四个指令最常见的情况是:使用new指令创建对象、读取或设置一个静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用类的静态方法时。
- 反射。
- 初始化子类,如果父类没有进行过初始化,则会触发父类初始化。
- 虚拟机启动时,会将main入口方法所在类初始化。
- JDK 1.7的动态语言支持,方法句柄所对应的类未初始化。
- 不会进行类初始化的情形:
- 子类引用父类静态字段,不会引起子类的初始化。
- 通过数组定义来引用类,不会触发类的初始化。
- 引用一个类常量不会触发它的初始化。
对象在内存中的存储布局分为:对象头、实例数据,对齐填充三部分。
- 对象头(包括两部分):
(1)Mark Word。存储运行时数据,包括GC年龄分代、锁状态标志、偏向线程ID等。
(2)类型指针。对象指向它的类元数据的指针,用以确定这个对象属于哪个类。
(*)对于数组对象,在对象头中还要额外开辟一块内存用于存储数组长度。 - 实例数据:
这部分时对象真正的有效信息,存储着代码中定义的所有字段内容,包括其从父类继承的字段。 - 对齐数据:
保证对象其实地址是8的整数倍。
对象主要分配在Eden区,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,有时也会直接分配到老年代。分配的规则并不固定,取决于使用了哪一种垃圾回收器组合及虚拟机中内存相关参数的设定。
大致有以下几种普遍规则:
- 对象优先在Eden区分配,如果Eden区域大小足够,就直接分一块内存。否则会触发一次Minor GC。
在触发Minor GC前会检查老年代剩余的最大连续空间(设大小为X)是否大于新生代中所有对象所占空间的总和,如果大于则保证Minor GC是安全的,可直接进行Minor GC。否则进一步判断是否支持担保失败,如果支持并且X的大小>历次晋升到老年代对象的和平均值,则先试着进行一次Minor GC,否则先进行一次Full GC。 - 如果为大对象(大对象即需要大量连续存储空间的对象,如很长的字符串或数组),当其大小达到一个阈值(-XX:PretenureSizeThreshold)时会被直接分配到老年代。以避免新生代区域内部之间发生大量的内存复制。
- 对于长久存活的对象将进入老年代。对象每熬过依次Minor GC就会将其对象头内的年龄计数器加一,到达一定阈值(默认15,-XX :MaxTenuringThr eshold)就会晋升到老年代。
- 动态年龄判定。如果在Survivor空间相同年龄的所有对象的大小的总和大于Survivor区域空间的一半,那么年龄大于或者等于该年龄的对象也会被移入老年代,无需等到达到(3)中的阈值。
《深入理解java虚拟机》