前言
突然想到学习一下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 | public void test(){ |
上面的代码虽然能获取到数组存储的真实的值,但是这不一定就是对象真实的内存地址,这是因为Java会做一些处理,这个下一章节再讲
Unsafe的几个方法介绍
- allocateMemory(long),申请指定字节的堆外内存,返回的申请到的内存开始字节的地址
- getInt(long):获取指定内存地址的int值,会自动读取当前位置开始四个字节的值(因为int是四字节),但是这种方式只支持访问堆外内存,除了getInt(long),还有getByte(读取一字节),getChar(读取两字节),getLong(读取8字节),除了get,还有put,具体参考Unsafe类
- getInt(Object, long):获取指定对象指定偏移量的int值,会自动读取当前位置开始四个字节的值,如果想过指定内存地址获取int值,可通过getInt(null, address),除了getInt(Object,long),还有getByte(读取一字节),getChar(读取两字节),getLong(读取8字节),除了get,还有put,具体参考Unsafe类
- getObject(Object, long),它和上面的getInt不一样,它分为两步,它先是获取到指定对象指定偏移量的int(对象引用大小为四字节)或者long(对象引用大小为八字节),这一步实际上就是获取对象引用,然后第二步就是通过引用访问具体的对象,哪一步出现问题都会导致JVM报错
- arrayBaseOffset(Class>)和arrayIndexScale(Class>):这两个方法分别可以获取数组头部偏移量以及数组单元素字节大小,对于Field也有类似的方法,具体参考Unsafe类
jol-core
jol-core是一个用于分析和展示 Java 对象内存布局的强大工具,可以很清晰查看具体对象的内存布局,获取具体对象内存地址以及获取指定内存地址的值,使用jol-core需要先引入依赖
1 | <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 | public static void test(){ |
可以看到,a和b是同一个对象,而c是另外一个,接下来测试一下addressC存储的是什么
这里先讲一下自己的思路,本来以为c的地址存储的应该是一个int值,这个值经过转换后应该是和addressA还有addressB一样的值,但是测试结果addressC存储的就是一个String实例,而后我又测试了一下String的char[]
1 | public static void test(){ |
可以看到,三个地址的char数组的引用地址是一致的,并且引用地址的内容没有问题
总结
经过以上测试,我的观点是:String和char[]在常量池中是分开存储的,而常量池中的String对象的char[]则指向常量池中的char[]
- 如果是String str = “hello”,则直接返回常量池中的String对象(没有则在常量池中创建)
- 如果是String str = new String(“hello”),那么则会创建一个String对象,然后这个String对象中的char[]指向常量池中的char[],而不是创建一个指向常量池中String对象的指针
- 如果使用intern,会返回常量池中的String对象(没有则在常量池中创建)
这一篇介绍的String对象都是明确字符串内容的,没明确字符串内容的String对象和本篇的情况又不一样(比如String str = FileUtil.readStr(File)这种),这些在另一篇文章里再提(现在还没写)