前言

突然想到学习一下Java内存相关知识是因为关于String实例化的问题,网上说String str = new String(“hello”)和String str = “hello”,这两个主要区别在于,前者会先生成一个String对象,在指向常量池中的”hello”,而后者会直接指向常量池中的”hello”,所以想研究一下是不是这样,记得以前学c语言的时候,查看指针是非常简单的,而Java进行了深度的封装,所以比较麻烦

数组

在讲内存之前,得先介绍一下数组,原因是因为我们没办法直接拿到对象本身的指针(具体原因还不清楚,也许对象对外只是提供了一个访问地址而已,并没有指针,而访问地址对于开发者又不可见),比如上面的str,我们是无法拿到它的指针的,也就无法拿到它的内存地址,但是数组比较特殊,因为在Java中,对于非基本数据类型(基本数据类型元素存的是元素本身),数组存的是元素的指针,即内存地址,所以可以通过数组获取对象的内存地址,比如上面的str,我们可以构建一个空String数组,然后把str放进去,这样数组第一个元素存储的就是str的内存地址

其实通过对象的变量也可以做到,比如有一个对象student,它有一个name字段,strudent实例存的也是name的内存地址,这种方式也可以获取到数据的内存地址,只是没有数组方便

Unsafe

上面介绍了可以通过数组获取数据的内存地址,但是我们在访问数组的时候,返回的也只是实例对象,而不是数组实际存储的值,这是因为Java做了处理,访问涉及到内存地址的数据的时候,都会直接返回该地址的实例对象。那么这个时候就可以通过Unsafe获取数组具体存储的值,而不是直接返回实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void test(){
Unsafe unsafe = getUnsafe();
String str = "one";
String[] strs = new String[]{str};
int offset = unsafe.arrayBaseOffset(String[].class); //获取数组类基础偏移量,一般就是object header,这里的偏移量跟对象实例没关系,只和类有关
int address = unsafe.getInt(strs, (long) offset); //以strs对象为基础,获取指定偏移量的数据,这里只填了offset,是因为object header后面就是实例数据
System.out.println(address);
}

//获取Unsafe,因为Unsafe是单例模式,并且只支持启动类加载加载的类访问,所以我们只能通过反射获取
private static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

上面的代码虽然能获取到数组存储的真实的值,但是这不一定就是对象真实的内存地址,这是因为Java会做一些处理,这个下一章节再讲

Unsafe的几个方法介绍

  1. allocateMemory(long),申请指定字节的堆外内存,返回的申请到的内存开始字节的地址
  2. getInt(long):获取指定内存地址的int值,会自动读取当前位置开始四个字节的值(因为int是四字节),但是这种方式只支持访问堆外内存,除了getInt(long),还有getByte(读取一字节),getChar(读取两字节),getLong(读取8字节),除了get,还有put,具体参考Unsafe类
  3. getInt(Object, long):获取指定对象指定偏移量的int值,会自动读取当前位置开始四个字节的值,如果想过指定内存地址获取int值,可通过getInt(null, address),除了getInt(Object,long),还有getByte(读取一字节),getChar(读取两字节),getLong(读取8字节),除了get,还有put,具体参考Unsafe类
  4. getObject(Object, long),它和上面的getInt不一样,它分为两步,它先是获取到指定对象指定偏移量的int(对象引用大小为四字节)或者long(对象引用大小为八字节),这一步实际上就是获取对象引用,然后第二步就是通过引用访问具体的对象,哪一步出现问题都会导致JVM报错
  5. arrayBaseOffset(Class)和arrayIndexScale(Class):这两个方法分别可以获取数组头部偏移量以及数组单元素字节大小,对于Field也有类似的方法,具体参考Unsafe类

jol-core

jol-core是一个用于分析和展示 Java 对象内存布局的强大工具,可以很清晰查看具体对象的内存布局,获取具体对象内存地址以及获取指定内存地址的值,使用jol-core需要先引入依赖

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>

