1 JVM 概述图

2 内存结构
2.1 程序计数器
2.1.1 概述
程序计数器(寄存器):是记录下一条jvm指令的执行地址行号,硬件通过寄存器实现。线程私有的; 不存在内存溢出,也是JVM规范中唯一没有OutOfMemoryError的区域。
2.1.2 示例
二进制字节码:JVM指令 Java源码
# 程序计数器记录下一条JVM指令的地址行号

2.2 虚拟机栈
2.2.1 栈定义
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
2.2.2 栈问题
-
垃圾回收是否涉及栈内存?
答案:栈内存不涉及垃圾回收
-
栈内存分配越大越好吗?
答案:栈内存不是越大越好,如果设置过大,会影响可用线程的数量;比如-Xss1m、-Xss2m,在总内存不变的情况下,可用线程数量会减少
-
方法内的局部变量是否线程安全?
答案:方法内的局部变量是线程安全(不会暴露给外部),因为方法内的局部变量各自在自已独立的内存中;如果是static int 就是线程共享的,就不是线程安全;主要看变量是否是线程共享、还是线程私有
核心1:如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
核心2:如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
2.2.3 栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
2.2.4 线程运行诊断
- CPU占用过高的诊断
public class TestDemo {
public static void main(String[] args) {
new Thread(()->{
System.out.println("1...");
while(true) {} //死循环,消耗CPU
},"thread1").start();
}
}
分析:用top定位哪个进程对cpu的占用过高;然后用ps命令进一步定位哪个线程引起的CPU占用过高;最后用jstack定位问题代码的源码行号
- 程序运行很长时间没有结果的诊断
public class TestDemo {
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (a){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("thread1...");
}
}
},"thread1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
synchronized (b){
synchronized (a){
System.out.println("thread2...");
}
}
},"thread2").start();
}
}
分析:通过jstack查看是否有死锁,得到如下结果证明,确实发生了死锁
Found one Java-level deadlock:
=============================
"thread2":
waiting to lock monitor 0x00007fa9e0812a98 (object 0x000000076ab76e90, a java.lang.Object),
which is held by "thread1"
"thread1":
waiting to lock monitor 0x00007fa9e0812ca8 (object 0x000000076ab76ea0, a java.lang.Object),
which is held by "thread2"
2.3 本地方法栈
本地方法栈中保存的是调用本地方法给的内存空间(不是由Java代码编写的,是由C/C++编写的方法),这样的方法很多,native方法,例如在Object类中的 clone() , hashCode() 等。
2.4 堆
2.4.1 概述
通过 new 关键字创建对象都会使用堆内存。特点是堆中的对象是线程共享的,需要考虑线程安全的问题,并且有垃圾回收机制。
2.4.2 堆内存溢出
生产环境建议:如果内存比较大,内存溢出不会那么快的暴露;这时,我们可以将堆内存调小,让内存溢出尽早暴露
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m
*/
public class TestDemo {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String str = "hello";
while (true){
list.add(str);
str += str;
i++;
}
} catch (Throwable e){
System.out.println(i);
e.printStackTrace();
}
}
}
2.4.3 堆内存诊断工具
- jps工具:查看当前系统中有哪些java进程
- jmap工具:查看堆内存占用情况 jmap -heap pid
- jstack 工具:线程监控
- jconsole工具:图形界面的,多功能的检测工具,可以连续监测
- jvisualvm工具:图形界面的,多功能的检测工具,可以连续监测;还有dump
- jconsole工具演示对堆内存的监控情况
//堆内存演示程序
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}

- Jvistualvm工具演示查看当前占用内存多的对象具体情况
//演示查看对象个数 堆转储 dump
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
}
Thread.sleep(1000000000L);
}
}
class Student{
private byte[] big = new byte[1024*1024];
}

2.5 元空间/方法区
2.5.1 概述
方法区是线程共享的,在JVM启动时创建,在逻辑上属于堆的一部分,方法区也可能会内存溢出

2.5.2 方法区内存溢出
1.8 以前会导致永久代内存溢出
- 永久代内存溢出 java.lang.OutOfMemoryError: PerGen space
- -XX:MaxPerSize=8m
1.8 之后会导致元空间内存溢出(系统内存)
- 元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
- -XX:MaxMetaspaceSize=8m
//元空间内存溢出
//-XX:MaxMetaspaceSize=10m -XX:-UseCompressedClassPointers
public class TestDemo extends ClassLoader {
public static void main(String[] args) throws InterruptedException {
int j = 0;
try {
TestDemo testDemo = new TestDemo();
for (int i = 0; i < 100000; i++,j++) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
byte[] code = cw.toByteArray();
testDemo.defineClass("Class"+i,code,0,code.length);
}
} finally {
System.out.println(j);
}
}
}
注:场景(动态加载类),如果框架使用不合理也会导致方法区内存溢出
- spring
- mybatis
2.6 常量池/运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
public class TestDemo {
public static void main(String[] args) {
System.out.println("Hello world");
}
}
# 反编译class文件
$ javap -v TestDemo.class
Classfile /Users/yorick/Documents/study/code/test-tmp/target/classes/com/yqj/TestDemo.class
Last modified 2021-9-6; size 543 bytes
MD5 checksum 383cf26c42b79cb732cb00a79b0dfead
Compiled from "TestDemo.java"
public class com.yqj.TestDemo
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 // com/yqj/TestDemo
#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 Lcom/yqj/TestDemo;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 TestDemo.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 com/yqj/TestDemo
#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 com.yqj.TestDemo();
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 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/yqj/TestDemo;
public static void main(java.lang.String[]);
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 13: 0
line 14: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "TestDemo.java"
2.6 StringTable串池
2.6.1 常量池与串池的关系
常量池中的信息,都会被加载到运行时常量池中。但此时只是常量池中的符号,还没有变成java对象,当调用JVM命令ldc后,变为Java字符串对象,并加入到串池 StringTable 中
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class T03_StringTable {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s4); //false
System.out.println(s3 == s5); //true
}
}
2.6.2 StringTable字符串延迟加载

