无知的我正在复盘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);
}
}
机制
- 当运行到某字符串行号,就会把某符号变为某字符串对象
- 该符号作为键值,查找StringTable(又叫串池)里是否有对应的对象 //@StringTable的结构是 hashtable,不能圹容
- 如果没有,则插入,否则,不作处理。
- 特点是 懒加载
代码字节码反编译后
局部变量表
常量池在字节码中。当运行时会被加载到运行时常量池中,当没有被引用时,只是符号,当被引用后,才称为对象
字符串变量拼接机制分析下面代码在字节码中的执行过程
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下
StringTable 垃圾回收默认打开了一个开关:如果89%时间花在了垃圾回收上,但是只有2%的堆空间被回收。会爆如下信息
代码实验触发垃圾回收
此时堆内存被分配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();
}
}
通过上面的实验可以知道,字符串入池后明显内存的占用减少了
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)