对象创建过程与内存分配
对象创建主要流程
-
类加载检查
例如:虚拟机接到一个new User() 指令的时候,会先去检查User()这个符号引用是否在常量池中,并且检查User类是否已经被加载,如果没有,就必须先加载类。
-
分配内存
通过类加载检查之后,就会为new 出来的对象分配内存。
划分内存的方法:
①指针碰撞
②空闲列表
并发问题解决办法:
①CAS
②本地线程分配缓冲(Thread Local Allocation Buffer )
-
初始化
内存分配完成后,将分配到的内存空间除对象头以外都初始化为零值,如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
-
设置对象头
对象在内存中分为三块区域:
①对象头:hashcode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID ,偏向时间戳,类型指针(类元数据的指针),数组对象有数组长度。
②实例数据:对象真正存储的有效信息,也是程序中所定义的各种类型的字段内容。
③对齐填充:保持对象大小是8的倍数,读取是最快的,这就是对齐的原因
-
执行方法
属性赋值,和执行构造方法。
堆内存分配方式
分配方式有
“指针碰撞” 和 “空闲列表”
两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。GC器的算法是“标记清除”(不规整、有内存碎片)还是“”标记整理(规整),复制算法也是规整的。
指针碰撞
用过的内存全部整合到一边,没有用过的内存放在另一边,中间有个分界指针,只需要向着没用过的内存方向将该指针移动内存大小位置即可。在堆内存规整的情况下使用(Serial,ParNew)
空闲列表
虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块来划分给对象实例,然后更新表记录。堆内存不规整的情况下使用(CMS)。
线程安全保证方式
CAS+失败重试
CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是 设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB
为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
栈上分配
如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以减小垃圾收集器的负载。
JVM允许将线程私有的对象标量替换后分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。
逃逸分析
逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。
标量替换
允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。
-XX:DoEscapeAnalysis启用逃逸分析;
-XX:+EliminateAllocations开启标量替换(默认打开);
-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果