文章目录
- 1、程序计数器
- 2、虚拟机栈
- 2.1 、定义
- 2.2、栈内存溢出
- 2.3 、线程运行诊断
- 3、本地方法栈
- 4、堆
- 4.1、定义
- 4.2 、堆内存溢出
- 4.3 、堆内存诊断
- 5、方法区(Method Area)
- 5.1 、定义
- 5.2、方法区组成
- 5.3 、方法区内存溢出
- 5.4 、运行时常量池
- 5.5 、StringTable
- 5.6 、StringTable位置
- 5.7 、StringTable垃圾回收
- 5.8 、StringTable性能调优
- 6、直接内存
- 6.1 、定义
- 6.2 、文件读取过程
- 6.3 、直接内存释放原理
先来一张 JVM 架构图
jvm内存结构:
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
ClassLoader
类加载器:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。Method Area
方法区:类是放在方法区中。Heap
堆:类的实例对象。- 当类调用方法时,会用到
JVM Stack
、本地方法栈
、PC Register
。 - 方法执行时的每行代码是由执行引擎中的
解释器
逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统
打交道就需要使用到本地方法接口
。
1、程序计数器
在java中使用CPU寄存器
作为程序计数器
作用:是记住下一条JVM指令的执行地址
特点:
- 是
线程私有
的,每个线程都有自己的程序计数器,用来记录程序运行到了那个位置 不会存在内存溢出
(java中唯一不用考虑内存溢出的地方)
2、虚拟机栈
2.1 、定义
Java Virtual Machine Stacks(Java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由
多个栈帧
(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着
当前正在执行
的那个方法,也就是栈顶的栈帧
垃圾回收是否涉及栈内存?
不涉及,因为栈内存会随着方法出栈释放掉
,不需要GC管理。GC只回收堆内存的无用垃圾
栈内存的分配越大越好吗?
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用
,但是可执行的线程数就会越少。
可以通过JVM指令来分配栈空间,如果不指定会默认分配 Java官方文档
方法内的局部变量是否线程安全?
- 如果方法内部局部变量没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,并逃离了方法,就不是线程安全的
每一个线程都会产生一个单独的栈帧(局部变量是线程私有的),不会出现共享资源抢占的问题,所以不会有线程安全问题
但是如果是变量是static,是多个线程共享的会产生线程安全问题
2.2、栈内存溢出
以下情况可能会导致栈内存溢出:
栈帧过多
导致栈内存溢出(例如不合理的递归调用)栈帧过大
导致栈帧溢出类的循环引用
导致内存溢出
栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError
使用 -Xss256k 指定栈内存大小
2.3 、线程运行诊断
案例1: cpu 占用过多
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程
- 用
top
命令定位哪个进程对cpu的占用过高 ps H -eo pid,tid,%cpu | grep 进程id
(用ps命令进一步定位是哪个线程引起的cpu占用过高)jstack 进程id
- 可以根据线程id (十进制转换成十六进制)找到有问题的线程,进一步定位到问题代码的源码行号
案例2:程序运行很长时间没有结果
有可能是死锁
jstack 进程id
定位问题代码行号
3、本地方法栈
本地方法栈就是存放native方法
的空间,线程私有
4、堆
4.1、定义
Heap 堆
- 通过new关键字,创建对象都会使用堆空间
特点:
- 它是
线程共享
的,堆中对象都需要考虑线程安全问题 - 有垃圾回收机制
4.2 、堆内存溢出
java.lang.OutOfMemoryError: Java heap space堆内存溢出
使用-Xmx8m来指定堆内存大小
4.3 、堆内存诊断
-
jps工具
-
jmap工具
- 查看堆内存占用情况:
jmap -heap 进程id
- 查看堆内存占用情况:
-
jconsole
工具- 图形界面的,多功能的监测工具,可以连续监测
-
垃圾回收后,内存占用仍然很高,可以使用
jvisualvm工具
堆转储(dump)
5、方法区(Method Area)
5.1 、定义
方法区官方定义:
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field
and method data
, and the code for methods
and constructors
, including the special methods (§2.9) used in class and instance initialization and interface initialization.
Java 虚拟机有一个方法区域,该区域在所有 Java 虚拟机线程之间共享。方法区域类似于常规语言或操作系统进程中的“文本”段的编译代码的存储区域。它存储每个类的结构
,如运行时常量池
、字段
和方法数据
,以及方法和构造函数的代码
,包括用于类和实例初始化和接口初始化的特殊方法
(2.9)
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
方法区域在虚拟机启动时创建
。虽然方法区域在逻辑上是堆的一部分
,但简单实现可以选择不对其进行垃圾收集或压缩。本规范并不要求方法区域的位置或用于管理已编译代码的策略。所述方法区域可以是固定的大小,或者可以根据计算的要求扩大,如果不需要更大的方法区域,则可以缩小。方法区域的内存不需要是连续的
。
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.
Java 虚拟机实现可以为程序员或用户提供对方法区域的初始大小的控制,以及在变大小方法区域的情况下对最大和最小方法区域大小的控制。
The following exceptional condition is associated with the method area:
下列异常情况与方法区域相关联:
-
If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an
OutOfMemoryError
.如果方法区域中的内存不能用于满足分配请求,则 Java 虚拟机抛出 OutOfMemoryError。
5.2、方法区组成
Hotspot JVM的结构,可以看到堆空间发生了改变
5.3 、方法区内存溢出
1.8 以前会导致永久代(PermGen)内存溢出
演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m
1.8 之后会导致元空间内存溢出
演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m
5.4 、运行时常量池
二进制字节码注册(类基本信息、常量池、类方法定义、虚拟机指令)
- 常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是
*.class 文件
中的,当该类被加载
,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.5 、StringTable
StringTable的特性:
- 常量池中的字符串仅是符号,第一次用到时才会转化为对象
- 利用
串池
的机制,来避免重复创建字符串对象 - 字符串变量拼接的原理是
StringBuilder
(1.8) - 字符串常量拼接的原理是
编译器优化
- 可以使用
intern
方法,主动将串池中换没有的字符串对象放入串池- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会
将串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把
此对象复制一份
,再放入串池(创建了两个对象),会把串池中的对象返回
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会
5.6 、StringTable位置
不同版本的JVM的StringTable存放的位置不一样,jdk1.6以后的版本将StringTable的位置由PermGen改到了Heap中,主要是因为永久代
的垃圾回收需要Full GC
(重量级GC),而Heap
只需要Monir GC
就能回收垃圾,而常量池是经常用到所以进行了优化
5.7 、StringTable垃圾回收
我们先演示一段简单的代码
public class GCStringTable {
public static void main(String[] args) {
int i = 0;
try {
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
需要添加一些虚拟机参数再执行:
#堆空间大小 打印信息StringTable的信息 打印GC信息(GC次数、时间等)
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
执行结果为:
0
Heap
PSYoungGen total 2560K, used 1892K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 92% used [0x00000000ffd00000,0x00000000ffed9180,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 3242K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 344K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13500 = 324000 bytes, avg 24.000
Number of literals : 13500 = 602424 bytes, avg 44.624
Total footprint : = 1086512 bytes
Average bucket size : 0.675
Variance of bucket size : 0.674
Std. dev. of bucket size: 0.821
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1698 = 40752 bytes, avg 24.000
Number of literals : 1698 = 173864 bytes, avg 102.393
Total footprint : = 694720 bytes
Average bucket size : 0.028
Variance of bucket size : 0.028
Std. dev. of bucket size: 0.169
Maximum bucket size : 3
我们看第20行StringTable statistics
,StringTable底层的实现是HashTable
,我们知道HashTable的底层实现是数组+链表+红黑树
数组的个数我们称为bucket
(桶),桶的个数我们称之为buckets,对应第27行,我们可以看到在本例中有60013个桶,第22行Number of entries表示键值对共有1698个,第23行literals表示字符串常量的个数,有1698个
我们现在更改一下代码:
public class GCStringTable {
public static void main(String[] args) {
int i = 0;
try {
for (int j = 0; j < 100; j++) {
String.valueOf(j).intern();//将字符串入串池
i++;
}
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
我们将100个字符串加入到字符串串池也就是StringTable中,再执行一下:
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1795 = 43080 bytes, avg 24.000
Number of literals : 1795 = 178496 bytes, avg 99.441
Total footprint : = 701680 bytes
Average bucket size : 0.030
Variance of bucket size : 0.030
Std. dev. of bucket size: 0.173
Maximum bucket size : 3
可以看到100个字符串并没有达到我们设置堆空间的阈值10M
,现在还没有触发垃圾回收,现在我们将字符串的数量增大到10000
,查看结果:
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 7514 = 180336 bytes, avg 24.000
Number of literals : 7514 = 453560 bytes, avg 60.362
Total footprint : = 1114000 bytes
Average bucket size : 0.125
Variance of bucket size : 0.131
Std. dev. of bucket size: 0.362
Maximum bucket size : 3
我们发现Number of literals
的值并没有增加10000,我们可以看到最上面有一行代码:
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->866K(9728K),
0.0017615 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
这行代码表示堆空间不足
,触发了新生代的Monir GC
,速度还是很快的
5.8 、StringTable性能调优
我们知道StringTable的底层是hashTable
,hashTable的性能和元素个数也就是桶的数量有关的。
如果hashTable中桶的个数比较多,那么它就越分散,hash碰撞的几率就越小
如果桶的数量较少,它就越集中,hash碰撞的几率就越大
那么如何调优呢?
- 调整
-XX:StringTableSize=桶个数
(最少设置为 1009 以上) - 考虑将字符串对象是否入池(用
intern
函数入池,如果池中有则返回池中的对象)
6、直接内存
6.1 、定义
Direct Memory (直接内存)不属于JVM管理,属于操作系统内存
- 常见于 NIO 操作时,用于
数据缓冲区
分配回收成本较高
,但读写性能高
- 不受 JVM 内存回收管理
6.2 、文件读取过程
我们在使用NIO进行数据读取时的流程为:
可以看到其实读一个文件会产生两个缓存区,分别是系统缓存区
和Java缓存区
,这样数据明显冗余了,所以传统NIO方式读取文件效率低下
我们来看BIO方式下读取:
我们通过ByteBuffer.allocateDirect
获取了一块直接内存
(direct memory),这块直接内存是操作系统和Java都可以共同访问的,所以读写性能是非常快的
6.3 、直接内存释放原理
- 使用了
Unsafe对象
完成直接内存的分配回收
,并且回收需要主动调用Unsafe#freeMemory
方法 - ByteBuffer的实现类内部,使用了
Cleaner(虚引用)
来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法
调用freeMemory来释放直接内存
直接内存的释放是调用了unsafe
类进行释放的
unsafe#allocateMemory(int size) //获得申请空间的地址
unsafe#setMemory(int size) // 申请空间
unsafe#freeMemory(long base) // 释放空间,需传入需要释放空间的地址
我们看对应源码
ByteBuffer#allocateDirect(int capacity)
现在我们知道了当ByteBuffer
对象被GC的时候,才会回收我们的直接内存,但是有的时候为了防止程序员频繁使用System.gc()
(这是full GC),我们会用命令关闭手动full gc
-XX:+DisableExplicitGC //关闭显示full GC
这个时候我们的ByteBuffer
对象如果没有被回收就会导致直接内存一直不会被回收
我们的解决方法是调用
unsafe#freeMemory(long base) // 释放空间,需传入需要释放空间的地址
手动释放内存