黑马JVM课程——JMV内存结构(一)

前言

什么是 JVM

定义

Java Virtual Machine —— Java 程序的运行环境(Java二进制字节码的运行环境)

好处

  • 一次编写,到处运行;
  • 自动内存管理,垃圾回收功能;
  • 数组下标越界检查;
  • 多态。

比较

jvm-jre-jdk比较.png

常见的JVM

常见的JVM图示

JVM内存结构

程序计数器

作用

  • 记住下一条jvm指令的执行地址

    java代码在编译之后,会编译成一条条指令,然后将指令交给解释器,解释器将指令解释成机器码然后交给CPU执行
    当程序执行一条指令的时候,程序计数器就会记录下一条指令的位置
    程序计数器是通过寄存器实现的,寄存器是CPU中运行最快的单元,因为从程序计数器获取执行位置的操作是非常频繁的

特点

  • 线程私有

    Java实现多线程的原理是通过由CPU轮询各个线程来实现多线程的
    当线程切换了之后,就需要程序计数器来记录当前线程下一次需要执行那一条指令,以便于cpu轮询回来的时候能够立即执行

  • 在Java虚拟机规范中唯一不会存在内存溢出

虚拟机栈

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个

线程运行时需要的内存空间
每个栈由多个栈帧组成
栈帧-每个方法运行时需要的内存

参数,局部变量,返回地址

当线程执行一个方法的时候,就会先把代表方法的栈帧压入栈内,当方法执行完毕,弹出栈

问题

垃圾回收是否涉及栈内存?

不涉及
栈内存中就是一次次的方法调用,没当一个方法调用完毕,就立刻出栈释放了内存,不需要垃圾回收来处理

栈内存分配越大越好吗?

可以通过jvm参数来指定 -Xss size
默认栈内存为1024kb,Windows会通过虚拟机内存进行计算
因为机器物理内存是一定的,如果栈内存划分比较大,会导致能创建的线程数量变少

方法内的局部变量是否是线程安全?

如果方法内部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离了方法的最哦用范围,需要考虑线程安全问题(基本类型不会有线程安全问题)

栈内存溢出

栈帧过多导致栈内存溢出(递归)

如下图所示,使用递归调用,没有设置临界值,最终会导致栈内存溢出
java.lang.StackOverflowError
注意,并不一定就是 39689 次调用会溢出,根据机器配置不同,会略有差异

递归栈内存溢出代码示意图

  • 栈帧过大导致栈内存溢出

线程与运行诊断

CPU占用过多

  • 首先通过top命令查看到占用CPU过高的进程
  • ps H -eo pid,tid,%cpu
    • 可以通过ps查看具体某个进程下得线程
    • -eo可以指定感兴趣的内容,pid进程号,tid线程号,%cpucpu占用情况
  • ps H -eo pid,tid,%cpu | grep pid
    • 可以通过过滤top命令查到的进程号来查看
  • jstack tid
    • 通过java提供的一个命令,可以查看到当前进程下运行的所有线程
    • 然后将有问题的线程进行16进制编码,就是线程中的nid
    • 然后可以根据具体的堆栈信息找到有问题的代码行数,进行排查

程序运行很长时间没有结果

  • jstack tid
    • 可以通过该命令查看所有线程的运行情况
    • 如果有死锁问题会在堆栈信息的最后打印
    • 不过如果是逻辑上的死锁,是不会直接打印的,需要自己梳理

本地方法栈

  • 使用 native 标记的方法为本地方法
  • 本地方法为C/C++编写
  • 由于有些功能java代码并不能很好的实现,于是就在jvm中引入了大量的C/C++代码

定义

  • 使用new关键字创建的对象,都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全问题
  • 有垃圾回收机制

堆内存溢出

如下图所示,不停的想list中添加字符串,最终会导致堆内存溢出
java.lang.OutOfMemoryError: Java heap space
添加字符串的个数,也就是i的值,跟对内存的大小有关
-Xms8m 可以通过jvm参数调整堆内存的大小

