学习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
 81- E:\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()),可能会影响字节内存的释放回收