jol-core有一个org.openjdk.jol.vm.VirtualMachine类,可以通过**VM.current()**获取该类的实例对象,它的方法大部分和Unsafe一致,底层也是调用的Unsafe,只是在Unsafe的基础上加了做了一些处理,使得对于开发者而言,更加方便,比如addressOf方法,它获取指定对象的内存地址,底层就是通过构建数组获取的,前面提到过通过Unsafe获取数组具体位置的int值,并不能代表元素的引用地址,VirtualMachine就做了专门的处理,可以将数组中的值转换为真实的内存地址,具体可以看一下源代码

jol-core还有一个强大的功能,查看对象内存布局,通过以下代码实现

1
System.out.println(ClassLayout.parseInstance(object).toPrintable());

这是一个查看String对象内存布局的例子,可以看到,可以很清晰的看到对象内存的结构

结尾

回到最开始说到的String实例化的问题,我们使用jol-core测试一下

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void test(){
String a = "hello";
String b = "hello";
String c = new String("hello");

VirtualMachine virtualMachine = VM.current();
long addressA = virtualMachine.addressOf(a);
long addressB = virtualMachine.addressOf(b);
long addressC = virtualMachine.addressOf(c);

System.out.println(addressA);
System.out.println(addressB);
System.out.println(addressC);
}

可以看到,a和b是同一个对象,而c是另外一个,接下来测试一下addressC存储的是什么

这里先讲一下自己的思路,本来以为c的地址存储的应该是一个int值,这个值经过转换后应该是和addressA还有addressB一样的值,但是测试结果addressC存储的就是一个String实例,而后我又测试了一下String的char[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void test(){
String a = "hello";
String b = "hello";
String c = new String("hello");

VirtualMachine vm = VM.current();
long addressA = vm.addressOf(a);
long addressB = vm.addressOf(b);
long addressC = vm.addressOf(c);

// addressA+12是因为在String实例对象的12个字节开始是char[]引用
// 所以vm.getInt(null, addressA + 12)获取的就是String实例对象char数组的引用地址转换前的值
// getRealAddress是将获取到的int值转换为真实的内存地址,但是并不通用,具体参考VirtualMachine
System.out.println(getRealAddress(vm.getInt(null, addressA + 12)));
System.out.println(getRealAddress(vm.getInt(null, addressB + 12)));
System.out.println(getRealAddress(vm.getInt(null, addressC + 12)));

System.out.print(vm.getChar(null, getRealAddress(vm.getInt(null, addressA + 12)) + vm.arrayBaseOffset("char")));
System.out.print(vm.getChar(null, getRealAddress(vm.getInt(null, addressA + 12)) + 2 + vm.arrayBaseOffset("char")));
System.out.print(vm.getChar(null, getRealAddress(vm.getInt(null, addressA + 12)) + 4 + vm.arrayBaseOffset("char")));
System.out.print(vm.getChar(null, getRealAddress(vm.getInt(null, addressA + 12)) + 6 + vm.arrayBaseOffset("char")));
System.out.print(vm.getChar(null, getRealAddress(vm.getInt(null, addressA + 12)) + 8 + vm.arrayBaseOffset("char")));
}

可以看到,三个地址的char数组的引用地址是一致的,并且引用地址的内容没有问题

总结

经过以上测试,我的观点是:String和char[]在常量池中是分开存储的,而常量池中的String对象的char[]则指向常量池中的char[]

  1. 如果是String str = “hello”,则直接返回常量池中的String对象(没有则在常量池中创建)
  2. 如果是String str = new String(“hello”),那么则会创建一个String对象,然后这个String对象中的char[]指向常量池中的char[],而不是创建一个指向常量池中String对象的指针
  3. 如果使用intern,会返回常量池中的String对象(没有则在常量池中创建)

这一篇介绍的String对象都是明确字符串内容的,没明确字符串内容的String对象和本篇的情况又不一样(比如String str = FileUtil.readStr(File)这种),这些在另一篇文章里再提(现在还没写)


本站总访问量:次本站总访问数:人
本站由 Peng Tong 使用 Stellar 1.29.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。