堆内存溢出示意图

堆内存诊断

jps 工具(命令行)

  • 查看当前系统中有哪些java进程
1
2
3
4
5
2912 
19988 Jps
21172
10044 Launcher
24332 Test1

jmap 工具(命令行)

  • 查看堆内存占用情况(只能看到某一个时刻)

jmap -heap 24332
使用如下伪代码,来查看jvm堆内存的变化
分别在创建对象前,创建对象后,执行垃圾回收后,观察日志的变化

主要可以观察Eden Space区域的内容变化,其中:

capacity表示总容量
used表示已经占用了的
free表示未占用的

可以看到,示例1中,used的值,也就是被占用的空间,大概只有10M左右
当我们创建了一个10M的数组之后,被占用的空间已经到了20M
当执行了垃圾回收之后,如第三个示例,只剩下1M多点的占用

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws InterruptedException {  
System.out.println("1.....");
Thread.sleep(30000);
byte[] bytes = new byte[1024*1024*10];//10mb
System.out.println("2.....");
Thread.sleep(30000);
bytes = null;
System.gc();
System.out.println("3....");
Thread.sleep(30000);
}
  1. 未创建对象时
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    Attaching to process ID 24332, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.152-b16

    using thread-local object allocation.
    Parallel GC with 10 thread(s)

    Heap Configuration:
    MinHeapFreeRatio = 0
    MaxHeapFreeRatio = 100
    MaxHeapSize = 4229955584 (4034.0MB)
    NewSize = 88604672 (84.5MB)
    MaxNewSize = 1409810432 (1344.5MB)
    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 = 10711168 (10.2149658203125MB)
    free = 55873408 (53.2850341796875MB)
    16.08656034694882% 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 = 177733632 (169.5MB)
    used = 0 (0.0MB)
    free = 177733632 (169.5MB)
    0.0% used

    3178 interned Strings occupying 260512 bytes.

2.创建对象后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Attaching to process ID 24332, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.152-b16

using thread-local object allocation.
Parallel GC with 10 thread(s)

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4229955584 (4034.0MB)
NewSize = 88604672 (84.5MB)
MaxNewSize = 1409810432 (1344.5MB)
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 = 21196944 (20.214981079101562MB)
free = 45387632 (43.28501892089844MB)
31.834615872600885% 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 = 177733632 (169.5MB)
used = 0 (0.0MB)
free = 177733632 (169.5MB)
0.0% used

3179 interned Strings occupying 260568 bytes.

3.执行垃圾回收后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Attaching to process ID 24332, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.152-b16

using thread-local object allocation.
Parallel GC with 10 thread(s)

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4229955584 (4034.0MB)
NewSize = 88604672 (84.5MB)
MaxNewSize = 1409810432 (1344.5MB)
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 = 1331712 (1.27001953125MB)
free = 65252864 (62.22998046875MB)
2.0000307578740157% 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 = 177733632 (169.5MB)
used = 1919568 (1.8306427001953125MB)
free = 175814064 (167.6693572998047MB)
1.0800251918556416% used

3165 interned Strings occupying 259584 bytes.

jconsole 工具(图形)

  • 图形界面的,多功能监测工具,可以连续监测

直接在命令行输入jconsole命令即可打开工具图形界面
在图形界面打开的时候会列出当前服务器上所有的java程序,可以选择链接其中的一个程序查看jvm运行情况

同样入上的代码,可以从界面中看出,对内存开始占用将近30M的内存
创建对象之后,升至40多兆
执行垃圾回收之后又极速下降
同时该图形界面还可以查看线程、类、CPU的具体情况

jsconsole图形界面示意图

案例

垃圾回收之后,内存占用依旧很高

