JVM是如何得到数组长度的
这篇笔记主要记录下我们在java中编写的数组,比如int arr[] ={1,2,3};,那么我们使用int len = arr.length;jvm底层是如何得到数组长度的;
数组在jvm底层是动态 产生的,也就是说不是静态的,类似于我们得到一个对象的hashcode一样,如果你没有重写过hashcode方法,那么对象的hashcode默认就是对象的内存地址,所以hashcode也是动态产生的,我们的数组也是一样的,我们定义好数组过后,数组在运行时是可以动态生成、动态添加以及动态删除的,所以数组的长度也是动态产生的;我们这边通过程序和HSDB来证明数组的长度在jvm底层是如何得到的,先看以下程序:
public class T0819 {
public static void main(String[] args) {
int arr [] ={1,2,3};
int length = arr.length;
System.out.println(length);
}
}
我们查看下字节码信息,通过javap -verbose T0819.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=3, args_size=1
0: iconst_3
1: newarray int
3: dup
4: iconst_0
5: iconst_1
6: iastore
7: dup
8: iconst_1
9: iconst_2
10: iastore
11: dup
12: iconst_2
13: iconst_3
14: iastore
15: astore_1
16: aload_1
17: arraylength
18: istore_2
19: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
22: iload_2
23: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
26: return
LineNumberTable:
line 7: 0
line 8: 16
line 9: 19
line 10: 26
LocalVariableTable:
Start Length Slot Name Signature
0 27 0 args [Ljava/lang/String;
16 11 1 arr [I
19 8 2 length I
}
看程序计数器17,arraylength,说白了要搞清楚数组长度在jvm中是如何动态得到的就要看jvm是如何处理字节码指令arraylength的;我这边找到openjdk是如何获取数组长度的一段代码块:
CASE(_arraylength):
{
arrayOop ary = (arrayOop) STACK_OBJECT(-1);
CHECK_NULL(ary);
SET_STACK_INT(ary->length(), -1);
UPDATE_PC_AND_CONTINUE(1);
}
int length() const {
return *(int*)(((intptr_t)this) + length_offset_in_bytes());
}
CASE代码中就是获取我们的数组长度的,其中case中的第三行代码就是获取长度然后压入栈
我们主要来分析下lengnth()这个方法,其中通过this指针得到数组的首位置,然后加上数组的偏移量进行相加就得到我们的数组长度 ,我们看下图:
((intptr_t)this)=得到我们数组的首位置
length_offset_in_bytes()=得到我们数组在内存中的偏移量
然后相加,取地址就得到了我们的数组长度
我们再来看下获取偏移量这个函数length_offset_in_bytes()
//如果不压缩,则在arrayOopDesc中声明的非静态字段之后分配
//如果压缩,它将占用oopDesc中_klass字段的后半部分
static int length_offset_in_bytes() {
return UseCompressedClassPointers ? klass_gap_offset_in_bytes() :
sizeof(arrayOopDesc);
}
看上面代码有个参数UseCompressedClassPointers ,这个参数也是我们jvm调优的一部分,这个参数是什么意思呢?我们再上面的笔记中知道我们的对象内存布局有对象头,实例数据和对齐填充;
而对象头又分为mark word、类型指针和数组长度,而类型指针压缩和不压缩的字节数是不一样的,
那么UseCompressedClassPointers 这个参数和UseCompressedoops有什么区别呢?
UseCompressedoops是压缩的是对象指针的长度,而UseCompressedClassPointers 压缩的是Klass对象的指针的长度,如果我们开启了UseCompressedoops,那么UseCompressedClassPointers 默认开启,
也就是说UseCompressedoops包含UseCompressedClassPointers 。
首先我们来分析下数组:
对应的Klass是:TypeArrayKlass实例
对应的oop是:TypeArrayOop实例
而类型指针在jvm中的代码块是:
union _metadata {
Klass* _klass; 8B
narrowKlass _compressed_klass; 4B
} _metadata;
它是一个联合体,整个联合体占用8B的空间,如果我们是压缩的,那么会使用4B,如果我们不压缩,那么会使用8B,那么我们如果压缩了的话,那是不是就浪费了空间,length_offset_in_bytes()方法的注释已经写上了,我们下面就开始分析整个问题。
对象的内存布局
对象头
Mark Word
Klass pointer
数组长度
实例数据
填充
我们如果开启了指针压缩的情况下:
Mark Word 8B
Klass pointer 4B
数组长度 4B
那么指针长度 + 数组长度就是=4B+4B=8B
如果我们不压缩就是=8B+4B=12B
所以我们理解下代码注释中的这句话“如果压缩,它将占用oopDesc中_klass字段的后半部分”
如果压缩,类型指针4B,因为我们的
union _metadata {
Klass* _klass; 8B
narrowKlass _compressed_klass; 4B
} _metadata;
是8B,压缩了使用的就是联合体中的_compressed_klass,而我们的数组长度是4B,所以说就是 用的_klass中后半段4B,就是这个意思,也就是说我们的数组长度用的是联合体中的后半段,如果开启了指针压缩。
我们来通过HSDB来查看下我们的数组长度:
我们是开启了指针压缩的,所以是——metadata._compressed_klass,我们再来查看下它的内存视图:
我们关闭指针压缩:-XX:-UseCompressedClassPointers
一看上图就是知道是关闭了指针压缩的