在Java的世界中,可以有多种方式生成一个对象。new是其中一种,也是使用最频繁的一种。今天咱们就来八一八new的实现细节以及与new相关的一些高端面试题。
如果你想对JVM了解更深,可以看我讲解的公开视频:JVM底层原理教程
new的实现细节我会从两个角度分析:Java字节码层面、openjdk源码层面。
与new相关的面试题有哪些呢?一、如何理解new是非原子操作?二、在高并发环境下,DCL需要加volatile,为什么?
对象大小是何时知晓的
new是用来生成对象的,对象是有大小的,那对象的大小是何时确定的呢?如果你没有看过openjdk源码,你肯定以为是在生成对象的时候动态计算的。但事实上呢,是在类加载阶段就已经计算出来了。上证据:
InstanceKlass::InstanceKlass(int vtable_len,
int itable_len,
int static_field_size,
int nonstatic_oop_map_size,
ReferenceType rt,
AccessFlags access_flags,
bool is_anonymous) {
……
// Set temporary value until parseClassFile updates it with
//the real instance size.
set_layout_helper(Klass::instance_layout_helper(0, true));
……
这段代码上面的注释的意思是:暂时设置一个临时值,直到parseClassFile 函数执行时用真实的实例的大小来update
还有没有证据呢?还有很多,再上一个
CASE(_new): {
……
InstanceKlass* ik = (InstanceKlass*) k_entry;
……
// 获取基于目标类生成的对象的大小
size_t obj_size = ik->size_helper();
……
这是JVM处理new指令的逻辑,可以看到这时候不是在计算对象大小,而是从InstanceKlass类型的ik中去取。
内存的分配方式
上面说到一个对象的大小是对象所属类的class文件在加载时就已经计算出来了,那现在我们知道要分配多大内存,那要如何分配呢?
这里讲到两个分配策略:指针碰撞、空闲列表。那如何理解这两个分配策略呢?JVM又是采用哪种分配策略的呢?
JVM采用何种分配策略是由当前使用的垃圾回收器决定的。为什么这么说呢?因为指针碰撞只能用于内存规整的环境中,而空闲列表就没有这样的前提条件。那全部用空闲列表不就好了,为什么还要设计指针碰撞呢?是时候对这两种策略深入讲解一下了。
指针压缩只能用于内存规整的环境中,即一边是已使用的内存,一边是未使用的内存,交界处即为碰撞处,名曰分界点的指示器。分配内存的时候是怎么做的呢?将指示器向未使用内存这边移动一段对象大小的距离,底层是通过CAS循环碰撞,CAS执行成功即碰撞成功,成功分配到内存。可以发现这种方式的核心是前期使用内存的约束要做好,因为不需要借助其他数据结构辅助实现,所以保证了分配内存的高效性。
JVM处理new的逻辑中就是采用的指针碰撞,因为新生代采用的是分代-复制算法,所以堆内存是规整的。看代码:
……
// 指针碰撞在堆区分配内存(其实就是循环执行CAS)
retry:
HeapWord* compare_to = *Universe::heap()->top_addr();
HeapWord* new_top = compare_to + obj_size;
if (new_top <= *Universe::heap()->end_addr()) {
if