1.可以先使用jmap指令查看具体占用堆内存的大小,或者直接使用jconsole指令查看
2.可以尝试直接使用jconsole面板手动执行GC
3.从图中可以看出,执行GC之后,虽然内存占用有所下降,但是下降的并不明显,依旧有二百多兆的占用,此时可以常使用使用jvisualvm工具
4.jvisualvm工具和jconsole类似,不过拥有更多的功能
jconsole问题排查示意图

5.如下图,jvisualvm工具可以对当前堆内存进行dump
垃圾回收案例分析图示1
6.对堆内存进行dump之后,可以对结果进行分析,从图中可以看出,其中最大的对象是ArrayList
垃圾回收案例分析图示2
7.点击查看详情后可以发现,List中包含了许多的Status对象,一直没有被垃圾回收,导致内存占用过高,然后可以根据具体代码分析,可能是有一个Status类的集合一直没有被释放掉
垃圾回收案例分析图示3

8.伪代码

1
2
3
4
5
6
7
8
9
10
11
12
public class Test2 {  
public static void main(String[] args) throws InterruptedException {
List<Status> list = new ArrayList<>();
for (int i = 0; i < 200; i++) {
list.add(new Status());
}
Thread.sleep(100000000);
}
}
class Status{
byte[] big = new byte[1024*1024];
}

方法区

定义

  • 线程共享
  • 主要存储类信息,成员变量,方法数据,成员方法和构造方法,特殊方法
  • 逻辑上方法区是堆得一个组成部分,不过并不是强制规定,各个JVM厂商可以有自己的实现
  • HotSpot虚拟机在jdk8之前,方法区(永久代)在堆里面
  • 从jdk8开始,更改为元空间,方法区不在属于堆得一部分,使用服务器直接内存
  • 不过方法区中的字符串表(stringTable)依旧存储在堆内

组成

方法区内存分布示意图

在jdk8之前,方法区依托永久代来实现,主要是为了能公用java堆得垃圾回收机制,不用再单独为方法区开发垃圾回收功能,不过并不是所有的虚拟机都是依托于永久代来实现的
在逻辑上属于堆的一部分,不过并不是堆,又名非堆

方法区内存溢出

jdk1.7
设置jvm参数:-XX:MaxPermSize=8m
因为此时方法区还是在永久代,需要设置永久代的大小,以到达OOM的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test extends ClassLoader {  
public static void main(String[] args) {
int j = 0;
try {
Test test = new Test();
for (int i = 0; i < 100000; i++, j++) {
//ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//参数:版本号,访问修饰符(public),类名,包名,父类,父接口
cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//生成字节数组
byte[] bytes = cw.toByteArray();
//执行了类的加载
test.defineClass("Class" + i, bytes, 0, bytes.length);
}
} finally {
System.out.println(j);
}
}
}

最终报错信息如下:
java.lang.OutOfMemoryError: PermGen space

1
2
3
4
5
6
7
25406
Exception in thread "Reference Handler" Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:140)
java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass(ClassLoader.java:800)
at java.lang.ClassLoader.defineClass(ClassLoader.java:643)
at com.ys.Test.main(Test.java:19)

jdk1.8
在jdk1.8开始,将方法区从堆中剥离,以元空间来实现,基于服务器直接内存
为了达到OOM的目的,需要限制元空间的大小,如果不设置参数的话,元空间不受限制,理论上来说服务器有多少内存可以用多少,不过还是会受服务器位数的限制,比如32位服务器4G的限制
-XX:MaxMetaspaceSize=8m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test extends ClassLoader {  
public static void main(String[] args) throws IOException {
int j = 0;
try {
Test test = new Test();
for (int i = 0; i < 100000; i++, j++) {
//ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//参数:版本号,访问修饰符(public),类名,包名,父类,父接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//生成字节数组
byte[] bytes = cw.toByteArray();
//执行了类的加载
test.defineClass("Class" + i, bytes, 0, bytes.length);
}
} finally {
System.out.println(j);
}
System.in.read();
}
}

代码和上面的一模一样,但是最后抛出的异常却是完全不同:
java.lang.OutOfMemoryError: Metaspace

