学习java虚拟机——(1)内存结构
本文最后更新于:5 个月前
学习java虚拟机——(1)内存结构
1. 程序计数器
1.1定义
Program Counter Register 程序计数器(寄存器)
1.2作用
记住下一条jvm指令的执行地址
1.3 特点
- 是线程私有的
- 不会存在内存溢出
2. 虚拟机栈
2.1 定义
每个线程运行时所需要的内存,称为虚拟机栈
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
- 垃圾回收是否涉及栈内存?
不需要==。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。垃圾回收==只会回收堆内存,不会回收栈内存。
- 栈内存分配越大越好吗?
==不是。因为==物理内存是一定的==,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。==
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
2.2 内存溢出
- 栈帧过多导致栈内存溢出(无限递归)
- 栈帧过大导致栈内存溢出
2.3 线程运行诊断
- 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进制的,需要转换
- 运行很长时间没有结果
可能由于多个线程发生死锁,jstack 进程id
通过查看进程中的线程的nid
3. 本地方法栈
一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法
4. 堆
4.1 定义
通过new关键字创建的对象都会被放在堆内存
特点
- 所有线程共享,堆内存中的对象都需要考虑线程安全问题
- 有垃圾回收机制
4.2 堆内存溢出
**java.lang.OutofMemoryError **:java heap space. 堆内存溢出
堆内存诊断
- jps : 查看当前系统中有哪些 java 进程*(获得进程id)*
jps
- jmap (某时刻) 查看堆内存占用情况
jmap -heap 进程id
- **jconsole **图形化界面的,多功能的监测工具,可以连续监测
jconsole
- **jvisualvm **
jvisualvm
5. 方法区
5.1 定义
方法区是在所有 Java 虚拟机线程之间共享。它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法 。
==方法区域是在虚拟机启动时创建的。==尽管方法区域在==逻辑上是堆的一部分==,但简单的实现可能会选择不进行垃圾回收或压缩它。此规范不规定方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小的,也可以根据计算的要求进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。
如果方法区域中的内存无法用于满足分配请求,则 Java 虚拟机将引发 .OutOfMemoryError
5.2 结构
StringTable后来放在堆里的原因是永久栈的垃圾回收效率不高, 在堆中只需触发minGc就可进行回收
5.3 方法区内存溢出
- 1.8以前会导致永久代内存溢出
- 1.8以后会导致元空间内存溢出
1 |
|
5.4 常量池
二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)
通过反编译来查看类的信息
获得对应类的.class文件
- 在JDK对应的bin目录下运行cmd,也可以在IDEA控制台输入
- 在控制台输入 javap -v 类的绝对路径
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81E:\jvm\out\production\jvm\cn\itcast\jvm\t5>javap -v HelloWorld.class
Classfile /E:/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
//类基本信息
Last modified 2022-9-5; size 567 bytes
MD5 checksum 8efebdac91aa496515fa1c161184e354
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.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 // cn/itcast/jvm/t5/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 Lcn/itcast/jvm/t5/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 cn/itcast/jvm/t5/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 cn.itcast.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
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
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;
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.4 运行时常量池
常量池:就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
运行时常量池:常量池是.class文件中的,当该*类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.5 StringTable 的特性
常量池中的字符串仅是符号,只有在被用到时才会转化为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder
字符串常量拼接的原理是编译器优化
可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入!如果没有则放入串池,会把串池中的对象返回
注意:无论是串池还是堆里面的字符串,都是对象
5.5.1 用来放字符串对象且里面的元素不重复
1 |
|
常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串
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)时,该字符串才会被创建并放入串池中。
5.5.2 使用拼接字符串变量对象创建字符串的过程
1 |
|
反编译后的结果
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.”“:()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/Str
ing;
27: astore 4
29: return
通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中.
5.5.3 使用拼接字符串常量对象的方法创建字符串
1 |
|
反编译后的结果
==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.”“:()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/Str==
==ing;==
==27: astore 4==
==//ab3初始化时直接从串池中获取字符串==
==29: ldc #4 // String ab==
==31: astore 5==
==33: return==
- 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。
- 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建
5.5.4 intern方法 1.8
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,则放入成功
- 如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
- 成功:
1 |
|
- 失败:
1 |
|
5.5.5 intern方法 1.6
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
- 如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象
5.6 StringTable 垃圾回收
StringTable在内存紧张时,会发生垃圾回收
5.7 StringTable 调优
- 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=xxxx
- 考虑是否需要将字符串对象入池, 可以通过
intern
方法减少重复入池
6. 直接内存
- 属于操作系统,常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
文件读写流程
使用了DirectBuffer
直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率
释放原理
直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory
来手动释放
通过
1 |
|
申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?
allocateDirect的实现
1 |
|
DirectByteBuffer类
1 |
|
这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存
1 |
|
对应对象的run方法
1 |
|
直接内存的回收机制总结
- 使用了Unsafe类来完成直接内存的分配回收,回收需要主动调用
freeMemory
方法 - ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存
1 |
|
禁用显式的垃圾回收(代码中的System.gc()
),可能会影响字节内存的释放回收