JVM内存结构
定义:
-
java程序的运行环境(java二进制字节码的运行环境)
-
一次编写,到处运行
-
自动内存管理,垃圾回收机制
-
数组下标越界检查
-
多态(虚方法表调用)
比较
- jvm java二进制运行环境
- jre jvm+基础类库(所有类)
- jdk jvm+ jre +编译工具
- 开发javase jdk+IDE工具
- 开发javaee程序 jdk + 应用服务器+IDE工具
常见JVM
-
HotSpot
-
OpenJ9
-
。。。。。
![img](https://nyimapicture.oss-cn-beijing.aliyuncs.com/img/20200608150440.png
组成结构
- 类加载器( .java 文件 通过javac(jdk)编译成.class文件,然后交给jvm的类加载器,加载到jvm的内存中)
- 内存结构
- 类放在方法中,实例对象放在堆
- 执行引擎
- 每行代码由解释器逐行执行
- 热点代码由即时编译器(JIT)
内存结构
1. 程序计数器
1.1定义、
-
JVM内存模型是自己圈出来一块内存区域供JVM使用,然后JVM再对这块内存进行抽象的划分(叫内存模型)
-
(JVM中的虚拟概念)(物理硬件上的寄存器)是JVM对寄存器的抽象表达
-
作用,是记住下一条jvm指令的执行地址 (0,3,4,5…)
-
特点
- 是线程私有的
- 不会存在内存溢出
- 流程: java源代码经过编译(javac) -----> 二进制字节码-- JVM指令--------> 解释器------->机器语言(机器码)------>CPU
2. 虚拟机栈(Xss)
2.1 定义
-
线程运行时的内存空间(每个线程拥有自己的栈空间)
- 线程中的每个方法运行时需要的内存(栈帧)例如: 参数,局部变量,返回地址
-
每个线程运行时所需要的内存,称为虚拟机栈
-
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
-
每个栈只能有一个活动栈帧,对应着正在执行的那个方法
2.2 栈帧
概述(Stack Frame)
栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈的基本元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。最顶部的栈帧称为当前栈帧,栈帧所关联的方法称为当前方法,定义这个方法的类称为当前类,该线程中,虚拟机有且也只会对当前栈帧进行操作。
栈帧的作用有存储数据,部分过程结果,处理动态链接,方法返回值和异常分派。
每一个栈帧包含的内容有局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译代码时,栈帧需要多大的局部变量表,多深的操作数栈都可以完全确定的,并写入到方法表的code属性中。
栈帧结构图:
在介绍栈帧的各个部分时,我们先来理解一下虚拟机是如何执行一个方法的,这样我们才能理解为什么栈帧需要这些部分,这些部分分别提供了什么功能。首先我们的方法被编译成了字节码,并生成了可执行的命令。通过程序计数器,虚拟机会一行一行的执行命令,直到进入一个新的方法入口,对应虚拟机栈也就是新的栈帧入栈,当前栈帧改变,又或者遇到返回指令或出现异常结束了方法,对应虚拟机也就是出栈。
————————————————
版权声明:本文为CSDN博主「spongeboblz」的原创文章。
原文链接:https://blog.csdn.net/u014296316/article/details/82668670
2.3 问题
-
- 垃圾回收是否会涉及栈内存
- 不会,因为栈帧不过是一次次的方法调用产生的内存,而在方法调用结束后,会自动弹出栈,被自动回收掉,所以不需要垃圾回收,GC是回收堆中的内存
- 栈内存分配越大越好吗?
- -Xss 设置栈内存大小除wind 1024 kb windows 根据虚拟内存
- 栈内存越大,可同时运行的线程就越少,因为物理内存的大小是固定的。
- 太小的话如果递归的方法较多,会导致栈内存不够用,就会导致内存溢出
- 方法内的局部变量是否线程安全?
- 如果方法内部的局部变量没有逃离方法的作用范围被访问,他就是线程安全的
- 如果局部变量引用了对象,并逃离了方法的作用范围,则考虑线程安全问题
- 线程私有的安全
- 垃圾回收是否会涉及栈内存
package com.sunyang.jvmdemo;
/**
* @Author: sunyang
* @Date: 2021/7/16
* @Description:
*/
public class ThreadSafeDemo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(() -> {
m2(sb);
}).start();
}
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
2.4 栈内存溢出
- 栈帧过多(递归调用没有正确的结束条件)
- 栈帧过大导致栈帧溢出(很难)
2.5 线程运行诊断
CPU占用过高
- Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
- top命令,查看是哪个进程占用CPU过高
- ps H -eo pid, tid(线程id), %cpu | grep 刚才通过top查到的进程号 通过ps命令进一步查看是哪个线程占用CPU过高
- jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换
3. 本地方法栈
3.1 定义
指的是 不是由java代码编写的方法通常是c语言编写的本地方法,这些方法所使用的jvm内存称之为本地方法栈(例如:Object类中的方法:clone(),hashcode(),wait());
4. 堆(Xmx)
4.1 定义
- 通过new 关键字,创建的对象都会使用堆内存(也可能是栈上分配,标量替换)
4.2 特点
- 他是线程共享的,堆中的对象都需要考虑线程安全问题。
- 有垃圾回收机制
4.3堆内存溢出
4.3.1 代码示范
package com.sunyang.jvmdemo;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: sunyang
* @Date: 2021/7/16
* @Description:
*/
public class HeapDemo {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>(); // 生命周期是整个main方法,方法不结束,接不会被回收,字符串也一直在被使用,就会越来越大
String a = "hello";
while (true) {
list.add(a); // hello, hellohello,..........
a = a + a; // hellohello
i++;
}
} catch (Exception e) {
e.printStackTrace();
System.out.println(i);
}
}
}
4.3.2 工具
-
jps 查看当前系统中有哪些java进程
-
jmap 查看对内存占用情况, jmap -heap 进程id
-
jconsole 图形界面 多功能检测
-
jvisualvm
5. 方法区(-XX:MaxMetaspaceSize)
5.1 定义
-
1.8以前(-XX:MaxPermSize)
-
所有线程共享
-
存储着和类结构相关的信息(运行时常量池 ,成员变量,方法代码,和构造器方法代码,和一些特殊方法(类构造器))类加载器
-
方法区在虚拟机启动时被创建
-
逻辑上是一个堆的组成部分(概念上定义了一个方法区,生产商具体实现是不是堆的一部分得看jvm生产商,不强制)
-
JDK8以前叫永久代(堆内存的一部分)
-
1.8以后叫元空间(本地内存 操作系统内存 不在堆中) JVM内存模型是自己圈出来一块内存区域供JVM使用,然后JVM再对这块内存进行抽象的划分(叫内存模型)
-
元空间和永久代只是方法区的一种实现 方法区只是一种概念,各个生产商的落地实现方法不同
-
编译成二进制字节码 (类的基本信息,常量池,类方法的定义,包含了虚拟机指令)
5.2 方法区内存溢出
- 场景:spring,mybatis,都使用了cglib利用asm动态生成字节码(因为1.8用的本地内存所以不容易出现方法区内存溢出)
6. 常量池(Constant pool)
6.1 定义
- 就是一张表格,这张表格中存储的都是字符串信息,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量(hello word)等信息。
- 存放编译期生成的各种字面量和符号引用
6.2 代码
Classfile /C:/ideaworkspace/jvmstudy/out/production/jvmstudy/StringTableDemo.class
Last modified 2021-7-16; size 548 bytes
MD5 checksum c493b3c6d53f5716326ff91993ec66e9
Compiled from "StringTableDemo.java"
public class StringTableDemo
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 // StringTableDemo
#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 LStringTableDemo;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 StringTableDemo.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 StringTableDemo
#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 StringTableDemo(); # 无参的构造方法
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LStringTableDemo;
public static void main(java.lang.String[]); # main方法
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; 去常量池中找#2
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "StringTableDemo.java"
7. 运行时常量池(包含StringTable)
7.1 定义
-
运行时常量池,常量池是.class文件中躺在硬盘上的,当该类被加载时(加载到内存),他的常量池信息就会被放入运行时常量池,并把你们的符号地址变为真实的地址。也就是#1 #2 #3 #4 之类的符号地址变成真正内存中存储该值的真实地址。
-
变量名不会占用程序内存
多出来的变量名存储在编译过程编译器的内存中,该结构就是符号表。
“变量名实际上是一个符号地址,在对程序编译连接时由系统给每一个变量名分配一个内存地址。在程序中从变量中取值,实际上是通过变量名找到相应的内存地址,从其存储单元中读取数据。”
变量名就是编译期存在于编译器中的一个符号地址,编译或链接的时候,编译器或者链接器会给每个变量确定内存地址或偏移,并且通过符号表的方式将变量名和地址的映射关系保存起来,当代码中进行变量访问时,实际上是编译器通过变量名找到对应的内存地址,将变量名操作替换为内存地址操作,运行时程序访问该内存内存,从内存中读取数据。
因此变量名不会占用内存,编译后就不存在了,只是在编译时编译器需要占用内存地址来保存变量名与地址的映射关系。
原文链接:https://blog.csdn.net/qazw9600/article/details/105938290
-
8. StringTable(串池)
-
不属于JVM内存模型
-
内存结构是一个哈希表(长度固定 且不可扩容的哈希表)
-
常量池中的信息,
-
(1.8之后在堆中)
当这个类运行时,也就是类被加载到内存时,常量池中的信息,都会被加载到运行时常量池中,这时a, b ,ab 都是常量池中的符号,还没有变为java字符串对象,等到执行到引用他的那行代码的时候
- 执行到 0: ldc #2 // String a
- 然后去 Constant pool 中找到 #2 = String #25 // a
- 然后找到 #25 = Utf8 a
- 把a作为key去StringTable中找,看有没有相同的key,如果没有则会创建一块空间 StringTable[ ](哈希表)就会把"a" 字符串对象放入串池(StringTable)
- 如果有则直接将字符串常量池中的地址返回给栈
- 当执行到这里的时候 会把 符号 a 变为 "a " 字符串对象
public class StringTableDemo {
public static void main(String[] args) {
String s1 = "a"; // 懒加载
String s2 = "b";
String s3 = "ab";
}
}
Constant pool:
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // StringTableDemo
#6 = Class #29 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LStringTableDemo;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 s1
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 s2
#21 = Utf8 s3
#22 = Utf8 SourceFile
#23 = Utf8 StringTableDemo.java
#24 = NameAndType #7:#8 // "<init>":()V
#25 = Utf8 a
#26 = Utf8 b
#27 = Utf8 ab
#28 = Utf8 StringTableDemo
#29 = Utf8 java/lang/Object
{
public StringTableDemo();
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LStringTableDemo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
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
LineNumberTable:
line 26: 0
line 27: 3
line 28: 6
line 31: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
}
/**
* @program: jvmstudy
* @description: Demo
* @author: SunYang
* @create: 2021-07-16 21:10
**/
// 编译成二进制字节码 (类的基本信息,常量池,类方法的定义,包含了虚拟机指令)
public class StringTableDemo {
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")
// StringBuilder.toString()源码
// @Override
// public String toString() {
// Create a copy, don't share the array
// return new String(value, 0, count);
}
}
}
这一步并不会去串池中找有没有”ab“这个字符串对象,虽然串池中存储的也是"ab"字符串常量的引用值,但是这一步相当于在堆中又重新new了一个"ab"字符串对象,然后将这个地址再赋值给s4; 这里的s1和s2 是变量,随时有可能更改,结果是不能确定的,所以必须在运行期间用new StringBuilder的方式动态的去拼接。
如果是 String s5 = “a” + “b” 则s5 == s3 因为他将两个字符串拼接在一起之后是"ab"字符串,就会去全局串池中去找,如果找到了,则返回"ab"字符串对象存储在堆中的对象的地址(串池中存储的也是地址) 因为这个"a" 和 “b” 都是常量 不会发生改变,所以javac在编译期间进行了优化,结果在编译期就已经确定为ab
29: ldc #4 // String ab
String s4 = s1 + s2; // 这一步的具体执行过程如下
9: new #5 // class java/lang/StringBuilder new 了一个StringBuilder对象
12: dup // 表达式的值为一个指向这个新建对象的引用
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V // 初始化这个对象 (无参的构造方法)
16: aload_1 // 去 LocalVariableTable:Slot :1 找到 s1
17: invokevirtual #7 // Method java/lang/StringBuilder.append: // 调用append将s1追加 (Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2 // 去 LocalVariableTable:Slot :2 找到 s2
21: invokevirtual #7 // Method java/lang/StringBuilder.append: // 调用append将s1追加 (Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; // 调用toString
27: astore 4 // 放到局部变量表的4号位置 29 1 4 s4 Ljava/lang/String;
29: return
LineNumberTable:
line 26: 0
line 27: 3
line 28: 6
line 29: 9
line 32: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
Constant pool:
#1 = Methodref #10.#29 // java/lang/Object."<init>":()V
#2 = String #30 // a
#3 = String #31 // b
#4 = String #32 // ab
#5 = Class #33 // java/lang/StringBuilder
#6 = Methodref #5.#29 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #36 // StringTableDemo
#10 = Class #37 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 LStringTableDemo;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 s1
#23 = Utf8 Ljava/lang/String;
#24 = Utf8 s2
#25 = Utf8 s3
#26 = Utf8 s4
#27 = Utf8 SourceFile
#28 = Utf8 StringTableDemo.java
#29 = NameAndType #11:#12 // "<init>":()V
#30 = Utf8 a
#31 = Utf8 b
#32 = Utf8 ab
#33 = Utf8 java/lang/StringBuilder
#34 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #40:#41 // toString:()Ljava/lang/String;
#36 = Utf8 StringTableDemo
#37 = Utf8 java/lang/Object
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 toString
#41 = Utf8 ()Ljava/lang/String;
{
public StringTableDemo();
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LStringTableDemo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
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
LineNumberTable:
line 26: 0
line 27: 3
line 28: 6
line 29: 9
line 32: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
}
8.1 字符串字面量懒加载
- 字符串字字面量也是延迟成为对象的
执行到哪一行代码创建哪一个字符串 。
8.2 intern()方法
/**
* @program: jvmstudy
* @description: Demo
* @author: SunYang
* @create: 2021-07-17 10:41
**/
public class InternDemo {
// StringTable["a", "b"]
public static void main(String[] args) {
String s = new String("a") + new String("b");
// new String("a") new String ("b") 在堆中,他们和StringTable中的”a“,"b"只是值相同,但是地址不同,
// 注意 这里说的StringTable["a", "b"] 并不是new String("a") 和new String("b")的时候放进去的,而是 String s = "a"这样放进去的和这个没关系
// 而 s= new String("ab") 只存在堆中, 不存在串池中,因为他是动态拼接而成 如果之前没有String ss = "ab" 或者 String ss = "a" + "b" 串池中就不会有”ab“
// StringTable只存的是常量的字符串,
String s2 = s.intern(); // 将这个字符串对象s尝试放入串池中,如果有则不会放入,如果没有则放入串池中,并且把串池中的对象引用地址返回
System.out.println(s2 == "ab");
}
}
-
因为s.intern() 已经将”ab“放入串池中,所以s2拿的是存在StringTable中的值;而这里的"ab"也会去StringTable中去找,如果找到了,则不创建新的,如果找不到则会在堆中创建新的“ab”字符串对象,并将引用地址返回,放到StringTable中存储,这里如果没有之前的 s.intern()这一步,那么"ab" 就会创建新的,而s2 和s 指向的是另一个地址,因为并没有放在StringTable中,只是值相同,但是引用地址不同。
-
/** * @program: jvmstudy * @description: Demo * @author: SunYang * @create: 2021-07-17 11:24 **/ public class InternDemo2 { public static void main(String[] args) { String x = "ab"; String s = new String("a") + new String("b"); String s2 = s.intern(); // 因为串池中已经有了"ab", 所以s.intern放入失败,所以返回的是 原有串池中存放的"ab"字符串对象的引用地址,也就是x = "ab" 的地址,与s无关 System.out.println(s2 == x); // true System.out.println(s == x); // false } }
- 1.6 会将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份(不是复制引用地址,是值然后在堆中再创建一份新的,并将返回地址放在串池中)`
8.3 位置
- 1.6在常量池中,常量池在永久代中 永久代只有在fullGC的时候才会触发
- 1.7以后1.8在堆中
8.4 StringTable的垃圾回收
- 当内存空间不足时,未被引用的字符串常量也会被垃圾回收
- -Xmx:10m -XX:+PrintStringTableStatistics -XX: +PrintGCDetails -verbase:gc
- 堆空间大小;打印字符串表的信息,字符串实例的个数,和占用的大小信息;打印垃圾回收的详细信息(次数和时间)
- 调优,将String TableSize调大,减少hash碰撞,但也不能太大,视情况而定
8.5 总结
- 常量池中的字符串仅仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理时StringBuilder(1.8)并new String()
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入
- 为什么一定要用StringTable?
- 有很多重复的字符串,都存入内存的话,会很大,例如用户信息中的地址,如果很多用户,会有很多重复的字符串,不入池的话,会很浪费内存空间
9. 三种池的区别
- 1.全局常量池在每个VM中只有一份,存放的是字符串常量的引用值。
- 2.class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
- 3.运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用(真实的内存地址),与全局常量池中的引用值保持一致。
https://blog.csdn.net/qq_26222859/article/details/73135660
10. 直接内存(不属于JVM内存模型)
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @program: jvmstudy
* @description: Demo
* @author: SunYang
* @create: 2021-07-17 16:07
**/
public class DirectMemoryDemo {
static final String FROM = "C:\\Users\\Sun\\Documents\\JVM\\JVM.mp4";
static final String TO = "C:\\Direct\\VM.mp4";
static final int _1Mb = 1024*1024;
public static void main(String[] args) {
io(); // io 用时: 1850.3941
directBuffer(); // direct 用时: 909.0105
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();)
{
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("direct 用时: " + (end -start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);)
{
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时: " + (end -start) / 1000_000.0);
}
}
10.1 定义
-
操作系统内存
-
常见于NIO操作,用于数据缓冲期(byteBuffer用的直接内存)
-
分配回收成本高,但读写性能高
-
不受JVM内存回收管理
-
直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
-
直接内存是 Java 堆外的、直接向系统申请的内存区间。
-
直接内存来源于 NIO,通过存在堆中的 DirectByteBuffer 操作Native 内存。
-
通常,访问直接内存的速度会优于Java堆,即读写性能高。
-
因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存
-
Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区
-
必须主动调用unsafe.freeMemory(base);进行释放直接内存 但是在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)); att = null; }
10.2 代码示例
- NIO和IO
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @program: jvmstudy
* @description: Demo
* @author: SunYang
* @create: 2021-07-17 16:07
**/
public class DirectMemoryDemo {
static final String FROM = "C:\\Users\\Sun\\Documents\\JVM\\JVM.mp4";
static final String TO = "C:\\Direct\\VM.mp4";
static final int _1Mb = 1024*1024;
public static void main(String[] args) {
io(); // io 用时: 1850.3941
directBuffer(); // direct 用时: 909.0105
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();)
{
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("direct 用时: " + (end -start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);)
{
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时: " + (end -start) / 1000_000.0);
}
}
10.3 图片解析
10.3.1 IO
- java程序并不具备直接操作磁盘读写的能力
- java程序要想读取硬盘上的文件先要从用户态切换成内核态调用系统调用去读取磁盘上的文件,
- 然后系统调用将磁盘上的文件读取到系统内存中的系统缓冲区中
- java代码并不能直接从系统缓冲区直接读取数据,(Unsafe 类可以直接操作系统内存,也就是后面的直接内存)
- 然后再copy到java堆内存中的byte数组缓冲区中 new byte[]
- 不能一下直接全部读取到内存中,如果文件过大内存会承受不住,所以会定义一个缓冲区,每次读取缓冲区大小的字节流放到缓冲区中
- 然后再由内核态切换成用户态
- 然后java程序再从java堆内存的缓冲区中读取数据
- 内存包括了 系统内存(这个系统内存不是直接内存,直接内存是下面那个图片中的公共区域) 和 java堆内存, 这个java堆内存就是jvm虚拟机从虚拟内存中划分出来的JVM所用的内存(然后将这一部分内存抽象成jvm内存模型)
- 但是他们两个都属于物理内存的一部分,只是java程序不能直接从系统内存缓冲区中读取数据,必须要拷贝到jvm虚拟机内存的堆内存的缓冲区中,才能读到
- 竖左边的是操作系统(OS),右边是Java虚拟机(JVM),应用程序无论是读操作还是写操作都必须在OS和JVM之间进行复制。(也就是copy)
10.3.2 NIO
-
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
-
直接与非直接缓冲区
字节缓冲区是direct或non-direct 。 给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接在其上执行本机 I/O 操作。 也就是说,它将尝试避免在每次调用底层操作系统的本机 I/O 操作之前(或之后)将缓冲区的内容复制到(或从)中间缓冲区。
可以通过调用此类的allocateDirect工厂方法来创建直接字节缓冲区。 此方法返回的缓冲区通常比非直接缓冲区具有更高的分配和解除分配成本。 直接缓冲区的内容可能驻留在正常的垃圾收集堆之外,因此它们对应用程序内存占用的影响可能并不明显。 因此,建议将直接缓冲区主要分配给受底层系统本地 I/O 操作影响的大型、长期存在的缓冲区。 一般而言,最好仅在程序性能产生可测量的增益时才分配直接缓冲区。
也可以通过mapping文件区域直接mapping到内存来创建直接字节缓冲区。 Java 平台的实现可以选择支持通过 JNI 从本机代码创建直接字节缓冲区。 如果这些类型的缓冲区之一的实例引用了不可访问的内存区域,则尝试访问该区域将不会更改缓冲区的内容,并且会导致在访问时或稍后抛出未指定的异常时间。(摘自ByteBuffer源码翻译) -
ByteBuffer.allocate(_1Mb); // 非直接缓冲区 IO 第一张图 DirectByteBuffer 底层 base = unsafe.allocateMemory(size);
-
ByteBuffer.allocateDirect(_1Mb); // 直接缓冲区 NIO第二张图
-
在NIO中,直接开辟物理内存映射文件,应用程序直接操作物理内存映射文件,这样就少了中间的copy过程,可以极大得提高读写效率。但这种方式也存在一个问题,消耗的内存会增大,内存的释放不受jvm内存回收管理,并不是一不使用就回收。(也就是所说的零拷贝)最关键的应该是建立的那个通道。
10.4 底层分配及释放原理
- 启动前
- 启动后创建直接内存,但是未释放
- 调用System.gc()释放直接内存底层调用的实际是 unsafe.freeMemory(base); 并不是jvm的垃圾回收机制
10.4.1 Unsafe
long base = unsafe.allocateMemory(_1Gb); // 还未开辟内存空间
unsafe.setMemory(base, _1Gb, (byte) 0); // 开辟了一个G的直接内存空间
unsafe.freeMemory(base); // 释放分配的内存
10.5 直接内存的回收
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* @program: jvmstudy
* @description: Demo
* @author: SunYang
* @create: 2021-07-17 17:32
**/
public class DirectBufferDemo {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配内存完毕。。。");
System.in.read();
System.out.println("开始释放内存。。。");
byteBuffer = null;
System.gc(); // 显示的垃圾回收 Full GC // -XX: +DisableExplicitGC 禁用现实的垃圾回收 使此语句无效
System.in.read();
}
}
ByteBuffer.allocateDirect(_1Gb); // 用ByteBuffer创建直接内存
// ByteBuffer的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); // 底层调用 unsafe.allocateMemory(size); 创建直接内存 并返回地址
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0); // 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)); //
att = null;
}
- 正常直接内存的释放必须调用 unsafe.freeMemory(base); 来释放分配给直接内存的内存
- 这里并没有调用unsafe.freeMemory(base); 但是 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 底层调用了unsafe.freeMemory(base);
10.5.1 Cleaner
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
new Deallocator(base, size, cap) // 回调任务对象
// Deallocator回调任务对象源码
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address); // 在这里调用了 unsafe.freeMemory(address); 释放了直接内存
address = 0;
Bits.unreserveMemory(size, capacity);
}
-
那这个又是怎么被调用的?
-
Cleaner是一个虚引用类型
-
当他所关联的对象(this) 也就是创建的DirectByteBuffer 对象(这个对象是我们java程序自己new 创建出来的对象)被垃圾回收掉之后,他就会执行任务
-
当我们将这个对象 byteBuffer = null; 时 ,代表byteBuffer 这个对象为空,也就是可以被垃圾回收掉了,我们手动调用了一下System.gc();也就是垃圾回收机制,jvm就将byteBuffer 这个垃圾回收掉了,如果不显示的调用gc,就得等到触发jvm的垃圾回收机制间接的直接内存也会被释放
-
那 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 所关联的这个对象this(byteBuffer )被垃圾回收掉以后
-
Cleaner就会触发虚引用的一个clean()方法,
-
// 他不是在主线程中执行的,他是在一个ReferneceHandler线程(守护线程)中执行的,他是用来监测这些虚引用的,一旦这些虚引用关联的这些对象被回收了 // 他就会调用虚引用中的clean方法;然后去执行任务对象 public void clean() { if (remove(this)) { try { this.thunk.run(); // 这里就会调用那个回调任务对象Deallocator中的run方法 间接调用 unsafe.freeMemory(address); } 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; } }); } } }
-
通常为了调优会关掉显示调用gc 所以建议手动调用unsafe.freeMemory(address); 释放直接内存;
Buffer 这个对象为空,也就是可以被垃圾回收掉了,我们手动调用了一下System.gc();也就是垃圾回收机制,jvm就将byteBuffer 这个垃圾回收掉了,如果不显示的调用gc,就得等到触发jvm的垃圾回收机制间接的直接内存也会被释放 -
那 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 所关联的这个对象this(byteBuffer )被垃圾回收掉以后
-
Cleaner就会触发虚引用的一个clean()方法,
-
// 他不是在主线程中执行的,他是在一个ReferneceHandler线程(守护线程)中执行的,他是用来监测这些虚引用的,一旦这些虚引用关联的这些对象被回收了 // 他就会调用虚引用中的clean方法;然后去执行任务对象 public void clean() { if (remove(this)) { try { this.thunk.run(); // 这里就会调用那个回调任务对象Deallocator中的run方法 间接调用 unsafe.freeMemory(address); } 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; } }); } } }
-
通常为了调优会关掉显示调用gc 所以建议手动调用unsafe.freeMemory(address); 释放直接内存;