大家好,又见面了,我是你们的朋友全栈君。
在网上看了很多博客,解释也比较多,关于字符串常量池的具体位置难以分辨谁真谁假。
对于jdk8以后的版本有人说字符串常量池在元空间中,也有人说字符串常量池存在堆中。
到底谁说的对?他们的说法有依据吗?
今天让我们来一起探讨一下这个问题
有人说字符串常量池在java堆中,可又有人说常量池存在元空间中。
分享几篇知乎文章 关于jvm运行时数据区的模型: 1、面试官 | JVM 为什么使用元空间替换了永久代? 2、Java方法区与元空间
为了解决这个问题,下面我们通过Idea、VisualVm、JDK(我用的是jdk14) 和 一段测试代码来探讨一下字符串常量池的位置
将下面代码粘贴到Idea中
代码语言:javascript复制import java.util.ArrayList;
public class StringConstantPoolTest {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
for (long i = 1; ; i ) {
// 死循环
arrayList.add(String.valueOf(i).intern());
if (i % 1000_0000 == 0) {
arrayList.clear();// 清除引用,让前面产生的对象进行回收,因为内存往往不足
System.out.println(i);
}
}
}
}
在启动参数上面我们设置内存空间的值 一个是设置堆内存最大值1G,另一个是设置元空间内存最大10M
代码语言:javascript复制-Xmx1G -XX:MaxMetaspaceSize=10M
然后我们执行这段程序打开VisualVM看下内存占用,插件市场里点击装上visualGC
下面是i
执行到这个值时的的情况
程序启动后,就需要一直观看我框出来的这些参数
宿主机内存情况
在我写的测试代码中,利用死循环,不断向ArrayList中添加对象,然后每隔1000_0000
次集合被清空一次,因为考虑内存原因我才在这来这样做,不然内存很快被消耗完,清空引用后内存不足就会进行gc。
而arrayList.add(String.valueOf(i).intern());
是利用String的native修饰的intern()
方法,将字符串存入常量池中,当然这是有一个过程的,首先需要在堆中创建一个字符串对象也就是String.valueOf(i)
,因为字符串常量池没有这个字符串对象所以会将堆中这个字符串对象存入字符串常量池中,但是现在我们还不知道常量池的位置
。
排除字符串常量池在虚拟机栈、程序计数器、本地方法栈的情况(Java虚拟机规范要求的),字符串常量池要么在堆中要么就在方法区中
假设字符串常量池在堆中
通过看VisualVM我们应该是判断不出字符串常量池是否在堆中的,因为字符串对象也在堆中创建,我们无法根据内存变化判断除字符串常量池在堆中。这种假设就没法继续推断了,进行另外一种假设
假设字符串常量池在元空间
元空间有一个特点,那就是使用的是本地内存,也就是宿主机的直接内存,如果没有设置最大值10M,那么只受宿主机内存限制。 通过观察,我们发现元空间不会像堆内存变化那么明显,它是一点一点增加的,而且我们设置的内存最大值才10M,按照old区的变化早就撑爆了,可是并没有发生OOM。
那么元空间中应该不存在字符串常量池
假设字符串常量池在方法区(元空间的一部分)
如果字符串常量引用被去除了,那么内存不够会触发gc回收字符串常量池中的对象,下面的测试代码就是想让字符串常量池的对象不被回收(又要保证不OOM导致程序退出终止),如果常量池在方法区,那么方法区应该会增大,那么宿主机的内存就会被使用。
通过两个线程来完成,A线程就是前面的逻辑,而B线程则是从字符串常量池中去取出常量。
代码语言:javascript复制import java.util.ArrayList;
public class StringConstantPoolTest {
public static void main(String[] args) {
new Thread(()->{
ArrayList<String> arrayList = new ArrayList<>();
for (long i = 1; ; i ) {
arrayList.add(String.valueOf(i).intern());
if (i % 1000_0000 == 0) {
arrayList.clear();// 清除引用,让前面产生的对象进行回收,因为内存往往不足
System.out.println(i);
}
}
},"A").start();
new Thread(()->{
for (long i = 1; ; i ) {
String.valueOf(i).intern();
if (i % 1000_0000 == 0) {
System.out.println(i);
}
}
},"B").start();
}
}
测试结果是任务栏管理器的内存没有太大的变化,推测字符串常量池不在方法区。
在看《深入理解java虚拟机》第三版时关于运行时常量池的说明 运行时常量池 != 字符串常量池
是两样东西。
public class TestDemo {
@Test
public void test01() {
//
String str1 = new StringBuilder("hello").append("World").toString();
System.out.println(str1.intern());
System.out.println(str1 == str1.intern());
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern());
System.out.println(str2 == str2.intern());
String str3 = new StringBuilder("hello").toString();
System.out.println(str3.intern());
System.out.println(str3 == str3.intern());
}
}
为什么打印出
代码语言:javascript复制helloWorld
true
java
false
hello
false
根据VisualVM和宿主机内存的数据,我们大致可以推断出字符串常量池与元空间存在关联关系,因为我们通过intern()
会不断往字符串常量池中添加数据,因此存有字符串常量池的那块空间,整体占用空间是不断增大(不考虑GC的情况,GC一般不会发生,只会在需要的时候才会主动进行gc)。
元空间存的字符串常量只是一个地址引用
,因此占用空间很小,10M内存执行了很久都没有报OOM,并且宿主机内存始终在一个稳定值,并没有因为不断添加常量进常量池而导致宿主机内存被占用内存一直增大。
程序一直执行,元空间最终肯定也会被占满,但关于堆中常量的引用已经被gc回收,那么元空间应该也会回收一部分空间(清除元空间引用关于堆中被gc的对象),然后就会维持在一个值的范围波动起伏而不会一直增然后OOM。
最终结论
真正意义上字符串常量池在堆中存储,元空间可能有引用堆中字符串常量,运行时常量池在方法区中。
根据变化情况,推出字符串常量池在堆的old区,字符串在Young的Eden区产生,调用intern()
就是先看old区有没有,如果没有将这个对象存入old区中,而字符串之所以通过==
比较会返回true
可能是jvm底层做的一件事情,移动了对象,再次查找对象时会找到移动后的那个对象(gc也会导致对象的移动,会将Eden区的对象移动到S0或S1,甚至可能移动到Old区。因此gc导致的移动至少jvm是肯定要做处理的,至于怎么处理的则需要继续深入才能探究,故推断intern()方法的作用就是将字符串移动到常量池中
)
字符串常量池再深入
运行时常量池的再深入,从jvm的内存分配角度谈谈这道字符串常量池的面试题。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/164769.html原文链接:https://javaforall.cn