JVM学习-虚拟机层面看String

String基本特性
  • String字符串,使用一对""引起来表示
    • String s1 = "lotus"; //字面量定义方式
    • String s2 = new String("hello")
  • String声明为final,不可被继承
  • String实现了Serializable接口,表示字符串支持序列化,实现Comparable接口,表示String可以比较大小
  • String在JDK8以前内部定义了final char[] value用于存储字符串数据,jdk9改为byte[]
  • String代表不可变字符序列,称不可变性
    • 当对字符串重新赋值时,重写指定内存区域赋值,不能使用原有的value进行赋值
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
    • 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
  • 字符串常量池不会存储相同内容的字符串
    • String的String Pool是一个固定大小的Hashtable,默认值大小长度1009.如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表很长,而链表长了后直接会影响当前调用String.intern的性能
    • 使用-XX:StringTableSize可以设置StringTable的长度
    • 在jdk6中StringTable是固定的,就是1009,所以常量池中的字符串过多会导致效率下降很快,StringTableSize设置数值没有要求
    • 在jdk7中StringTable的长度默认60013,StringTableSize设置数值没有要求
    • 在jdk8开始,设置StringTable的长度,1009是可设置的最小值
String内存分配
  • Java语言中有8种基本数据类型和一种比较特殊的类型String,这些类型为了使它们运行过程中速度更快,更节省内存,都提供了一种常量池的概念
  • 常量池类似于一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String的常量池比较特殊,主要使用方法有两种
    • 直接使用双引号声明出的String对象会直接存储在常量池中
    • 不使用双引号声明的String对象,可以使用String提供的intern()方法
  • Java6及以前,字符串常量池存放在永久代
  • Java7中对字符串池的逻辑做了改变,将字符串常量池位置调整到Java堆内
    • 所有的字符串都保存在堆中,和其他普通对象一样,这样可以让你在进行调优应用时仅需调整堆大小就可以了
    • 字符串常量池概念原本使用得比较多,但是这个改动使我们有足够的理由重新考虑使用String.intern()
  • Java8元空间,字符串常量池在堆中
字符串拼接操作
  • 常量与常量拼接结果在常量池,原理是编译期优化
java 复制代码
 @Test
    public void test1() {
        String s1 = "a" + "b" + "c";          //等同于"abc",//编译期优化
        String s2 = "abc";
        //最终.java编译成.class,再执行.class
        // String s1 = "abc";       String s2 = "abc"
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
    //执行结果
    true
	true
  • 常量池中不会存在相同内容的常量
  • 只要其中有一个是变量,结果就在堆中,变量拼接原理是StringBuilder
java 复制代码
  @Test
    public void test2() {
        String s1 = "javaEE";
        String s2 = "hadoop";

        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";    //编译期优化
        //拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体内容为拼接结果
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2;

        System.out.println(s3 == s4);  //true
        System.out.println(s3 == s5);  //false
        System.out.println(s3 == s6);  //false
        System.out.println(s3 == s7);  //false
        System.out.println(s5 == s6);  //false
        System.out.println(s5 == s7);  //false
        System.out.println(s6 == s7);  //false
		//判断s6的值是否在常量池中,如果查到则直接返回常量池中的地址,如字符串常量池中不存在,则在常量池中加载一份,并返回此对象地址
        String s8 = s6.intern();
        System.out.println(s3 == s8);   //true
    }
 @Test
    public void test3() {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        /**
         * s1 + s2执行细节
         * StringBuilder sb = new StringBuilder();
         * sb.append("a")
         * sb.append("b")
         * sb.toString()----------
         * @Override---StringBuilder
         *     public String toString() {
         *         // Create a copy, don't share the array
         *         return new String(value, 0, count);
         *     }
         *     
         *  在jdk5.0以后使用StringBuilder,5.0之前使用StringBuilder
         */
        String s4 = s1 + s2;
        System.out.println(s3 == s4);   //false
    }
@Test
    public void test4() {
        /**
         * 1.字符串拼接不一定使用StringBuilder
         * 如果拼接符号左右两边都是字符串常量或常量引用,仍使用编译期优化,即非StringBuilder
         * 2.针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final时使用上
         */
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);     //true
    }

  /**
     * 通过StringBuilder的append方式添加字符串效率要远高于使用String的字符串拼接方式
     * 详情:①使用StringBuilder的append方式,自始至终只创建一个StringBuilder对象
     *      使用String拼接的方式,每次循环会创建一个StringBuilder和一个String
     *      ②使用String拼接方式,内存由于创建了较多的StringBuilder对象,占用内存大,GC耗时
     * 改进空间:在实际开发中,如果能基本确定要前前后后添加的字符串长度不高于某个限定值highLevel情况下,建议使用构造器创建StringBuilder
     * StringBuilder sb = new StringBuilder(highLevel)
     */
    @Test
    public void test5() {
        long start = System.currentTimeMillis();
        //method1(100000);     //花费时间:3641
        method2(100000);   //花费时间:0
        long end = System.currentTimeMillis();
        System.out.println("花费时间:" + (end - start));
    }

    private void method1(int highLevel) {
        String src = "";
        for (int i = 0; i < highLevel; i++) {
            src += "a";        //每次循环创建一个StringBuilder,String
        }
    }
    private void method2(int highLevel) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            sb.append("a");
        }
    }
  • 如果拼接的结果调用intern()方法,则该去将常量池中还没有的字符串对象放入池中,并返回些对象地址