由此可以看到,当运行到某行时,某行的符号才会转换为Java字符串对象,并加入到串池中。当串池中存在该字符串对象时,不会再创建对象,而是沿用StringTable串池中的对象。
2.6.3 StringTable的特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (JDK1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回
//1.8
public class TestDemo {
public static void main(String[] args) {
String x = "ab";
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
System.out.println(x == s1); //false
System.out.println(x == s2); //true
}
}
2.6.4 StringTable位置
- JDK1.6版本,字符串常量池是在永久代中;
- JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
- JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。
//证明1.8版本时,字符串常量池是在堆中的
// -Xmx8m
public class TestDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
try {
for (i = 0; i < 200000; i++) {
list.add(String.valueOf(i).intern());
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
# 报错信息,可以发现是堆内存溢出,说明StringTable串池是开辟在堆中的
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at com.yqj.TestDemo.main(TestDemo.java:14)
106710
2.6.5 StringTable的垃圾回收
jdk1.8后,串池放在堆中,如果堆空间不足,串池也会进行垃圾回收。
//-Xmx8m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
public class TestDemo {
public static void main(String[] args) {
int i = 0;
try {
for (int j = 0; j < 20000; j++) {
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
# 运行结果
# 触发了垃圾回收
[GC (Allocation Failure) [PSYoungGen: 1536K->496K(2048K)] 1536K->512K(7680K), 0.0014248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
20000
Heap
PSYoungGen total 2048K, used 1491K [0x00000007bfd80000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1536K, 64% used [0x00000007bfd80000,0x00000007bfe78cf8,0x00000007bff00000)
from space 512K, 96% used [0x00000007bff00000,0x00000007bff7c010,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 5632K, used 16K [0x00000007bf800000, 0x00000007bfd80000, 0x00000007bfd80000)
object space 5632K, 0% used [0x00000007bf800000,0x00000007bf804000,0x00000007bfd80000)
Metaspace used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 345K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 12884 = 309216 bytes, avg 24.000
Number of literals : 12884 = 495800 bytes, avg 38.482
Total footprint : = 965104 bytes
Average bucket size : 0.644
Variance of bucket size : 0.645
Std. dev. of bucket size: 0.803
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
# 串池中的实例数量也没有达到创建的20000,说明部分实例对象被垃圾回收器回收
Number of entries : 17105 = 410520 bytes, avg 24.000
Number of literals : 17105 = 917296 bytes, avg 53.627
Total footprint : = 1807920 bytes
Average bucket size : 0.285
Variance of bucket size : 0.387
Std. dev. of bucket size: 0.622
Maximum bucket size : 4
2.6.6 StringTable性能调优
-
将StringTable的桶个数加大可以有效提升StringTable的性能。原因是减少了哈希碰撞,使得查找速率提升。(-XX:StringTableSize=桶个数(默认60013))
-
考虑将字符串对象是否入池
2.7 直接内存 Direct Memory
2.7.1 概述
直接内存用于数据缓冲区,分配回收的成本比较高,但读写性能高,不受JVM内存回收管理。
- 普通内存:需要从用户态向内核态申请资源,即用户态会创建一个java 缓冲区byte[],内核态会创建系统缓冲区。
- 直接内存:需要从用户态向内核态申请资源,即内核态会创建一块直接内存direct memory,这块direct memory内存可以在用户态、内核态使用。

2.7.2 直接内存和传统方式读取大文件
public class TestDemo {
private static String FROM = "/Users/yorick/Downloads/auth-resource.jar";
private static String TO = "/Users/yorick/Downloads/copy_auth-resource.jar";
private static int _1MB = 1024 * 1024;
public static void main(String[] args) {
io();
directBuffer();
}
//采用传统的读取文件方式
private static void io() {
long start = System.nanoTime();
try (FileInputStream fis = new FileInputStream(FROM);
FileOutputStream fos = new FileOutputStream(TO)) {
byte[] buffer = new byte[_1MB];
int len;
while ((len = fis.read(buffer)) != -1){
fos.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io time: " + (end - start));
}
//采用直接内存的读取文件方式
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel()) {
ByteBuffer buffer = ByteBuffer.allocateDirect(_1MB);
int len;
while ((len = from.read(buffer)) != -1) {
buffer.flip();
to.write(buffer);
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer time: " + (end - start));
}
}
2.7.3 DirectBuffer的分配和回收原理
- 使用了UnSafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
public class TestDemo {
private static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
//获取Unsafe对象
Unsafe unsafe = getUnsafe();
//分配内存,返回分配的内存地址
long memory = unsafe.allocateMemory(_1Gb);
//给分配的内存赋值为初始值0
unsafe.setMemory(memory,_1Gb,(byte) 0);
System.in.read();
//释放内存
unsafe.freeMemory(memory);
System.in.read();
}
private static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
网友评论