JVM内存模型
内存结构
1、程序计数器(寄存器)
作用:用于记住下一条JVM
指令的内存地址,物理上是一个寄存器,因为指令需要频繁地读取,所以JVM
设计时将CPU
(运算器 、 控制器 、 寄存器组 和 内部总线 构成)中读取速度最快的寄存器单元用作程序计数器
特点:
- 线程私有
- 不会存在内存溢出
2、虚拟机栈
2.1 定义
线程运行需要的内存空间,每一个栈由多个栈帧组成,包含参数,局部变量,返回地址等
-
垃圾回收是否涉及栈内存?
垃圾回收主要在堆空间内完成
-
栈内存分配越大越好吗?
不是,栈内存空间划分太大会降低线程的数量,因为物理空间是有限的,栈空间越大只能加快某些方法的递归调用,不会提升效率。
-
方法内的局部变量是否是线程安全?
局部变量存在于每个线程的栈帧内,而如果是静态成员变量则非线程安全。
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。
如果是局部变量引用了对象,并逃离方法的作用范围,那么需要考虑线程安全问题了。
2.2 栈溢出(StackOverflow)
1、栈帧过多(递归方法调用)
2、栈帧过大(一个栈帧里面局部变量太多,这种情况不常见)
可通过-Xss size
设置栈空间大小,一般默认大小
2.3 线程运行诊断
案例一、cpu占用过高
定位:
1、用top
定位哪个进程对cpu的占用过高
2、ps H -eo pid,tid,%cpu | grep
进程id
(用ps
命令进一步定位是哪个线程引起的cpu占用过高)
3、jstack
线程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码
案例二、程序运行很长时间没有结果
可能原因:程序死锁,可利用jstack
去排查
3、本地方法栈
Java
无法执行而去调用底层的C/C++
代码
4、堆
Heap
,通过new
关键词创建对象都会放在堆内存中
特点:
- 线程共享,需要考虑线程安全问题
- 有垃圾回收机制(GC)
4.1 堆内存溢出
当某个对象不再使用后JVM
会进行垃圾回收,但是如果长时间使用并增大内存可能导致堆内存溢出
public class OOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
String str = "kexing";
try {
while(true){
str = str + str;
list.add(str);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray0(Unsafe.java:1278)
at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray(Unsafe.java:1271)
at java.base/java.lang.StringConcatHelper.newArray(StringConcatHelper.java:458)
at java.base/java.lang.StringConcatHelper.simpleConcat(StringConcatHelper.java:423)
at java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder)
at java.base/java.lang.invoke.DelegatingMethodHandle$Holder.reinvoke_L(DelegatingMethodHandle$Holder)
at java.base/java.lang.invoke.Invokers$Holder.linkToTargetMethod(Invokers$Holder)
at site.kexing.heap.OOM.main(OOM.java:17)
4.2 查看堆内存数据
1、通过jps
查看进程
2、jhsdb jmap --heap --pid 16279
查看堆内存数据
Heap Usage:
G1 Heap:
regions = 2012
capacity = 2109734912 (2012.0MB)
used = 1561400 (1.4890670776367188MB)
free = 2108173512 (2010.5109329223633MB)
0.07400929809327628% used
G1 Young Generation:
Eden Space:
regions = 0
capacity = 4194304 (4.0MB)
used = 0 (0.0MB)
free = 4194304 (4.0MB)
0.0% used
Survivor Space:
regions = 0
capacity = 0 (0.0MB)
used = 0 (0.0MB)
free = 0 (0.0MB)
0.0% used
G1 Old Generation:
regions = 3
capacity = 6291456 (6.0MB)
used = 1561400 (1.4890670776367188MB)
free = 4730056 (4.510932922363281MB)
24.817784627278645% used
5、方法区
方法区与Java
堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
在JDK1.7
及以前,习惯把方法区称为永久代,而JDK1.8
开始,使用元空间取代了永久代。
➢本质上,方法区和永久代并不等价。仅是对hotspot
而言的。《Java 虚拟规范》对如何实现方法区,不做统一要求。例如: BEA JRockit/IBM J9
中不存在永久代的概念。
➢现在来看,当年使用永久代,不是好的idea
。导致Java
程序更容易OOM
(超过-XX :MaxPermSize
.上限)
5.1、内存溢出
JDK1.8以前永久代内存溢出:java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize=8m
JDK1.8之后元空间内存溢出:java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspaceSize=8m
5.2、运行时常量池
常量池,就是一张表,JVM
指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
运行时常量池,常量池是.class
文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.3、StringTable
JDK1.6
中StringTable
存放在永久代中,因为永久代中只能通过Full gc
进行垃圾回收,而String
在Java
中又是需要频繁使用,所以JDK8
中StringTable
放在堆空间新生代中
public class Demo {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
String c = a + b;
System.out.println(ab == c);
}
}
查看字节码(JDK8):
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, 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."<init>":()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/String;
27: astore 4
29: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_3
33: aload 4
35: if_acmpne 42
38: iconst_1
39: goto 43
42: iconst_0
43: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
46: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
line 10: 29
line 11: 46
LocalVariableTable:
Start Length Slot Name Signature
0 47 0 args [Ljava/lang/String;
3 44 1 a Ljava/lang/String;
6 41 2 b Ljava/lang/String;
9 38 3 ab Ljava/lang/String;
29 18 4 c Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 42
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
}
SourceFile: "Demo.java"
可以发现String c = a + b;
底层是使用StringBuilder
操作,new StringBuilder().append("a").append("b").toString();
,使用toString
生成一个新String
对象。
StringTable特性
- 利用串池机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是
StringBuilder(1.8)
- 字符串拼接时编译器会进行优化
- 可以使用
intern
方法,主动将串池中还没有的字符串对象放入串池
StringTable调优
StringTable
底层其实是一个散列表,数组+链表结构,为了避免大面积hash
碰撞,可以扩大桶位数量,设置虚拟机参数:-XX:+PrintStringTableStatistics -XX:StringTableSize=200000
- 如果应用里有大量字符串并且存在很多重复的字符串,可以考虑使用
intern()
方法将字符串入池,而不是都存在Eden区
中,这样字符串仅会占用较少的空间。