intern的使用
  • JDK6中,将这个字符串对象尝试放入串池
    • 如果串池中有,则并不会放入,返回已有的串池中的对象地址
    • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
  • JDK7起,将这个字符串对象尝试放入串池
    • 如果串池中有,则并不会放入,返回已有的串池中的对象地址
    • 如果没有,会把此对象的引用地址复制一份,放入串池,并返回串池中的对象地址
java 复制代码
/**
 * 如何保证变量s指向的是字符串常量池中的数据?
 * 方式①:String s = "lotus";//字面量定义的方式
 * 方式②:调用intern()方法
 *        String s = new String("lotus").intern()
 *        String s = new StringBuilder("lotus").toString().intern()
 */
public class StringIntern {
    public static void main(String[] args) {
        /**
         * new String("1"),会创建两个对象
         * 对象1:new String("1")
         * 对象2:常量池中"1"
         */
        String s = new String("1");   //s指向堆空间创建的字符串对象地址
        s.intern();
        String s2 = "1";                      //s2指向常量池中对象地址
        System.out.println(s == s2);   //jdk6:false,jdk7/8:false
        /**
         * new String("1") + new String("1")会创建两个对象
         * 对象1:new StringBuilder
         * 对象2:new String("1")
         * 对象3:常量池中"1"
         * 对象4:new String("2")
         * 对象5:常量池中"2"
         *   深入剖析:StringBuilder的toString()
         *   对象6:new String("12")
         *   强调,toString()调用,在常量也中没有生顾"12"
         */
        String s3 = new String("1") + new String("2");   //s3变量地址为new String("12")
        /**
         * 在字符串常量池中生成"12"
         * jdk6:创建一个新的对象"12",也就有新的地址
         * jdk7/8:此时常量池中并没有创建"12",而是创建一个指向堆空间的指针
         */
        s3.intern();        
        String s4 = "12";   //使用上一行代码执行时,在常量池中生成的"12"的地址
        System.out.println(s3 == s4);    jdk6:false,jdk7/8:true
    }
}
G1的String去重操作
  • 许多Java应用做的测试得出以下结果
    • 堆存活数据集合里面String对象占了25%
    • 堆存活数据集合里面重复的String对象有13.5%
    • String对象的平均长度是45
  • 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里,Java堆中存活的数据集合差不多25%是String对象 ,更进一步,这里面差不多一半String对象是重复的,重复的意思是说string1.equals(string2)=true,堆上存在重复的string对象必然是一种内存资源浪费,这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就避免浪费内存
  • 实现
    • 当垃圾收集器工作的时候,会访问堆上存活的对象,对每一个访问的对象都会检查是否是候选的要去重的String对象
    • 如果是,把这个对象的一个引用插入到队列中等待后续的处理,一个去重的线程在后台运行,处理这个队列,处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象
    • 使用一个hashtable来记录所有的被String对象使用的不重复的char数组,当去重的时候,会查看这个hashtable,不看堆上是否已经存在一个一模一样的char数组
    • 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终被垃圾收集器回收
    • 如果查看失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组
  • 命令行选项
    • UseStringDeduplication (bool):开启String去重,默认是不开启的,需要手动开启
    • PrintStringDeduplicationStatistics(bool):打印详细的去重统计信息
    • StringDeduplicationAgeThreshold(uintx):达到这个年龄的String对象被认为是去重的候选对象
相关推荐
P.H. Infinity1 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天1 小时前
java的threadlocal为何内存泄漏
java
caridle1 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^1 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋31 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花1 小时前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端1 小时前
第六章 7.0 LinkList
java·开发语言·网络
Wx-bishekaifayuan2 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
全栈开发圈2 小时前
新书速览|Java网络爬虫精解与实践
java·开发语言·爬虫