黑马JVM课程——JMV内存结构(一)
前言
什么是 JVM
定义
Java Virtual Machine —— Java 程序的运行环境(Java二进制字节码的运行环境)
好处
- 一次编写,到处运行;
- 自动内存管理,垃圾回收功能;
- 数组下标越界检查;
- 多态。
比较
常见的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
线程号,%cpu
cpu占用情况
- 可以通过
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 | 2912 |
jmap 工具(命令行)
- 查看堆内存占用情况(只能看到某一个时刻)
jmap -heap 24332
使用如下伪代码,来查看jvm堆内存的变化
分别在创建对象前,创建对象后,执行垃圾回收后,观察日志的变化主要可以观察
Eden Space
区域的内容变化,其中:
capacity
表示总容量used
表示已经占用了的free
表示未占用的可以看到,示例1中,
used
的值,也就是被占用的空间,大概只有10M左右
当我们创建了一个10M的数组之后,被占用的空间已经到了20M
当执行了垃圾回收之后,如第三个示例,只剩下1M多点的占用
1 | public static void main(String[] args) throws InterruptedException { |
- 未创建对象时
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
47Attaching 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 | Attaching to process ID 24332, please wait... |
3.执行垃圾回收后
1 | Attaching to process ID 24332, please wait... |
jconsole 工具(图形)
- 图形界面的,多功能监测工具,可以连续监测
直接在命令行输入
jconsole
命令即可打开工具图形界面
在图形界面打开的时候会列出当前服务器上所有的java程序,可以选择链接其中的一个程序查看jvm运行情况同样入上的代码,可以从界面中看出,对内存开始占用将近30M的内存
创建对象之后,升至40多兆
执行垃圾回收之后又极速下降
同时该图形界面还可以查看线程、类、CPU的具体情况
案例
垃圾回收之后,内存占用依旧很高
1.可以先使用jmap
指令查看具体占用堆内存的大小,或者直接使用jconsole
指令查看
2.可以尝试直接使用jconsole
面板手动执行GC
3.从图中可以看出,执行GC之后,虽然内存占用有所下降,但是下降的并不明显,依旧有二百多兆的占用,此时可以常使用使用jvisualvm
工具
4.jvisualvm
工具和jconsole
类似,不过拥有更多的功能
5.如下图,jvisualvm
工具可以对当前堆内存进行dump
6.对堆内存进行dump之后,可以对结果进行分析,从图中可以看出,其中最大的对象是ArrayList
7.点击查看详情后可以发现,List中包含了许多的Status对象,一直没有被垃圾回收,导致内存占用过高,然后可以根据具体代码分析,可能是有一个Status类的集合一直没有被释放掉
8.伪代码
1 | public class Test2 { |
方法区
定义
- 线程共享
- 主要存储类信息,成员变量,方法数据,成员方法和构造方法,特殊方法
- 逻辑上方法区是堆得一个组成部分,不过并不是强制规定,各个JVM厂商可以有自己的实现
- HotSpot虚拟机在jdk8之前,方法区(永久代)在堆里面
- 从jdk8开始,更改为元空间,方法区不在属于堆得一部分,使用服务器直接内存
- 不过方法区中的字符串表(stringTable)依旧存储在堆内
组成
在jdk8之前,方法区依托永久代来实现,主要是为了能公用java堆得垃圾回收机制,不用再单独为方法区开发垃圾回收功能,不过并不是所有的虚拟机都是依托于永久代来实现的
在逻辑上属于堆的一部分,不过并不是堆,又名非堆
方法区内存溢出
jdk1.7
设置jvm参数:-XX:MaxPermSize=8m
因为此时方法区还是在永久代,需要设置永久代的大小,以到达OOM的目的
1 | public class Test extends ClassLoader { |
最终报错信息如下:java.lang.OutOfMemoryError: PermGen space
1 | 25406 |
jdk1.8
在jdk1.8开始,将方法区从堆中剥离,以元空间来实现,基于服务器直接内存
为了达到OOM的目的,需要限制元空间的大小,如果不设置参数的话,元空间不受限制,理论上来说服务器有多少内存可以用多少,不过还是会受服务器位数的限制,比如32位服务器4G的限制-XX:MaxMetaspaceSize=8m
1 | public class Test extends ClassLoader { |
代码和上面的一模一样,但是最后抛出的异常却是完全不同:java.lang.OutOfMemoryError: Metaspace
1 | 5411 |
运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是
*.class
文件中的,当该类被加载,它的长量池信息就会放入运行时常量池,并把里面的符号地址变成真实地址
例如,如下代码:
1 | public class Test { |
使用 javac Test.java
命令进行编译,编译后生产 Test.class
字节码文件
使用 javap -v Test.class
命令进行反编译,就可以查看字节码文件内容
内容如下:
具体内容解读如下:(从上到下)
文件信息
最后修改时间
文件md5值
访问修饰符,类型(类还是接口),包名,类名
版本(52指jdk1.8)
访问修饰符
常量池
构造函数(不写默认会生成无参构造)
main方法,以及main方法中代码编译出的指令
1 | Classfile /E:/project/java/demo2/src/main/java/com/example/demo/test15/Test.class |
对上面的代码进行一些简单的解读:
首先程序执行到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 | [GC (Allocation Failure) [PSYoungGen: 2048K->490K(2560K)] 2048K->1060K(9728K), 0.0007148 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
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 | DirectByteBuffer(int cap) { // package-private |