JVM内存结构分为五个区域:程序计数器 、虚拟机栈、本地方法栈、堆、方法区。
1.程序计数器
1.1定义
Program Counter Register 程序计数器(寄存器)
- 作用: 是记录下一条需要执行的jvm 指令地址行号。
- 特点:
-
- 是线程私有的
- 在JVM中唯一的一个不会存在内存溢出的区域
1.2例子
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
- 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释交给执行。
- 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。
1.3小结
Q1: 程序计数器的作用?
记录下一条需要执行的jvm 指令地址行号。
Q2: 程序计数器是线程共享的的吗?
不是, 程序计数器是线程私有的。
2.虚拟机栈
2.1定义
- 每个线程运行需要的内存空间,称为虚拟机栈。
-
- 每个栈由多个栈帧(Frame)组成,一次栈帧对应的一次方法的调用。说白了就是每次调用方法时所占用的内存。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
- 栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的。
- 存储各种基本数据类型(
boolean
、byte
、char
、short
、int
、float
、long
、double
)局部变量、对象引用(引用指针,并非对象本身)
2.2栈内存溢出
- 栈帧过大、过多、或者第三方类库操作,都有可能造成栈溢出 java.lang.StackOverflowError 。
- 使用 -Xss256k 指定栈内存大小,一般默认为512k~1024k,取决于操作系统。
package com.lcuyp.jvm.a_stack;
/**
* @Description: 虚拟机栈
* @author: yp
*/
public class StackDemo01 {
public static int count=0;
public static void main(String[] args) {
try {
fun01();
} catch (Exception e) {
e.printStackTrace();
System.out.println("count=" + count);
}
}
public static void fun01(){
System.out.println("hello...");
fun01();
}
}
2.3小结
Q1:虚拟机栈是什么?
虚拟机栈每个线程运行需要的内存空间。每个栈由多个栈帧(Frame)组成,一次栈帧对应的一次方法的调用。栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的。
Q2:垃圾回收是否涉及栈内存?
不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。线程结束, 栈就会销毁。
Q3: 栈内存分配越大越好吗?
不是。因为物理内存是一定的,栈内存(每个栈)越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
Q4:方法内的局部变量是否线程安全?
- 如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。
3.本地方法栈
一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。
通过-Xoss参数设置本地方法栈大小。
4.堆
4.1定义
- 作用: 通过new关键字创建的对象和数组都会被放在堆内存
- 特点:
-
- 它是线程共享,堆内存中的对象都需要考虑线程安全问题
- 有垃圾回收机制
4.2.堆内存结构
4.2.1JDK1.7
年轻(新生)代(Eden、From Survivor和 To Survivor),老年代, 永久代
- Young 年轻(新生)代
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次(默认15次)垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。 - Tenured 老年代
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。 - Perm 永久代
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。
4.2.2JDK1.8
年轻(新生)代(Eden、From Survivor和To Survivor),老年代, metaspace。
jdk1.8的内存模型是由2部分组成,年轻代 + 老年代。
- 年轻代:Eden + 2*Survivor
- 老年代:OldGen
在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在。
4.2.3默认分配
- 默认情况下:新生代、老年代的比例如何? 1:2
- 默认情况下: 新生代的Eden、S0、S1的比例如何?8:1:1
- 默认初始堆内存为物理机内存的1/64
- 默认最大堆内存为物理机内存的1/4
4.3堆内存溢出
java.lang.OutofMemoryError :java heap space. 堆内存溢出
可以使用 -Xmx8m 来指定最大堆内存大小。
package com.lcuyp.jvm.b_heap;
import java.util.ArrayList;
import java.util.List;
/**
* @Description: 堆
* @author: yp
*/
public class HeapDemo01 {
private static int i = 0;
public static void main(String[] args) {
try {
List<String> list = new ArrayList<String>();
String a = "hello";
while (true){
list.add(a);
a=a+a;
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
4.4堆的各种参数
参数 | 说明 |
-Xms | 初始内存 (默认为物理内存的1/64) |
-Xmx | 最大内存(默认为物理内存的1/4) |
-Xmn | 设置新生代的大小(初始值及最大值)。通常默认即可。 |
-XX:NewRatio | 配置新生代与老年代在堆结构的占比。赋的值即为老年代的占比,剩下的1给新生代 |
-XX:SurvivorRatio | 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1 |
-XX:MaxTenuringThreshold | 设置新生代垃圾的最大年龄。超过此值,仍未被回收的话,则进入老年代。默认值为15 |
-XX:+PrintGCDetails | -XX:+PrintGCDetails |
-XX:HandlePromotionFailure | 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间, |
4.5小结
Q1:堆里面存放的是什么? 它是线程共享的吗?
存放对象和数组的。是线程共享的。
Q2:堆的内存结构是怎么样的?
- JDK7
-
- 新生代 1/3
-
-
- 伊甸园 8/10
- S0 1/10
- S1 1/10
-
-
- 老年代 2/3
- 永久代(方法)
- JDK8
-
- 新生代 1/3
-
-
- 伊甸园 8/10
- S0 1/10
- S1 1/10
-
-
- 老年代 2/3
5.方法区
5.1方法区介绍
5.1.1介绍
类加载子系统负责从文件系统或是网络中加载.class文件(字节码文件),把加载后的class类信息存放于方法区,方法区内部采用 C++ 的 instanceKlass 描述 java 类。除了类信息之外,方法区还会存放运行时常量池信息。
- instanceKlass 是存储在方法区(1.8 后的元空间内,本地内存),但 _java_mirror是存储在堆中
- instanceKlass和.class (JAVA镜像类)互相保存了对方的地址
- 每个 Java 对象的头中包含一个类型指针,指向该对象所属类的 InstanceKlass 对象。通过这个类型指针,对象可以找到方法区中的 InstanceKlass 对象。InstanceKlass 对象中有一个 _java_mirror 字段,指向一个存储在 Java 堆中的 java.lang.Class 实例。这样,对象可以通过类型指针找到方法区中的 InstanceKlass,再通过 _java_mirror 字段获取 java.lang.Class 实例,从而获取类的各种信息。
5.1.2在哪里
- 1.8 之前会导致永久代内存溢出
使用 -XX:MaxPermSize=8m 指定永久代内存大小 - 1.8 之后会导致元空间内存溢出
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
5.1.3 栈、堆、方法区的关系
5.1.4小结
Q1: 方法区里面放的是什么?
类信息。类加载子系统负责从文件系统或是网络中加载.class文件(字节码文件),把加载后的class类信息存放于方法区,方法区内部采用 C++ 的 instanceKlass 描述 java 类。除了类信息之外,方法区还会存放运行时常量池信息。
Q2:方法区用的是堆空间吗?
- JDK7 方法区在永久代, 在堆空间
- JDK8 方法区在元空间, 用的本地内存
5.2方法区内部结构
5.2.1结构整体说明
方法区是概念,永久代(元空间)是实现。
- JDK1.6以及之前: 有永久代
- JDK1.7:有永久代,但已经逐步“去永久代”。字符串常量池,静态变量移除,保存在堆中。
- JDK1.8及以后:无永久代。类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池仍然在堆。
5.2.2类型信息
对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于 interface 或是 java.lang.object,都没有父类)
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
5.2.3域(Filed)信息
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient 的某个子集)
5.2.4方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或 void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract 和 native 方法除外)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
5.2.5non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
- 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
/**
* non-final的类变量
*/
public class MethodAreaTest {
public static void main(String[] args) {
Order order = new Order();
order.hello();
System.out.println(order.count);
}
}
class Order {
public static int count = 1;
public static final int number = 2;
public static void hello() {
System.out.println("hello!");
}
}
5.2.6全局常量 static final
全局常量就是使用 static final 进行修饰被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
5.2.7运行时常量池
5.2.7.1常量池
常量就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息。
- 二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)
package com.lcuyp.jvm.c_methodarea;
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
- 反编译字节码
javap -v HelloWorld.class
##############################类的基本信息#####################################
Classfile /F:/workspace/2021/SpringCloud/jvm/target/classes/com/lcuyp/jvm/c_methodarea/HelloWorld.class
Last modified 2021-8-23; size 587 bytes
MD5 checksum 84850a4eb7dc7dba6bd240d41feb22bc
Compiled from "HelloWorld.java"
public class com.lcuyp.jvm.c_methodarea.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
##############################常量池#####################################
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/lcuyp/jvm/c_methodarea/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/lcuyp/jvm/c_methodarea/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/lcuyp/jvm/c_methodarea/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
##############################类方法定义#####################################
{
public com.lcuyp.jvm.c_methodarea.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lcuyp/jvm/c_methodarea/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。
5.2.7.2运行时常量池
常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
- 方法区,内部包含了运行时常量池
- 字节码文件,内部包含了常量池
5.2.8小结
Q1: 方法区的内部结构是怎么样的?
类型信息,域信息,方法信息,静态类变量,类常量,运行时常量池等
5.3StringTable
5.3.1介绍
StringTable即串池,是运行常量池的重要组成部分。注:1.7之后,在堆中了。
从面试题说起
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
System.out.println(x1 == x2);
5.3.2StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
-
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
5.3.3案例
- 案例一:字符串变量拼接
package com.lcuyp.jvm.c_methodarea;
public class StringTable {
public static void main(String[] args) {
String s1 = "a"; //StringTable ["a"]
String s2 = "b"; //StringTable ["a","b"]
String s3 = "ab";//StringTable ["a","b","ab"]
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()==> new String("ab")
System.out.println(s3 == s4);
}
}
使用 javap -v 反编译后的结果:
Classfile /F:/workspace/2021/SpringCloud/jvm/target/classes/com/lcuyp/jvm/c_methodarea/StringTable.class
Last modified 2021-8-23; size 961 bytes
MD5 checksum 67606aa124c554aef3d5dce3dd74e8cd
Compiled from "StringTable.java"
public class com.lcuyp.jvm.c_methodarea.StringTable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#35 // java/lang/Object."<init>":()V
#2 = String #36 // a
#3 = String #37 // b
#4 = String #38 // ab
#5 = Class #39 // java/lang/StringBuilder
#6 = Methodref #5.#35 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#40 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#41 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Fieldref #42.#43 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Methodref #44.#45 // java/io/PrintStream.println:(Z)V
#11 = Class #46 // com/lcuyp/jvm/c_methodarea/StringTable
#12 = Class #47 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/lcuyp/jvm/c_methodarea/StringTable;
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 args
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 s1
#25 = Utf8 Ljava/lang/String;
#26 = Utf8 s2
#27 = Utf8 s3
#28 = Utf8 s4
#29 = Utf8 StackMapTable
#30 = Class #23 // "[Ljava/lang/String;"
#31 = Class #48 // java/lang/String
#32 = Class #49 // java/io/PrintStream
#33 = Utf8 SourceFile
#34 = Utf8 StringTable.java
#35 = NameAndType #13:#14 // "<init>":()V
#36 = Utf8 a
#37 = Utf8 b
#38 = Utf8 ab
#39 = Utf8 java/lang/StringBuilder
#40 = NameAndType #50:#51 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#41 = NameAndType #52:#53 // toString:()Ljava/lang/String;
#42 = Class #54 // java/lang/System
#43 = NameAndType #55:#56 // out:Ljava/io/PrintStream;
#44 = Class #49 // java/io/PrintStream
#45 = NameAndType #57:#58 // println:(Z)V
#46 = Utf8 com/lcuyp/jvm/c_methodarea/StringTable
#47 = Utf8 java/lang/Object
#48 = Utf8 java/lang/String
#49 = Utf8 java/io/PrintStream
#50 = Utf8 append
#51 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#52 = Utf8 toString
#53 = Utf8 ()Ljava/lang/String;
#54 = Utf8 java/lang/System
#55 = Utf8 out
#56 = Utf8 Ljava/io/PrintStream;
#57 = Utf8 println
#58 = Utf8 (Z)V
{
public com.lcuyp.jvm.c_methodarea.StringTable();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lcuyp/jvm/c_methodarea/StringTable;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_3
33: aload 4
35: if_acmpne 42
38: iconst_1
39: goto 43
42: iconst_0
43: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
46: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
line 10: 29
line 11: 46
LocalVariableTable:
Start Length Slot Name Signature
0 47 0 args [Ljava/lang/String;
3 44 1 s1 Ljava/lang/String;
6 41 2 s2 Ljava/lang/String;
9 38 3 s3 Ljava/lang/String;
29 18 4 s4 Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 42
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
}
SourceFile: "StringTable.java"
- 案例二:字符串常量拼接(编译器优化)
package com.lcuyp.jvm.c_methodarea;
public class StringTable {
public static void main(String[] args) {
String s1 = "a"; //StringTable ["a"]
String s2 = "b"; //StringTable ["a","b"]
String s3 = "ab";//StringTable ["a","b","ab"]
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()==> new String("ab")
String s5 = "a"+"b"; //StringTable ["a","b","ab"]
System.out.println(s3 == s4);
System.out.println(s3 == s5);
}
}
- 案例三:字符串延迟加载。常量池中的字符串仅是符号,第一次用到时才变为对象
package com.lcuyp.jvm.c_methodarea;
import java.util.ArrayList;
import java.util.List;
public class StringTable02 {
public static void main(String[] args) {
List<String> list = new ArrayList<String>(); //2302
list.add("1"); //2303
list.add("2");
list.add("3");
list.add("4");
list.add("5");
list.add("6");
list.add("7");
list.add("8");
list.add("9");
list.add("10"); //2312
list.add("1"); //2312
list.add("2");
list.add("3");
list.add("4");
list.add("5");
list.add("6");
list.add("7");
list.add("8");
list.add("9");
list.add("10");
}
}
此处为语雀视频卡片,点击链接查看:2023.03.12.16.10.59.avi
5.5.4intern
- 作用: 将该字符串对象尝试放入到串池中
-
- 如果串池中没有该字符串对象,则放入成功
- 如果串池有该字符串对象,则放入失败
- 无论放入是否成功,都会返回串池中的字符串对象
- 案例一
package com.lcuyp.jvm.c_methodarea;
public class StringTable03 {
//串池StringTable:["a","b","ab"]
public static void main(String[] args) {
//堆:new String("a"),new String("b"),new String("ab")
String str = new String("a") + new String("b");
//尝试把str放入串池, 成功, 返回str="ab"
String st2 = str.intern();
String str3 = "ab";
System.out.println(str == st2); //true
System.out.println(str == str3); //true
}
}
- 案例二
package com.lcuyp.jvm.c_methodarea;
public class StringTable04 {
//串池StringTable:["ab","a","b"]
public static void main(String[] args) {
String str3 = "ab";
//堆:new String("a"),new String("b"),new String("ab")
String str = new String("a") + new String("b");
//尝试把str放入串池StringTable,失败了,也就是说str还是new String("ab"),但str2是"ab"
String str2 = str.intern();
System.out.println(str == str2);//false
System.out.println(str == str3);//false
System.out.println(str2 == str3);//true
}
}
- 案例三(面试题)
package com.lcuyp.jvm.c_methodarea;
public class StringTable05 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
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");
String x1 = "cd";
x2.intern();
System.out.println(x1 == x2);//false
}
}
5.5.5StringTable垃圾回收
- StringTable里面的字符串也会被垃圾回收, 并不是说永久的在内存里面
- 例子
package com.lcuyp.jvm.d_methodarea;
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class StringTable06 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j <= 100; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
- 先用j=100测试结果
- 再使用j=10000测试, 正常的话, 字符串的数量应该是1838+9900个
5.5.6StringTable 性能调优
- StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间 -XX:StringTableSize=桶个数(最少设置为 1009 以上,默认是60013个)
- 考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池
- 例子
package com.lcuyp.jvm.d_methodarea;
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 StringTable07 {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(StringTable07.class.getClassLoader().getResourceAsStream("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);
}
}
}
- 参数一测试: -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
- 参数二测试: -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=10000
- 参数三测试: -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=100000
5.5.7小结
Q1:字符串常量相加和变量相加有什么区别?
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
Q2:intern()的作用?
- 将该字符串对象尝试放入到串池中
-
- 如果串池中没有该字符串对象,则放入成功
- 如果串池有该字符串对象,则放入失败
- 无论放入是否成功,都会返回串池中的字符串对象
- 利用串池的intern()机制,来避免重复创建字符串对象,减少内存开销
Q3:StringTable里面的字符串会回收吗?
会. jdk1.7之后, 串池在堆里面了
6.JVM内存参数设置
- Tomcat启动直接加在bin目录下catalina.sh文件里
- Spring Boot程序的JVM参数设置格式
java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
- -Xss:每个线程的栈大小
- -Xms:设置堆的初始可用大小,默认物理内存的1/64
- -Xmx:设置堆的最大可用大小,默认物理内存的1/4
- -Xmn:新生代大小
- -XX:NewRatio:默认2表示新生代占老年代的1/2,占整个堆内存的1/3。
- -XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
- -XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
- -XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。