java8以后字符串常量池的位置,以及元空间的探秘,使用VisualVM进行实战验证

java8以后字符串常量池的位置,以及元空间的探秘,使用VisualVM进行实战验证  在网上看了很多博客,解释也比较多,关于字符串常量池的具体位置难以分辨谁真谁假。  对于jdk8以后的版本有人说字符串常量池在元空间中,也有人说字符串常量池存在堆中。  到底谁说的对?他们的说法有依据吗?  今天让我们来一起探讨一下这个问题有人说字符串常量池在java堆中,可又有人说常量池存在元空间中。分享几篇知乎文章关于jvm运行时数据区的模型:1、面试官|JVM为什么使用元空间替换了永久代?2、Java方法区与元空间为了解决这个问题,下面我们通过Idea、VisualVm

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

  在网上看了很多博客,解释也比较多,关于字符串常量池的具体位置难以分辨谁真谁假。

  对于jdk8以后的版本有人说字符串常量池在元空间中,也有人说字符串常量池存在堆中。

  到底谁说的对?他们的说法有依据吗?


  今天让我们来一起探讨一下这个问题

有人说字符串常量池在java堆中,可又有人说常量池存在元空间中。

分享几篇知乎文章 关于jvm运行时数据区的模型:
1、面试官 | JVM 为什么使用元空间替换了永久代?
2、Java方法区与元空间

在这里插入图片描述


为了解决这个问题,下面我们通过Idea、VisualVm、JDK(我用的是jdk14) 和 一段测试代码来探讨一下字符串常量池的位置

将下面代码粘贴到Idea中

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

-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线程则是从字符串常量池中去取出常量。

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());
    }
    
}

为什么打印出

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的内存分配角度谈谈这道字符串常量池的面试题。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/164769.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • JUC多线程:线程池的创建及工作原理

    JUC多线程:线程池的创建及工作原理

  • idea git 使用(idea开发工具怎么使用)

    简介以下会介绍Git在IDEA中的使用,包含大多数的开发场景,这里是用Github做远程仓库,假设小组中有两个人,队长A,和队员B场景一:队长A创建项目并提交到远程Git仓库场景二:队员B从远程Git仓库上获取项目源码场景三:队员B修改了部分源码,提交到远程仓库场景四:队长A从远程仓库获取队员B的提交场景五:队员B接受了一个新功能的任务,创建了一个分支并在分支上开发场景六:队员B把…

  • Git 分支合并分支代码

    Git 分支合并分支代码git分支合并分支

    2022年10月31日
  • maven打包加时间戳[通俗易懂]

    maven打包加时间戳[通俗易懂]maven打包加时间戳方法总结基于Maven的项目,发布时需要打包,如tar.gz。web项目打成war格式包。每次打包时希望自己加上时间戳,假如我的项目名是myproject,默认打包后名为myproject.war。而我希望的名字为myproject-1.0.0-20160217。方便以后对包进行查找与管理,如何实现这种效果呢?

  • MVC框架详解(资源整理)

    MVC框架详解(资源整理)一、什么是MVC?二、SpringMVC流程图详解三、简单例子四、常用注解总结五、MVC优点与不足

  • HTML入门教程_html代码基础

    HTML入门教程_html代码基础一、什么是HTMLHTML是英文HyperTextMark-upLanguage(超文本标记语言)的缩写,它规定了HTML的语法规则,用来表示比“文本”更丰富的意义,比如图片,表格,链接等。浏览器(IE,火狐等)软件知道HTML语言的语法,可以用来查看HTML文档。目前为止互联网上的绝大多数网页都是使用HTML语言来编写的。开始学习什么是HTML

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号