jvm内存结构·上
程序计数器
虚拟机栈(线程私有栈-方法的栈帧\FILO)
- 当前线程的压入的方法的栈帧
- -Xss"SIZE": 设置线程栈大小
栈内存溢出
- SO:StackOverflowError at '错误代码处'
- 递归调用
- 有时第三方库也会导致SO:ObjectMapper / @JsonIgnore
线程安全
- 方法的局部变量/堆对象是否有可能被多个线程共享
- 非线程安全情况:
局部变量的引用来自于方法的参数或方法的返回值
线程诊断(栈诊断工具:jstack)
- linux环境:
-
nohup 进程(路径)名
-
top :用top定位哪个进程对cpu占用过高
-
ps H -eo pid,tid,%cpu | grep '十进制进程id'
:用ps命令进一步定位 哪个线程 引起的cpu占用过高 -
jstack '十进制进程id' ->转16进制(32665 -> 0x7f99)
nid = 0x7f99
观察线程状态:java.lang.thread.state:runable/timed_waiting at 错误行 -
jstack -> 检测
死循环(一直runable)、对象线程同步锁死锁(一直block)
本地方法栈(native方法(NM)的栈)
- 本地方法是用C/C++编写的java方法
为了更好的和操作系统 进行交互 - 给NM的运行提供内存空间
堆
- 线程创建的对象
- 线程共享,要考虑线程安全
- 垃圾回收
堆内存溢出
- OOM:OutOfMemoryError: java heap space / at '错误代码处'
- OOM原因:一直在创建对象,并且不进行GC
- -Xmx'size'
- Space: Eden、From、To、PS Old Generation
- Space Use: capacity、used、free(capacity=used+free)
堆内存诊断
- jps:查看本地java进程pid
- jmap -heap 'pid'
切点式检测 - jconsole
连续监测,很直观,功能更丰富
案例:gc后内存占用率仍很高
如何使用监测工具进行排查
- 问题点:PS Old Generation堆内存无法被GC回收
- 思考问题:是因为编程失误导致对象一直被引用吗?
- **监测工具:jvisualvm (可视化方式展示jvm)
- jvisualvm--监视--堆dump:保存具体时刻的堆的快照
- 堆dump--检查--保留大小最大的对象
- **总结:实际情况也是,先堆转储/堆dump,然后检查最大的对象们
方法区:MethodArea
- 定义:.text段
包括:run-time常量池、成员变量和方法数据,方法或构造器代码 - 不同的jvm厂商实现MA的方式可能不同
有的使用 heap的一部分 ,有的 使用操作系统提供的本地内存 - OOM:MA内存溢出
MA内存溢出
- 1.8前:永久代内存溢出
- 1.8后:元空间内存溢出
jvm内存结构·中
方法区:MethodArea(概念)|永久代或元空间(实现)
- 定义:.text段+.rodata段+
包括:Class、ClassLoader、常量池(constant pool)、运行时常量池 - 不同的jvm厂商实现MA的方式可能不同
有的使用 heap的一部分PermGen ,有的 使用操作系统提供的本地内存Metaspace - OOM:MA内存溢出
MA内存溢出
- 1.8前:永久代内存溢出
- 报错信息:OutOfMemory:PermGen
- VM模拟:-XX:MaxPermGenSize=8m
- 场景模拟:(asm)ClassWrite-.visit-.toArrayByte-.defineClass
- 实际场景:实际场景经常需要动态的类加载
比如spring和mybatis需要生成代理类,使用不当就会导致MA的OOME - 动态生成类、动态加载类的场景非常多,不要轻视
- 1.8后:元空间内存溢出
未加载的类字节码 / (字节码的)常量池(CP)
- 反编译字节码工具:
javap -v '类名'.class
内容 :- 类基本信息
- 类文件路径:Classfile
- 签名:MD5 checksum
- 最后修改:Last Modified
- 类版本:minor/major version(0-51)
- 类权限标志:flags: ACC_PUBLIC、ACC_SUPER、···
- 常量池:constant pool
- 类的方法定义
- 实例:sout."hello world"的包装方法的字节码
public static void main(java.lang.String[]);
descriptor: (···)
flags: (0x0009) ACC_PUBLIC、ACC_STATIC
Code: stack=2, locals=1, args_size=1
0: getstatic #2--Flied java.lang.System.out
3: ldc #3--String hello world
5: invokevirtual #4 --Method java.io.P-S-.println
8: return- 解析:
- 获得字节码中'#n'(符号地址)
找到在常量池中的具体值#2: Fieldref #21,#22
-
#21: Class #28
#28: Utf-8 java/lang/System -
#22: NameAndType #29:#30
#29: Utf-8 out
#30: Utf-8 Ljava/io/P-S-
#3: String #23
#23: Utf-8 hello world -
#21: Class #28
- 执行完整的字节码指令
- 获得字节码中'#n'(符号地址)
- 类基本信息
加载后字节码 / 运行时常量池(RTCP)
- 运行时常量池(RTCP)的定义:
类字节码被加载后,常量池CP将被加载到JVM的运行时常量池中
(其中符号位被替换成真实的对象) - String Table(串池 / ST):
(只存在于RTCP,字节码的常量池不存在串池 / ST)
StringTable本质是一个JVM的哈希表结构的内存-
串池懒/延迟创建常串对象
- 例子:
()->{
str1 = "a";
str2 = "b";
str3 = "ab";
} - 编译后查看字节码:
0: ldc #2-- // 加载RTCP中String a符号,使其成为字符串对象
2: astore_1--//将加载的"a"存储到slot1(str1)
// ldc #2 会把RTCP的a符号变成串池中具体的"a"字符串对象
// 过程:ldc会检查a符号串作为key,检查"a"是否存在于串池,
// 如果不存在则创建"a",并将"a"加入串池
3: ldc #3--//加载String b
5: astore_2--//将加载的"b"存储到slot1(str2)
ldc#4,astore_3--加载String ab,将加载的ab存储到slot1(str3)
LocalVariableTable:(局部变量表)
Start Length Slot Name Signature:
0 10 0 args [Ljava/lang/String;
3 7 1 str1 Ljava/lang/String;
6 4 2 str2 Ljava/lang/String;
9 1 3 str3 Ljava/lang/String;
- 总结:懒的:
并不是执行前就在串池ST中创建字符串对象;
当执行到代码时,才进行串池ST对目标字符串对象的判断、创建和加载
查询StringTable中是否有该串,有-返回对象,没有-添加
- 例子:
-
串变量拼接
- 例子:
()->{
str1 = "a"; // ldc "a",astore_1
str2 = "b"; // ldc "b",astore_2
str3 = "ab"; // ldc "ab",astore_3
str4 = str1 + str2; // new StringBuilder().append(str1///"a").append(str2///"b").toString();
// 这里的 StringBuilder 的 toString 方法 = new String(sb.value, 0 ,sb.length)
// new String()产生一个堆中的字符串对象"ab",而不是串池中存在的"ab" }
- 例子:
-
串常量拼接(编译期优化)
()->{
str1 = "a"; // ldc "a",astore_1
str2 = "b"; // ldc "b",astore_2
str3 = "ab"; // ldc "ab"#4,astore_3
str4 = "a" + "b"; // ldc "ab"#4,astore_4
//这里直接引用了str3引用的串池中的"ab"
//编译器会对常量对象的运算进行优化 -
串池懒创建串字面量
()->{
sout."1"; // 常量池字符串数量 101
sout."2"; // 常量池字符串数量 102
sout."3"; // 常量池字符串数量 103
sout."1"; // 常量池字符串数量 103
sout."2"; // 常量池字符串数量 103
//这里直接引用了str3引用的串池中的"ab"
//编译器会对常量对象的运算进行优化 -
ST.intern
()->{String s = new String ("a") + new String("b");}
字节码的描述:
String var10000 = new String("a");
String s = var10000 + new String("b");
System.out.println();
字节码源码:
Code:
stack=4, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String a
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: new #2 // class java/lang/String
12: dup
13: ldc #5 // String b
15: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
18: invokedynamic #6, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
-