1
2
3
4
5
6
5411
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.example.demo.test5.Test.main(Test.java:25)

运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的长量池信息就会放入运行时常量池,并把里面的符号地址变成真实地址

例如,如下代码:

1
2
3
4
5
public class Test {  
public static void main(String[] args) {
System.out.println("hello world");
}
}

使用 javac Test.java 命令进行编译,编译后生产 Test.class 字节码文件
使用 javap -v Test.class 命令进行反编译,就可以查看字节码文件内容
内容如下:

具体内容解读如下:(从上到下)
文件信息
最后修改时间
文件md5值
访问修饰符,类型(类还是接口),包名,类名
版本(52指jdk1.8)
访问修饰符
常量池
构造函数(不写默认会生成无参构造)
main方法,以及main方法中代码编译出的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Classfile /E:/project/java/demo2/src/main/java/com/example/demo/test15/Test.class
Last modified 2022-8-11; size 437 bytes
MD5 checksum 23e36db3fde35b83da8bb567f036f0d8
Compiled from "Test.java"
public class com.example.demo.test15.Test
minor version: 0
major version: 52
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/example/demo/test15/Test
#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 Test.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/example/demo/test15/Test
#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.example.demo.test15.Test();
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 7: 0

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 9: 0
line 10: 8
}
SourceFile: "Test.java"

对上面的代码进行一些简单的解读:
首先程序执行到main方法之后,还会执行 getstatic 指令,获取一个静态变量,具体从哪里获取,从后面的 #2 位置,该位置对应常量池表中的位置

#2 = Fieldref 表示引用的一个成员变量,具体引用的哪个成员变量,继续看后面的 #16.#17
#16 = Class 是一个类,表示要找哪个类下面的成员变量,对应的是 #23
#23 = Utf8 暂且理解是一个字符串,后面跟着的就是具体的类名 java/lang/System
#17 又引用了 #24:#25
#24 = Utf8 表示类里面具体哪个成员变量, out 成员变量名称
#25 = Utf8 这个成员变量的类型, Ljava/io/PrintStream;
ldc 将常量池中的常量值入栈,后面跟着是具体的常量位置 #3
#3 = String 表示是一个String 类型,#18
#18 = Utf8 常量,具体常量的内容 hello world
invokevirtual 指令调用用实例方法,#4
#4 = Methodref 调用方法,#19.#20
#19 = Class 调用的方法属于那个类,#26 指向具体类路径的名称
#20 = NameAndType#27:#28 指向具体的方法名称,以及方法的参数类型

StringTable

我们定义一个变量:String a = "hello";,编译之后,会被定义到class文件的常量池中;
当项目开始运行,类被加载到内存中,常量池中的内容也会被加载到运行时常量池中;
但此时“hello”字符串在运行时常量池中,只是一个符号;
java实行了懒加载机制,当代码运行到这一行的时候,会进行加载;
“hello”会从一个字符串符号,变成一个字符串对象,然后会去StringTable中查找是否有相同的字符串;
如果有,直接返回对应的地址,如果没有就会把“hello”放到StringTable中,然后再把地址赋值给变量
String a = new String("hello");这种创建对象的方式,使用的对象并不是在StringTable中,而是会在堆中

StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变成对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBulider(1.8)
  • 字符串常量拼接的原理是编译期优化(会直接将对应的常量拼接成新的常量)
  • 可以使用intern方法,主动将串池中没有的字符串对象放入串池
    • 如果串池中已经包含相同的对象,会返回串池中的对象,如果串池中没有,会先放入串池,然后返回串池中的对象
    • 在1.6的jdk中,因为StringTable是在方法区中的,所以往串池中放对象的时候是复制,所以返回的对象和原对象不同,从1.7开始,StringTable已经迁移到了堆中,所以再主动往串池中存放对象的时候,只是将对象做一下标记,并不会再复制对象,所以地址会和原对象相同

