对象创建与内存分配
对象创建与内存分配
当我们在Java中创建一个对象时,JVM会执行一系列步骤来完成对象的创建和初始化。本文则记录对象在JVM中完整的创建流程。
当我们去new一个对象的时候,完整的流程如下:类加载检查、内存分配、初始化、设置对象头、执行init()。
类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
内存分配
类加载完成之后,会为新对象进行内存分配。对象所需的内存大小在类加载完成之后便可以确定。此时为对象分配内存空间等于把一块确定大小的内存从JVM堆中划分出来给对象使用。
在进行堆内存划分的时候会存在两个问题:如何划分问题和并发问题。
内存划分方式
为新对象在堆中划分内存,划分方式有两种:指针碰撞(默认使用)和空闲列表。
指针碰撞(Bump the Pointer)
通过一个指针指向分界点,这个指针表示已分配和未分配内存的分界。当需要分配内存时,将指针往空闲一侧移动与对象大小相等的距离即可。
需要注意的是这需要Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边。主要用于Serial和ParNew等不会产生内存碎片的垃圾收集器。
空闲列表(Free List)
JVM维护一张内存列表,记录可用的内存块信息。当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。
空闲列表即使是Java堆中的内存并不规整,已分配内存和未分配内存交错也是适用的。最常见使用此方案的垃圾收集器是CMS(Concurrent
Mark-Sweep)。
并发问题处理
在并发情况对象在内存划分时,可能会出现正在给A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况,导致数据不一致或内存分配错误。
JVM采用以下方法来解决并发问题:CMS和本地线程分配缓冲(TLAB)。
CMS(Compare and Swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
本地线程分配缓冲(Thread Local Allocation Buffer)
为每个线程在Java堆中预先分配一小块内存,哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
虚拟机是否使用TLAB,可以通过 -XX:+/-UseTLAB(默认开启) 参数来设定。
栈上分配
通常Java对象都是在堆上分配内存,当对象没有被引用时,光依靠GC进行回收内存,对象创建过多就会给GC带来较大的压力,间接影响应用的性能。
为了减少临时对象在堆内存分配的数量,JVM通过逃逸分析技术确定对象不会被外部访问,则说明对象不会逃逸出改栈帧,可以将对象在栈上分配内存,这样对象占用的内存就会随着栈帧出栈而销毁,达到减轻GC回收的压力。
逃逸分析技术还需要结合热点探测技术,以此为前提,详情可在 JIT(即时编译)深入理解 文章查看。