前言:
- 视频:解密JVM【黑马程序员出品】(B站)
- 本篇博客是根据黑马教程所做的笔记,为复习方便。
1.程序计数器
1.1 定义
Program Counter Register 程序计数器(寄存器)
1.2 作用
-
记住下一条JVM指令的执行地址
-
物理上,通过寄存器实现
1.3 特点
- 线程私有
- CPU会为每个线程分配时间片,当当前线程的时间片使用完以后,CPU就会去执行另一个线程中的代码
- 程序计数器是每个线程所私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
- 每个线程都有独立的线程计数器
- 没有内存溢出
2、虚拟机栈
- 栈:
- 线程运行需要的内存空间
- 一个栈由多个栈帧组成
- 先进后出原则
- 栈帧:
- 每个方法运行时需要的内存
2.1 定义
Java Virtual Machine Stacks(Java虚拟机栈)
- 每个线程运行需要的内存空间,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
演示代码
/**
* 演示栈帧
*/
public class Demo01_1 {
public static void main(String[] args) {
method1();
}
private static void method1() {
method2(1, 2);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点
问题辨析
-
1.垃圾回收是否涉及栈内存?
- (回答一)否。栈内存就是由一次次的方法调用产生的栈帧内存,而栈帧内存在每一次方法调用结束后都会被弹出栈,自动被回收掉。所以无需通过垃圾回收机制去回收内存。
- (回答二)不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
-
2.栈内存的分配越大越好吗?
- 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
-
3.方法内的局部变量是否是线程安全的?
-
如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
-
如果如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题
-
代码演示:
- m1 StringBuilder对象是方法的局部变量,每个线程私有的,其他线程不可能访问到StringBuilder对象,因此线程安全
- m2 StringBuilder对象是方法参数,不是线程安全。有可能有其他线程访问,对多个线程来说是共享的
- m3 StringBuilder对象作为返回值,逃离了方法作用范围,能够被其他线程访问,不再线程安全
package com.cloudguest.Demo; /** * 局部变量的线程安全问题 */ public class Demo01_17 { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); sb.append(4); sb.append(5); sb.append(6); // 匿名内部类 new Thread(() -> { m2(sb); }).start(); } //m1 StringBuilder对象是方法的局部变量,每个线程私有的,其他线程不可能访问到StringBuilder对象,因此线程安全 public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } //m2 StringBuilder对象是方法参数,不是线程安全。有可能有其他线程访问,对多个线程来说是共享的 //见主函数演示 public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb); } //m3 StringBuilder对象作为返回值,逃离了方法作用范围,能够被其他线程访问,不再线程安全 public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; } }
-
2.2 栈内存溢出(stackOverflowError)
Java.lang.stackOverflowError 栈内存溢出
发生原因
- 栈帧过多导致栈内存溢出
- 如方法的递归调用可能会出现
- 栈帧过大导致栈内存溢出
2.2.1 方法没有正确结束递归导致栈内存溢出
代码演示:
package com.cloudguest.Demo;
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k
*/
public class Demo01_2 {
private static int count;
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
private static void method1() {
count++;
method1();
}
}
-Xss
默认值未设置:
-Xss256k
: 栈的总大小变小,递归调用次数(count)变小
2.2.2 第三方库导致栈内存溢出
- Json数据转换
package com.cloudguest.demo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
/**
* json数据转换
*/
public class Demo01_19 {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Dylan");
Emp e1 = new Emp();
e1.setName("Li");
e1.setDept(d);
Emp e2 = new Emp();
e2.setName("Zhang");
e2.setDept(d);
d.setEmps(Arrays.asList(e1, e2));
// {name : 'Dylan', emps: [{name:'Li', dept: '', emps: ...无限递归... }]}
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
//@JsonIgnore // 添加注解JsonIgnore转换时忽略dept属性的转换,双向关联--》单项关联,只通过部门关联员工
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
- 注意:
- 1.运行错误
- 2.
stackOverflowError
原因: - 3.解决思路:
- 添加
@JsonIgnore
注解- 添加注解JsonIgnore转换时忽略dept属性的转换,双向关联–》单项关联,只通过部门关联员工
- 添加
- 1.运行错误
2.3 线程运行诊断
案例1:CPU占用过多
- 在Linux上使用
nohup
命令后台运行java代码
- 使用top命令实时监测(查看进程)
从下图可以看出,java代码在后台不断使用cpu在跑,占用cpu达到99.4%左右:
- 使用
ps H -eo pid,tid,%cpu
命令查看Linux所有线程的pid
,tid
,cpu占用
-
使用
ps H -eo pid,tid,%cpu | grep 32655
查看32655进程 -
使用
jstack 32655
查看进程中的线程的nid
-
分析源代码
-
定位方法总结:
- Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
- top命令,查看是哪个进程占用CPU过高
- ps H -eo pid, tid(线程id), %cpu | grep 刚才通过top查到的进程号 通过ps命令进一步查看是哪个线程占用CPU过高
- jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换
- Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
案例2: 程序运行很长时间没有结果
-
在Linux上使用
nohup
命令后台运行java代码 等待很久没有得到结果,可能是线程发现死锁
- 使用
jstack 32752
查看进程中的线程的nid
-
分析源代码
开始时候,Thread0(第一个线程)锁住对象a,然后休眠2s;在Thread0休眠的时间段里,其他代码继续运行。
1s之后,Thread1开始运行,锁住对象b,然后尝试去锁住对象a,由于Thread0已经将对象a锁住,所以Thread1想锁住对象a,需要等待Thread0释放对象a的锁。
2s之后,Thread0从睡眠状态醒过来,尝试去获得对象b上的锁,但是b对象已经在1s时刻,被Thread1锁住,所以Thread0也陷入等待。
死锁原因:Thread1等待Thread0释放对象a的锁,Thread0等待Thread1释放对象b的锁。
3、本地方法栈(Native Method Stacks)
作用:带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法。
如java.lang.Object
中:
wait()
方法,hashCode()
方法…
4、堆
4.1 定义
Heap堆:
- 通过new关键字,创建的对象都会被放在堆内存
特点:
- 所有线程共享,堆内存中的对象都需要考虑线程安全问题
- 有垃圾回收机制
4.2 堆内存溢出
- 代码:
package com.cloudguest.demo;
import java.util.ArrayList;
import java.util.List;
/**
* 演示堆内存溢出
* -Xmx8m
*/
public class Demo01_5 {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>(); //list集合声明开始
String a = "hello";
while (true) {
list.add(a); // hello,hellohello,hellohellohello, ...
a = a + a; // 与上一次得到的字符串拼接
i++;
}
}catch (Throwable e){
e.printStackTrace();
System.out.println(i); //输出拼接多少次造成堆内存溢出
}
}
}
- 堆内存溢出 (java.lang.OutofMemoryError :java heap space):
- 分析:
list集合从声明开始,到catch块前面,都是有效范围,因此不能被垃圾回收掉。直到将堆空间占满,造成OutofMemoryError
。
- 修改堆空间值:
Xmx8m
- 最终循环17次,造成堆空间不足
4.3 堆内存诊断
- jps 工具
- 查看当前系统中有哪些java进程
- jmap 工具
- 查看堆内存占用情况,
jmap -heap 进程id
- 查看堆内存占用情况,
- jconsole 工具
- 图形界面的,多功能的监测工具,可以连续监测
-
jvisualvm 工具
演示代码:
package com.cloudguest.demo;
/**
* 演示堆内存
*/
public class Demo01_4 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 11024 * 100]; // 10Mb
System.out.println("2...");
Thread.sleep(30000);
array = null; //byte[]数组对象可以被垃圾回收
System.gc(); //显示调用垃圾回收
System.out.println("3...");
Thread.sleep(1000000L);
}
}
堆内存诊断:
- jmap演示:
E:\programming\java\itcast\jvm>jps
2656
13160 Demo01_4
14632 Jps
4188 Launcher
E:\programming\java\itcast\jvm>jmap -heap 13160 //不同时间点(打印1,2,3)堆内存占用不同
Attaching to process ID 13160, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.271-b09
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4240441344 (4044.0MB)
NewSize = 88604672 (84.5MB)
MaxNewSize = 1413480448 (1348.0MB)
OldSize = 177733632 (169.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 66584576 (63.5MB)
used = 6658608 (6.3501434326171875MB) //堆内存占用
free = 59925968 (57.14985656738281MB)
10.000225878137304% used
From Space:
capacity = 11010048 (10.5MB)
used = 0 (0.0MB)
free = 11010048 (10.5MB)
0.0% used
To Space:
capacity = 11010048 (10.5MB)
used = 0 (0.0MB)
free = 11010048 (10.5MB)
0.0% used
PS Old Generation
capacity = 1307049984 (1246.5MB)
used = 1128857616 (1076.562515258789MB)
free = 178192368 (169.93748474121094MB)
86.36682834005528% used
3186 interned Strings occupying 261168 bytes.
jconsole
演示
案例
package com.cloudguest.demo;
import java.util.ArrayList;
import java.util.List;
/**
* 演示查看的对象个数 演示查看的对象个数 堆转储 dump
*/
public class Demo01_13 {
public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
// Student student = new Student();
}
Thread.sleep(10000000000L);
}
}
class Student {
private byte[] big = new byte[1024 * 1024]; //1Mb
}
-
垃圾回收后,内存占用仍然很高
-
jconsole分析:
- jvisualvm分析
5、方法区
5.1 定义
5.2 组成
5.3 方法区内存溢出
- 1.8以前会导致永久代内存溢出
- 1.8以后会导致元空间内存溢出
案例:演示元空间内存溢出(jdk1.8~)
package com.cloudguest.demo;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出
* -XX:MaxMetaspaceSize=8m
*/
public class Demo01_8 extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
Demo01_8 test = new Demo01_8();
for (int i = 0; i < 10000; i++, j++) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] code = cw.toByteArray();
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
- 默认情况,利用系统内存(16g),未发生元空间内存溢出
- 更改元空间虚拟机参数
-XX:MaxMetaspaceSize=8m
- 报错:
java.lang.OutOfMemoryError: Metaspace
,元空间内存溢出
- 报错:
案例:演示永久代内存溢出 (jdk1.6测试)
- 代码同上
- 报错:
java.lang.OutOfMemoryError: PermGen space
,永久代内存溢出
总结
- 1.8以前会导致永久代内存溢出
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
- 1.8以后会导致元空间内存溢出
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
场景
- spring
- mybatis
5.4 运行时常量池
- 通过
HelloWorld.java
初识常量池
通过反编译来查看类的信息
-
获得对应类的.class文件
- 在JDK对应的bin目录下运行cmd,也可以在IDEA控制台输入
- 输入
javac 对应类的绝对路径
javac HelloWorld.java
输入完成后,对应的目录下就会出现类的.class文件
-
在控制台输入
javap -v 类的绝对路径
javap -v HelloWorld.class
-
然后能在控制台看到反编译以后类的信息了
- 类的基本信息
E:\programming\java\itcast\jvm\src\main\java\com\cloudguest\jvm\t5>dir
2020/12/23 21:54 <DIR> .
2020/12/23 21:54 <DIR> ..
2020/12/23 21:54 448 HelloWorld.class
2020/12/23 21:52 230 HelloWorld.java
2 个文件 678 字节
2 个目录 282,418,880,512 可用字节
E:\programming\java\itcast\jvm\src\main\java\com\cloudguest\jvm\t5>javap -v HelloWorld.class
Classfile /E:/programming/java/itcast/jvm/src/main/java/com/cloudguest/jvm/t5/HelloWorld.class //类的文件
Last modified 2020-12-23; size 448 bytes //最后修改时间
MD5 checksum 0c6f4ef9e2ca5eddb6b38e6ca713c2f3 //签名
Compiled from "HelloWorld.java"
public class com.cloudguest.jvm.t5.HelloWorld
minor version: 0
major version: 52 //jdk内部版本 - 1.8
flags: ACC_PUBLIC, ACC_SUPER //访问修饰符
Constant pool: //常量池
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // hello world!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // com/cloudguest/jvm/t5/HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 com/cloudguest/jvm/t5/HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{ //类的方法定义
public com.cloudguest.jvm.t5.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
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
// 虚拟机中执行编译的方法(Code:内是真正编译执行的内容,#号的内容需要在常量池`Constant pool`中查找)
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! 加载`hello world`参数
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 虚方法调用
8: return //方法执行结束
LineNumberTable:
line 6: 0
line 7: 8
}
SourceFile: "HelloWorld.java"
-
常量池,就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
-
运行时常量池,常量池是*.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.5 StringTable(串池)
面试题:
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();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
常量池与串池的关系
1.用来放字符串对象且里面的元素不重复
//StringTable[]
public class Demo01_22 {
// 常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有变为java字符串对象
// 当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容) --> StringTable[“a”]
// 当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中 --> StringTable[“a”,“b”]
// 当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中 --> StringTable[“a”,“b”,“ab”]
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串
Code:
stack=1, locals=4, 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: return
当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)
当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中
当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中
最终StringTable [“a”, “b”, “ab”]
注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。
2.使用拼接字符串变量对象创建字符串的过程
public class Demo01_22 {
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")
}
}
反编译后的结果
Code:
stack=2, 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: return
}
通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中
String s3 = "ab";
String s4 = s1 + s2;
//结果为false,因为s3是存在于串池之中,s4是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
System.out.println(s3 == s4);
3.使用拼接字符串常量对象的方法创建字符串
public class Demo01_22 {
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()
//使用拼接字符串常量对象的方法创建字符串
String s5 = "a" + "b"; //javac 在编译期间的优化,结果已经在编译器确定为ab
}
}
反编译后的结果:
Code:
stack=2, locals=6, 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: ldc #4 // String ab
31: astore 5
33: return
- 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以s5直接从串池中获取值,所以进行的操作s3与s5一致。
- 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuffer来创建
4.字符串字面量也是【延迟】成为对象的
/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println(); //字符串个数 = 2163
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("0");
System.out.println("1"); //字符串个数 = 2173
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("0");
}
}
- 从字符串数量变化可知,执行
TestString
的时候,不是直接将所有字符串对象全部放入串池,而是执行一行代码,遇见一个没见过的字符串对象,才放入串池。 - 遇到串池中已有的字符串,会直接从串池中获取,不会创建新的字符串对象。
5.5 StringTable特性
- 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
- jdk1.8 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
- jdk1.6 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
- 注意:无论是串池还是堆里面的字符串,都是对象
intern方法
演示
- intern方法(jdk 1.8)
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,则放入成功
- 如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
例1
public class Demo23 {
//StringTable["a","b","ab"]
public static void main(String[] args) {
String s = new String("a") + new String("b"); //new String("ab") ,仅仅存在于堆中,动态拼接的得到的结果
//堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将s这个字符串对象尝试放入串池,如果有,则不会放入,如果没有则放入串池,会把串池中的对象返回
//true,这里的"ab"用的就是s2中放入串池的对象,因此为同一个对象
System.out.println(s2 == "ab");
//true
System.out.println(s == "ab");
}
}
例2
public class Demo23 {
//StringTable["ab","a","b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b"); //new String("ab") ,仅仅存在于堆中,动态拼接的得到的结果
//堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将s这个字符串对象尝试放入串池。串池中存在,不会放入,会把串池中的对象返回
//true,s2是把x在串池中的对象"ab"返回,因此二者一致
System.out.println(s2 == x);
//false,s的"ab"仅存在于堆中
System.out.println(s == x);
}
}
- intern方法(jdk 1.6)
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
- 如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象
面试题解答
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
- jdk1.8中
String x2 = new String("c") + new String("d"); //StringTable["c","d"],堆中new String("cd")
String x1 = "cd"; //StringTable["c","d","cd"]
x2.intern(); //将x2这个对象尝试放入串池,有,不会放入,会将串池中"cd"对象返回
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2); //false
//如果调换了【最后两行代码】的位置 -- true
//调换了【最后两行代码】的位置 + 是jdk1.6 -- false
- jdk1.8中
String x2 = new String("c") + new String("d"); //StringTable["c","d"],堆中new String("cd")
x2.intern(); //将x2这个对象尝试放入串池,没有,放入成功,会将串池中对象返回,StringTable["c","d","cd"]
String x1 = "cd"; //将"cd"这个对象尝试放入串池,有,不会放入,会将串池中"cd"对象返回,即与x2一致
// 如果是jdk1.6呢
System.out.println(x1 == x2); //true
- jdk1.6中
String x2 = new String("c") + new String("d"); //new String("cd")
x2.intern(); //将x2这个对象尝试放入串池,没有,将该字符串对象复制一份,再将副本放入到串池中,会将串池中对象返回
//常量池中的"cd"是一个副本,同堆中的"cd"是不同的对象
String x1 = "cd"; //x1引用自变量"cd",得到的是堆中常量池的副本,但x2还是堆中的对象
System.out.println(x1 == x2); //false
5.6 StringTable 位置
- 与版本有关。
- 在
jvm1.6
时候,StringTable是常量池的一部分,存在于永久代中; - 从
jvm1.7
开始,StringTable从永久代转移到了堆中,- 原因: 永久代的回收效率太低。
- 优点:大大减轻了内存的字符串对内存的占用。
- 在
5.7 StringTable 垃圾回收
StringTable在内存紧张时,会发生垃圾回收
演示StringTable垃圾回收
/**
* 演示StringTable垃圾回收
* -Xmx10m 虚拟机堆内存最大值
* -XX:+PrintStringTableStatistics 打印字符串表的统计信息,可以观察到串池中字符串的相关信息
* -XX:+PrintGCDetails -verbose:gc 打印垃圾回收的详细信息
*/
public class Demo1_7 {
public static void main(String[] args) {
int i = 0;
try {
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
打印信息:
0
Heap //打印堆内存占用情况
PSYoungGen total 2560K, used 1791K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 87% used [0x00000000ffd00000,0x00000000ffebfdd8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 3193K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 348K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13359 = 320616 bytes, avg 24.000
Number of literals : 13359 = 571832 bytes, avg 42.805
Total footprint : = 1052536 bytes
Average bucket size : 0.668
Variance of bucket size : 0.669
Std. dev. of bucket size: 0.818
Maximum bucket size : 6
StringTable statistics: //常量池统计信息,底层类似于HashTbale实现,结构为数组 + 链表
Number of buckets : 60013 = 480104 bytes, avg 8.000 //数组个数,桶(buckets)
Number of entries : 1764 = 42336 bytes, avg 24.000 //键值对个数
Number of literals : 1764 = 158120 bytes, avg 89.637 //串池中字符串对象个数
Total footprint : = 680560 bytes
Average bucket size : 0.029
Variance of bucket size : 0.030
Std. dev. of bucket size: 0.172
Maximum bucket size : 3
Process finished with exit code 0
修改代码;
int i = 0;
try {
for (int j = 0; j < 100; j++) { //j=100,j=10000
String.valueOf(j).intern(); //将产生的字符串对象加入到StringTable中
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
j=100
打印信息:
j=10000
打印信息(buckets的个数没有增加10000个,只有6149):
5.8 StringTable 性能调优
StringTable 性能调优,就是调整StringTable桶的个数
-
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=xxxx
-
考虑是否需要将字符串对象入池
可以通过intern方法减少重复入池
6.直接内存
6.1 定义
Direct Memory
- 属于操作系统
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
文件读写流程
使用了DirectBuffer(用于数据缓冲区)
直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率。
6.2 分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存
直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放。
通过
//通过ByteBuffer申请1M的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?
allocateDirect的实现
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer类
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size); //申请内存
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象
att = null;
}
这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存
public void clean() {
if (remove(this)) {
try {
this.thunk.run(); //调用run方法
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
对应对象的run方法
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address); //释放直接内存中占用的内存
address = 0;
Bits.unreserveMemory(size, capacity);
}
直接内存的回收机制总结
- 使用了Unsafe类来完成直接内存的分配回收,回收需要主动调用freeMemory方法
- ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存