StringTable位置

在jdk1.6中,StringTable是在方法区中,但是方法区垃圾回收很少,StringTable使用又非常频繁
所以在jdk1.7中把StringTable迁移到了堆中
在1.8中将整个方法区的实现更改成了元空间

StringTable垃圾回收

-Xmx10m 设置虚拟机堆内存最大值
-XX:+PrintStringTableStatistics 打印字符串表的统计信息
-XX:+PrintGCDetails -verbose:gc

添加虚拟机参数后最终打印信息如下,
前三行表示垃圾回收的信息,垃圾回收的区域,回收前占用,回收后占用,回收耗费时间
Heap下面的内容则是堆得一些信息
SymbolTable statistics:运行时常量池所占用的空间的大小
StringTable statistics: 字符串池占用空间大小,底层用hashTable实现,默认桶位60013

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[GC (Allocation Failure) [PSYoungGen: 2048K->490K(2560K)] 2048K->1060K(9728K), 0.0007148 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2457K->503K(2560K)] 3027K->1588K(9728K), 0.0004959 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2551K->504K(2560K)] 3636K->1785K(9728K), 0.0006075 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 2560K, used 637K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 6% used [0x00000000ffd00000,0x00000000ffd21710,0x00000000fff00000)
from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e010,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 1281K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 17% used [0x00000000ff600000,0x00000000ff7406d0,0x00000000ffd00000)
Metaspace used 3234K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 351K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13223 = 317352 bytes, avg 24.000
Number of literals : 13223 = 567200 bytes, avg 42.895
Total footprint : = 1044640 bytes
Average bucket size : 0.661
Variance of bucket size : 0.664
Std. dev. of bucket size: 0.815
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1740 = 41760 bytes, avg 24.000
Number of literals : 1740 = 156352 bytes, avg 89.857
Total footprint : = 678216 bytes
Average bucket size : 0.029
Variance of bucket size : 0.029
Std. dev. of bucket size: 0.171
Maximum bucket size : 2

StringTable性能调优

  • 调整 -XX:StringTableSize=20000:调整StringTable桶位的个数,如果项目中用到的常量比较多,可以适当调大
  • 考虑将字符串对象是否如池
    • 如果需要在内存中加载大量的字符串,而这些字符串又有重复的可能性,可以考虑将字符串入池,字符串会自动做去重处理

直接内存

定义

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

假设我们用java实现一个大文件的复制操作,java本身是不具备操作io的能力的,需要调用操作系统的相关功能
所以每次去读取文件,CPU就需要先由用户态切换到内核态,然后把磁盘中的文件加载到系统缓冲区中
然后再把CPU切换成用户态,并把数据复制到JVM内存中
将文件写入到本地的时候同样,从JVM内存复制到系统缓冲区,然后写入磁盘
如果jvm操作直接内存,则可以省略从系统缓存向jvm内存复制的过程,提高文件处理的性能

DirectByteBuffer

DirectByteBuffer 是 NIO中的一个缓冲类,它所创建的内存区域就是直接内存
Java中想要操作直接内存需要通过Unsafe类来进行操控,Unsafe不会对普通程序员放开,需要通过反射获取
unsafe.setMemory() 方法分配直接内存,long类型返回值是分配的内存地址
unsafe.freeMemory(address) 回收直接内存,通过分配内存时分配的地址,进行回收内存

下面代码是 DirectByteBuffer 构造方法的源代码
base = unsafe.allocateMemory(size); 可以看到是通过Unsafe来进行分配内存的
至于回收内存,则是在 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 这一行
Deallocator 是一个任务对象,实现了Runnable接口,会在run方法中对直接内存进行回收
分配完直接内存后,会依托Cleaner创建一个任务,而任务的执行时间,则是需要看Cleaner对象
Cleaner是一个虚引用对象,当和他关联的对象被回收了之后,就会调用他的clean方法,在clean方法中,会执行与之 绑定的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}