JVM深入理解 | 运行时常量池 | 含反编译分析及代码实验 | 无知的我费曼笔记(图文排版无水印)

JVM深入理解 | 运行时常量池 | 含反编译分析及代码实验 | 无知的我费曼笔记(图文排版无水印),第1张

无知的我正在复盘JVM。。。
笔记特点是

  • 重新整理了涉及资料的一些语言描述、排版而使用了自己更容易理解的描述。。
  • 提升了总结归纳性
  • 同样是回答了一些常见关键问题。。

文章目录
      • 运行时常量池
        • 常量池和串池的关系
        • 字符串变量拼接机制
        • Javac在编译器期间的优化
        • 字符串延迟加载
        • StringTable 五个特性
        • StringTable 位置
        • StringTable 垃圾回收
        • StringTable 调优
          • 增加 -XX:StringTableSize=桶个数
          • 将字符串入池

运行时常量池
  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
常量池和串池的关系

运行机制

package cn.itcast.jvm.t1.stringtable;

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab

        System.out.println(s3 == s5);

    }
}

机制

  1. 当运行到某字符串行号,就会把某符号变为某字符串对象
  2. 该符号作为键值,查找StringTable(又叫串池)里是否有对应的对象 //@StringTable的结构是 hashtable,不能圹容
  3. 如果没有,则插入,否则,不作处理。
  4. 特点是 懒加载

代码字节码反编译后

局部变量表

常量池在字节码中。当运行时会被加载到运行时常量池中,当没有被引用时,只是符号,当被引用后,才称为对象

字符串变量拼接机制

分析下面代码在字节码中的执行过程

package cn.itcast.jvm.t1.stringtable;

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
    }
}

字符串变量拼接机制。如下

@查看toString()源码可以知道

调用了new String()创建了新的值为“ab”的对象

练习测试 输出的结果是什么?

package cn.itcast.jvm.t1.stringtable;

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")

        System.out.println(s3 == s4);

    }
}

答案是false。这是因为二者是不同的对象,虽然值相同。为什么是不同的对象,这是因为通过上面的实验中的结果可以知道

Javac在编译器期间的优化

分析下面代码在字节码中的执行过程

package cn.itcast.jvm.t1.stringtable;

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
    }
}

为什么"a" + “b” 直接就是 String ab?

这是因为在编译期间就已经可以拼接好了。这是因为其不会发生改变。(“String s4 = s1 + s2”而在运行期间用StringBuilder动态拼接。这是因为为了解决s1、s2的引用随时有可能发生变化的问题)

字符串延迟加载

分析下面代码在字节码中的执行过程

package cn.itcast.jvm.t1.stringtable;

/**
 * 演示字符串字面量也是【延迟】成为对象的
 */
public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println(); // 字符串个数 2275

        System.out.print("1");
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print("1"); // 字符串个数 2285
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print(x); // 字符串个数
    }
}

在前半部分每到一行语句才创建一个String对象,而不是直接全部创建。字符串个数为2285

在后半部分直接引用串池中对应的String对象,这是因为在前半部分时,串池中已经创建过了对应的String对象。字符串个数不变

StringTable 五个特性

常量池中的字符串仅是符号,第一次用到时才变为对象

利用串池的机制,来避免重复创建字符串对象

字符串变量拼接的原理是 StringBuilder (1.8)

字符串常量拼接的原理是编译期优化

可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池

  • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
  • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回

分析下面代码的执行背后逻辑

package cn.itcast.jvm.t1.stringtable;

public class Demo1_23 {

    //  [,"a", "b","ab"]
    public static void main(String[] args) {
        String s = new String("a") + new String("b");//在串池中创建“a”、“b”符号。在堆中创建对应的String对象。赋值给s给再创建ab的String对象。如下
    // 堆  new String("a")   new String("b") new String("ab")

        String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

        System.out.println( s2 == x);
        System.out.println( s == x);
    }
}

输出结果是true、true。这是因为s2、s引用的是串池中的符号

分析下面代码的执行背后逻辑 2

package cn.itcast.jvm.t1.stringtable;

public class Demo1_23 {

    //  ["ab", "a", "b"]
    public static void main(String[] args) {

        String x = "ab";
        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b") new String("ab")
        String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

        System.out.println( s2 == x);
        System.out.println( s == x );
    }

}

输出结果是true、false。false是因为s对象符号放入串池中不成功

测试题 回答面试题

package cn.itcast.jvm.t1.stringtable;

/**
 * 演示字符串相关面试题
 */
public class Demo1_21 {

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b"; // ab
        String s4 = s1 + s2;   // new String("ab")
        String s5 = "ab";
        String s6 = s4.intern();

// 问
        System.out.println(s3 == s4); // false
        System.out.println(s3 == s5); // true
        System.out.println(s3 == s6); // true

        String x2 = new String("c") + new String("d"); // new String("cd")
        x2.intern();
        String x1 = "cd";

// 问,如果调换了【最后两行代码】的位置呢?如果是jdk1.6呢
        System.out.println(x1 == x2); true;flase
    }
}
StringTable 位置

为什么?

永久代回收效率很低,堆中回收效率高。这是因为当老年代空间不足才会触发回收

验证 通过代码实验

思路 不断使用长期存活的对象引用字符串

  • 如果爆永久代空间溢出,说明StringTable存放在永久代
  • 如果爆堆空间溢出,说明StringTable存放在堆中
package cn.itcast.jvm.t1.stringtable;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示 StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下设置 -XX:MaxPermSize=10m
 */
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

在jdk6下

在jdk8下

默认打开了一个开关:如果89%时间花在了垃圾回收上,但是只有2%的堆空间被回收。会爆如下信息

StringTable 垃圾回收

代码实验触发垃圾回收

此时堆内存被分配10M;往串池中存放100000个字符串对象

package cn.itcast.jvm.t1.stringtable;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

因为堆中本身已经存放有类名等等的字符串对象,再加上这100000个字符串对象,所以超过了堆内存,系统自动进行了GC垃圾回收,最后只存放有这么多字符串对象

StringTable 调优 增加 -XX:StringTableSize=桶个数

下面代码是 将Linux的单词表(48万个单词)全部写入串池 //已经考虑了垃圾回收的问题

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }
    }
}

200000个数

1009个数

通过上面实验,可以知道1009个数明显要慢很多。这是因为涉及到了“链表的遍历查找”

将字符串入池

案例 推特地址信息字符串去重而减少堆占用

从30G下降到数百M

将字符串不去重地存入到堆空间中

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line);
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

将字符串去重(利用 line.intern() 入池)地存入到堆空间中

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());//!!!
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

通过上面的实验可以知道,字符串入池后明显内存的占用减少了

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/921902.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存