先来说一个常考的面试题。运行时常量池 vs 字符串常量池运行时常量池像是类的"档案袋",保存了类的各种常量信息;字符串常量池则是一个专门的"字符串缓存区",用来优化字符串的存储和使用。当类加载时,运行时常量池中的字符串字面量以及编译期可确定的常量表达式结果(如"a"+"b"即"ab"会存入字符串常量池)会被处理,最终在字符串常量池中创建对应的字符串对象。JDK 1.8+ 内存结构:┌─────────────────────┐│ 堆 ││ ┌───────────────┐ ││ │字符串常量池 │ │ ← 字符串对象/引用│ └───────────────┘ ││ ┌───────────────┐ ││ │普通对象实例 │ ││ └───────────────┘ │└─────────────────────┘┌─────────────────────┐│ 元空间 ││ ┌───────────────┐ ││ │运行时常量池 │ │ ← 类元信息、符号引用│ └───────────────┘ ││ ┌───────────────┐ ││ │类信息 │ ││ └───────────────┘ │└─────────────────────┘(1)位置JDK 1.6及之前:两者都在方法区(永久代)JDK 1.7:字符串常量池移到堆中,运行时常量池在永久代(方法区)JDK 1.8+:字符串常量池仍在堆中,运行时常量池在元空间(方法区)(2)存储内容不同运行时常量池包含:类的全限定名、字段名和描述符、方法名和描述符(描述符是指在字节码中完整描述类型信息)、各种字面量(包括字符串字面量)、符号引用字符串常量池包含:字符串对象或引用(JDK 1.7+)、字符串对象实例(JDK 1.6)。(3)创建时机不同运行时常量池创建时机 :类加载时创建,当类被加载到JVM时,类文件中的常量池信息被加载到运行时常量池。字符串常量池创建时机:动态创建和维护。情况1,类加载时可能创建(如果类中有字符串字面量);情况2,运行时动态创建intern()。例如:// 情况1:类加载时可能创建(如果类中有字符串字面量)Strings1="hello";// 类加载时可能将"hello"放入字符串常量池// 情况2:运行时动态创建Strings2=newString("world").intern();// 运行时动态入池Strings3="hel"+"lo";// 编译期优化,可能使用常量池已有对象注意,上述代码的"hello"字面量、"world"字面量都会在类加载时放入字符串常量池,且s1,s2,s3指向字符串常量池地址。而下面的代码:Strings4=newString("apple");"apple"字面量会入池,但new出来的对象在堆中,s4指向堆中地址。(4)作用不同运行时常量池:1. 存储类的元信息,支持动态链接;2. 存放符号引用,在解析阶段转为直接引用字符串常量池: 1. 提高字符串重用,节省内存; 2. 提升字符串操作性能。例如:publicclassPurposeDifference{publicstaticvoidmain(String[]args){// ===== 运行时常量池的作用 =====// 1. 存储类的元信息,支持动态链接Class?clazz=String.class;// 类引用从运行时常量池获取// 2. 存放符号引用,在解析阶段转为直接引用Objectobj=newObject();// Object类的符号引用在运行时常量池// ===== 字符串常量池的作用 =====// 1. 提高字符串重用,节省内存Stringstr1="java";Stringstr2="java";// 复用常量池中的对象System.out.println(str1==str2);// true// 2. 提升字符串操作性能// 避免重复创建相同内容的字符串}}总结:一、StringTable(字符串常量池)先看一道面试题:1.1 字符串拼接先来看一下其他的例子:常量池中的信息,都会被加载到运行时常量池中,这之前a,b,ab都是常量池中的符号,还没有变为java中的字符串对象。当执行到虚拟机指令的ldc命令时,会把a符号变为字符串对象"a",会准备一块StringTable空间(StringTable是一个哈希表,不能扩容),会将key到StringTable中去找,没有则存入StringTable串池,b和ab符号原理相同。接下来增加拼接字符串的代码:可以看到底层使用StringBuilder进行拼接然后new String()了一个新的字符串对象(new String()得到的对象存在堆中)。字符串变量的拼接原理是StringBuilder()。1.2 编译期优化上图中蓝色部分的ldc指令可以看到直接加载了"ab"字符串,和虚拟机指令“6:ldc #4”结果一致,当执行ldc将符号加载入运行时常量池中的StringTable时,由于StringTable中已经有该字符串了,所以不会存一个新的对象。上述结果是javac在编译期间的优化,结果已在编译期确定为ab,而变量s1+s2的拼接方式,s1和s2是变量,可能发生变化,所以编译期间不能确定,只有在运行期间才能确定。字符串常量的拼接的原理是编译器优化。1.3 字符串延迟加载通过上图中的debug过程,可以发现,在执行过程中内存中的字符串个数逐渐增加(所以在编译时并不会把所有字符串加载在运行时常量池中,而是运行时用到了再加载,即常量池中的字符串仅是符号,第一次用到时才会变为对象),且已经加载过的字符串不会重复加载。1.4 intern()1.4.1 定义intern()方法是主动将串池(字符串常量池)中还没有的字符串对象放入串池。不同版本的intern()的底层原理:说明:JDK1.6的intern()是深拷贝,复制一个字符串对象放入到串池中,串池中的字符串和堆中的字符串地址不相同。JDK1.7+的intern()是浅拷贝,将堆中new String()对象的引用地址拷贝到串池中,且后面的字符串都指向该引用地址。1.4.2 JDK1.6和JDK1.7+对比示例一Strings=newString("a")+newString("b");Strings2=s.intern();System.out.println(s2=="ab");System.out.println(s=="ab");对于上述代码,JDK1.6和JDK1.7+的结果不同。(1)JDK1.6的执行结果truefalse代码分析:Strings=newString("a")+newString("b");// 1. 创建了两个字符串常量"a"和"b"(如果常量池没有的话)// 2. 创建两个String对象(堆中)// 3. StringBuilder拼接,生成"ab"(堆中),注意此时"ab"不在常量池// s指向堆中的"ab"对象Strings2=s.intern();// intern():如果常量池没有"ab",则会在常量池新建一个"ab"字符串对象//s2 指向常量池中的新对象,s 仍然指向堆中的原对象System.out.println(s2=="ab");// trueSystem.out.println(s=="ab");// false(2)JDK1.7+的执行结果truetrue原因:s.intern() 发现常量池没有"ab",不会在常量池新建对象,而是将堆中"ab"对象的引用记录到常量池,s2 和 “ab” 字面量都指向堆中的同一个对象(即 s)。示例二Stringx="ab";Strings=newString("a")+newString("b");Strings2=s.intern();System.out.println(s2==x);System.out.println(s==x);对于上述例子,JDK1.6和JDK1.7+的结果相同。truefalse执行分析第一步:String x = “ab”;先在常量池创建"ab"字符串(如果还没有的话),x 直接指向常量池中的"ab"。第二步:String s = new String(“a”) + new String(“b”);创建堆中的"ab"对象(通过StringBuilder拼接),s 指向堆中的"ab"对象。此时常量池已经有"ab"了(第一
JVM篇2-StringTable、直接内存、垃圾回收
先来说一个常考的面试题。运行时常量池 vs 字符串常量池运行时常量池像是类的"档案袋",保存了类的各种常量信息;字符串常量池则是一个专门的"字符串缓存区",用来优化字符串的存储和使用。当类加载时,运行时常量池中的字符串字面量以及编译期可确定的常量表达式结果(如"a"+"b"即"ab"会存入字符串常量池)会被处理,最终在字符串常量池中创建对应的字符串对象。JDK 1.8+ 内存结构:┌─────────────────────┐│ 堆 ││ ┌───────────────┐ ││ │字符串常量池 │ │ ← 字符串对象/引用│ └───────────────┘ ││ ┌───────────────┐ ││ │普通对象实例 │ ││ └───────────────┘ │└─────────────────────┘┌─────────────────────┐│ 元空间 ││ ┌───────────────┐ ││ │运行时常量池 │ │ ← 类元信息、符号引用│ └───────────────┘ ││ ┌───────────────┐ ││ │类信息 │ ││ └───────────────┘ │└─────────────────────┘(1)位置JDK 1.6及之前:两者都在方法区(永久代)JDK 1.7:字符串常量池移到堆中,运行时常量池在永久代(方法区)JDK 1.8+:字符串常量池仍在堆中,运行时常量池在元空间(方法区)(2)存储内容不同运行时常量池包含:类的全限定名、字段名和描述符、方法名和描述符(描述符是指在字节码中完整描述类型信息)、各种字面量(包括字符串字面量)、符号引用字符串常量池包含:字符串对象或引用(JDK 1.7+)、字符串对象实例(JDK 1.6)。(3)创建时机不同运行时常量池创建时机 :类加载时创建,当类被加载到JVM时,类文件中的常量池信息被加载到运行时常量池。字符串常量池创建时机:动态创建和维护。情况1,类加载时可能创建(如果类中有字符串字面量);情况2,运行时动态创建intern()。例如:// 情况1:类加载时可能创建(如果类中有字符串字面量)Strings1="hello";// 类加载时可能将"hello"放入字符串常量池// 情况2:运行时动态创建Strings2=newString("world").intern();// 运行时动态入池Strings3="hel"+"lo";// 编译期优化,可能使用常量池已有对象注意,上述代码的"hello"字面量、"world"字面量都会在类加载时放入字符串常量池,且s1,s2,s3指向字符串常量池地址。而下面的代码:Strings4=newString("apple");"apple"字面量会入池,但new出来的对象在堆中,s4指向堆中地址。(4)作用不同运行时常量池:1. 存储类的元信息,支持动态链接;2. 存放符号引用,在解析阶段转为直接引用字符串常量池: 1. 提高字符串重用,节省内存; 2. 提升字符串操作性能。例如:publicclassPurposeDifference{publicstaticvoidmain(String[]args){// ===== 运行时常量池的作用 =====// 1. 存储类的元信息,支持动态链接Class?clazz=String.class;// 类引用从运行时常量池获取// 2. 存放符号引用,在解析阶段转为直接引用Objectobj=newObject();// Object类的符号引用在运行时常量池// ===== 字符串常量池的作用 =====// 1. 提高字符串重用,节省内存Stringstr1="java";Stringstr2="java";// 复用常量池中的对象System.out.println(str1==str2);// true// 2. 提升字符串操作性能// 避免重复创建相同内容的字符串}}总结:一、StringTable(字符串常量池)先看一道面试题:1.1 字符串拼接先来看一下其他的例子:常量池中的信息,都会被加载到运行时常量池中,这之前a,b,ab都是常量池中的符号,还没有变为java中的字符串对象。当执行到虚拟机指令的ldc命令时,会把a符号变为字符串对象"a",会准备一块StringTable空间(StringTable是一个哈希表,不能扩容),会将key到StringTable中去找,没有则存入StringTable串池,b和ab符号原理相同。接下来增加拼接字符串的代码:可以看到底层使用StringBuilder进行拼接然后new String()了一个新的字符串对象(new String()得到的对象存在堆中)。字符串变量的拼接原理是StringBuilder()。1.2 编译期优化上图中蓝色部分的ldc指令可以看到直接加载了"ab"字符串,和虚拟机指令“6:ldc #4”结果一致,当执行ldc将符号加载入运行时常量池中的StringTable时,由于StringTable中已经有该字符串了,所以不会存一个新的对象。上述结果是javac在编译期间的优化,结果已在编译期确定为ab,而变量s1+s2的拼接方式,s1和s2是变量,可能发生变化,所以编译期间不能确定,只有在运行期间才能确定。字符串常量的拼接的原理是编译器优化。1.3 字符串延迟加载通过上图中的debug过程,可以发现,在执行过程中内存中的字符串个数逐渐增加(所以在编译时并不会把所有字符串加载在运行时常量池中,而是运行时用到了再加载,即常量池中的字符串仅是符号,第一次用到时才会变为对象),且已经加载过的字符串不会重复加载。1.4 intern()1.4.1 定义intern()方法是主动将串池(字符串常量池)中还没有的字符串对象放入串池。不同版本的intern()的底层原理:说明:JDK1.6的intern()是深拷贝,复制一个字符串对象放入到串池中,串池中的字符串和堆中的字符串地址不相同。JDK1.7+的intern()是浅拷贝,将堆中new String()对象的引用地址拷贝到串池中,且后面的字符串都指向该引用地址。1.4.2 JDK1.6和JDK1.7+对比示例一Strings=newString("a")+newString("b");Strings2=s.intern();System.out.println(s2=="ab");System.out.println(s=="ab");对于上述代码,JDK1.6和JDK1.7+的结果不同。(1)JDK1.6的执行结果truefalse代码分析:Strings=newString("a")+newString("b");// 1. 创建了两个字符串常量"a"和"b"(如果常量池没有的话)// 2. 创建两个String对象(堆中)// 3. StringBuilder拼接,生成"ab"(堆中),注意此时"ab"不在常量池// s指向堆中的"ab"对象Strings2=s.intern();// intern():如果常量池没有"ab",则会在常量池新建一个"ab"字符串对象//s2 指向常量池中的新对象,s 仍然指向堆中的原对象System.out.println(s2=="ab");// trueSystem.out.println(s=="ab");// false(2)JDK1.7+的执行结果truetrue原因:s.intern() 发现常量池没有"ab",不会在常量池新建对象,而是将堆中"ab"对象的引用记录到常量池,s2 和 “ab” 字面量都指向堆中的同一个对象(即 s)。示例二Stringx="ab";Strings=newString("a")+newString("b");Strings2=s.intern();System.out.println(s2==x);System.out.println(s==x);对于上述例子,JDK1.6和JDK1.7+的结果相同。truefalse执行分析第一步:String x = “ab”;先在常量池创建"ab"字符串(如果还没有的话),x 直接指向常量池中的"ab"。第二步:String s = new String(“a”) + new String(“b”);创建堆中的"ab"对象(通过StringBuilder拼接),s 指向堆中的"ab"对象。此时常量池已经有"ab"了(第一