目录
1、对象的创建与内存分配
当虚拟机遇到一条new字节码指令时,会先检查这个类型是否已经加载。如果还没有,就进行类加载过程
在类加载检查通过后,虚拟机将为新生对象分配内存。
一个对象所需的内存大小,在类加载完成后就可以确定下来,所以只需要把一块确定大小的内存从堆中划分出来。
1、如何从堆上划分内存
Java堆的内存管理有两种方式:
- 指针碰撞:堆内存绝对规整,一边存储数据,一边空闲,中间用一个指针分隔。分配内存只需要挪动指针即可
- 空闲列表:已使用的内存和空闲内存混在一起,虚拟机需要维护一个列表来记录哪些内存块是可用的。分配内存时从列表上划分。
虚拟机选择哪种内存管理方式,与Java堆是否规整有关。而堆是否规整,又取决于该虚拟机使用的垃圾回收器是否带有“空间压缩整理”功能决定。
- 当使用Serial、ParNew这类带空间压缩整理的收集器,就采用指针碰撞,简单高效
- 使用CMS这种基于清除(Sweep)算法的收集器,理论上就需要采用空闲列表来分配内存
2、划分内存如何保证并发安全
对象创建是虚拟机中非常频繁的操作,如果不作处理,很可能出现正在给A分配内存,还没有完成,B又来使用原先的内存状态分配内存的情况。
虚拟机使用两种方式确保线程安全:
-
对分配内存空间的动作进行同步。(具体是采用乐观锁+失败重试的方式)
-
把内存分配的动作按照线程分配在不同的空间进行。
每个线程在
堆中的Eden区
预先分配一小块内存,作为“本地线程分配缓冲(TLAB)”,线程优先在自己的TLAB中分配内存
,不够用了再进行同步。
虚拟机是否启用TLAB,通过这个参数来设定:
-XX:+/-UseTLAB
3、内存分配完成之后的工作
内存分配出来了,虚拟机对分配到的这部分空间进行处理,把除了对象头之外的地方都初始化为零值,接着填充对象头。
此时对虚拟机来说,对象就创建出来了。但这是一个空白对象,Java代码中的构造函数还未执行,所以对象还尚未初始化。
只有new指令执行完后,执行<init>()构造方法,一个可用的Java对象才被完整创建出来。
4、总结 Java对象的创建过程
Java对象的创建过程:
类型检查、分配内存、初始化零值、设置对象头、执行构造方法
-
遇到new关键字,先检查这个指令的参数是否能在常量池中找到该类型的符号引用。
-
如果找到了,检查这个类型是否已经完成加载并初始化
-
如果没有找到,说明类还没有加载,先进行类加载过程。
-
类加载的检查阶段通过后,这个类的对象需要占用的内存大小就已经确定了。JVM就会给对象分配内存
-
分配内存涉及到三个细节:
-
内存分配的两种方式:指针碰撞、空闲列表
-
内存分配的线程安全:乐观锁+失败重试
-
对于小对象,线程优先在堆中自己的“本地线程分配缓冲区 TLAB”上分配内存
对于大对象,可以选择直接放入老年代
-
-
-
处理分配到的内存空间,把除了对象头之外的地方都初始化为零值,这样对象的成员变量就有默认值了
-
填充对象头,设置对象的类型、分代年龄、是否启用偏向锁。hashcode会在第一次调用时懒加载。
-
执行该对象的构造方法
然后就获得了一个可用的Java对象。
5、对象内存分配的基本策略
整体来说:
- 新对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
1、新对象优先在Eden区分配
如果Eden区空间不足,就会触发一次Minor GC。
- 如果在新生代GC的期间,Eden区的存活对象很多,survivor区放不下,就会通过分配担保机制,把新生代的对象复制到老年代中。
- 如果老年代的空间也不足,就会触发一次Full GC,这个比较耗时。
2、为什么大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
有两方面考虑:
- 可能新生代的Eden区内存空间不足,不得不提前触发一次GC。因为大对象有更大的概率会遇到内存不足的情况。
- 以后的新生代GC,如果大对象存活了下来,suvivor区可能放不下,还是会通过分配担保机制进入老年代。所以可以选择直接放入老年代。
有一个参数:
-XX:PretenureSizeThreshold
大于这个数量直接在老年代分配。缺省为0 ,表示不会直接分配在老年代。
3、长期存活的对象将进入老年代
每个对象会保存一个分代年龄,每熬过一次GC,分代年龄就+1
当分代年龄超过阈值,就会晋升到老年代。
可以通过这个参数调整,默认15
-XX:MaxTenuringThreshold
HotSpot在这里用了动态分代年龄的机制,在分代收集理论中有记录。
2、对象的内存布局
在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
1、对象头
对象头部分包含两类信息:
- mark word:用于存储对象自身的运行时数据,如哈希值、CG分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- klass word:类型指针,即对象指向它的类型元数据的指针,JVM通过这个指针来确定该对象是哪个类的实例。
如果对象是一个数组,对象头还必须存储它的长度,否则无法确定数组对象的大小。
2、实例数据
存放对象的有效信息,包括代码中定义的和从父类中继承的。存储顺序有两方面影响:
- 代码中的书写顺序
- 虚拟机分配策略
默认的分配策略是,相同宽度的字段会被分配到一起存放。以这个条件为前提,父类的变量会在子类的之前。
如果HotSpot开启这个参数,那么子类中的较窄变量可以允许插入父类变量的空隙中,节省一点空间
+XX:CompactFields:true //默认就为true
3、对齐填充
这部分起到占位符的作用。
HotSpot虚拟机的自动内存管理系统要求,对象起始地址必须是8字节的整数倍,所以任何对象的大小都必须是8字节的整数倍。
对象头的部分已经被精心设计成了8字节或16字节,而实例数据部分内容无法保证长度,不够8字节的地方,用对齐填充来补齐即可。
3、对象的访问定位
对象的访问定位方式是指,栈上的引用如何指向堆上的对象。
Java程序会通过栈上的 reference 数据来操作堆上的具体对象
,这个 reference 类型只被固定成是一个引用,而没有指定实现方式。
所以虚拟机可以自由实现对象的访问方式。主流的方式有这两种:
- 使用句柄访问:
- Java堆中划分出一块内存作为句柄池
- reference 中存储对象的句柄地址
- 句柄中包含了对象的“实例数据”与“类型数据”各自具体的具体地址信息。
- 使用直接指针访问:
- reference 中直接存储对象的具体地址
两种方式各有优势:
- 句柄访问的最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动时(比如发生垃圾回收),只会改变句柄中的实例数据指针,而不需要修改 reference 。
- 直接指针访问的好处就是速度更快,节省了一次指针定位的开销。对象访问操作非常频繁,这也是HotSpot使用的方案。
具体的对象访问定位方式和GC的类型有关。