JVM对象的内存分配,内存布局和访问定位
java对象我们再熟悉不过了,但java对象是怎么在内存里分配,存储,访问的呢?下面就分析一下。
(注:虚拟机针对HotSpot,java对象仅限于普通的java对象,不包括数组和Class对象,而且是分配在堆上的对象。我们知道并不是所有对象都分配在堆上的)
对象的内存分配
在我们日常使用中,创建对象通常new一下就行了,当JVM遇到new指令时,会检查要new的这个对象的类是否已被加载、解析和初始化过。如果没有,那就要先执行相应的类加载过程,这里不讨论。
在类加载完成后,JVM会为这个新生的对象分配内存。对象所需内存的大小在类加载完成后便能确定。所以只需从堆里划分出一个确定大小的内存空间就行了。我们知道堆内存是GC的主要区域,GC靠的是垃圾收集器,
不同的垃圾收集器采用的垃圾收集算法不尽相同,GC完成后,内存存在两种情况:连续和不连续
可用内存
不可用内存
不连续内存
连续内存
如果内存是连续的,中间放一个指针作为分界指示器,给对象分配内存只需将指针向空闲的一边挪动一段与对象大小相等的距离即可,这种分配方式称为“指针碰撞”(Bump the Pointer)
如果内存是不连续的,那指针碰撞就不行了,JVM需要维护一个列表,记录上哪些内存是可用的,在分配的时候从列表里找到一块足够大的空间划分给对象实例,并更新列表的记录,这种分配方式称为“空闲列表”(Free List)
除了如何划分空间以外,还有一个问题需要考虑,那就是JVM上对象的创建是非常频繁的,在并发的情况下并不是线程安全的。解决这个问题有两种方案:
- 对分配内存空间的动作进行同步处理,可以采用CAS配上失败重试的方式保证线程安全
- 把分配内存空间的动作按照线程划分在不同的空间中进行,即每个线程在堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存就在哪个线程的TLAB上分配,只有
TLAB用完了并分配新的TLAB时,才需要同步锁定。
查看JVM参数可知,JVM是默认使用TLAB的
这里有个面试题:堆内存一定是线程共享的吗?你如果答到了TLAB的话,绝对会让面试官眼前一亮的
对象的内存布局
对象的访问定位
建立对象是为了使用对象,java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在JVM规范中只规定了一个指向对象的引用,并没有定义这个引用通过何种方式去定位、访问堆中的对象的具体位置,
所以对象的访问方式也取决于jvm的具体实现。目前主流的访问方式有使用句柄和直接指针两种。HotSpot是使用直接指针来进行对象访问的。
- 使用句柄访问,需要在堆中划分出一块内存作为句柄池,reference中存放的就是对象的句柄地址。而句柄中包含了对象实例数据和对象类型数据各自的地址信息
- 使用直接指针访问,reference里存储的直接就是对象的地址,而对象里包含了到对象类型数据的指针
两种访问方式各有优势,使用句柄来访问的好处就是reference中存储的句柄地址不会变,在对象被移动(GC时对象移动是非常普遍的行为)时只会改变句柄中实例数据的指针
直接指针的访问方式速度更快,节省了一次指针定位的时间开销