1. 什么是 JVM?
1.1 定义
JVM,即Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)
1.2 好处
- 一次编写,到处运行的基石
- 自动内存管理,垃圾回收功能
- 数据下标越界检查
- 多态
1.3 JVM、JRE、JDK 比较
1.4 常见的 JVM
JVM 只是套规范,基于这套规范的实现常见如下几种:
2. 内存结构
2.1 整体架构
JVM 内存结构主要包含:程序计数器、虚拟机栈、本地方法栈、堆、方法区,其中前三者是线程私有的,后两者是线程共享的。整体架构如下图:
2.2 程序计数器
2.2.1 作用
用于保存 JVM 中下一条要执行的指令的地址。
2.2.2 特点
线程私有
- CPU会为每个线程分配时间片,当当前线程的时间片使用完以后,CPU就会去执行另一个线程的代码。程序计数器是每个线程所私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一条指令
不会存在内存溢出。
- 因为永远只存储一个指令地址,JVM规范中唯一一个不存在OutOfMemoryError的区域
2.3 虚拟机栈
2.3.1 定义
每个线程运行需要的内存空间,称为虚拟机栈。每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的方法。
使用 IDE 调试演示,从控制台可以看到方法进入虚拟机栈后的样子,如下:
2.3.2 特点
- 线程私有的
- 存在栈内存溢出
2.3.3 问题辨析
垃圾回收是否涉及栈内存?
- 不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈,内存会自动释放。所以无需通过垃圾回收机制去回收栈内存
栈内存的分配越大越好吗?
- 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少
方法内的局部变量是否是线程安全的?
- 如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
- 如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题
- 代码示例:
//是线程安全的,因为局部变量sb只在m1()方法内使用 public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } //不是线程安全的,因为局部变量sb是作为参数传过来的,m2()执行的同时可能会被其他线程修改 public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } //不是线程安全的,因为局部变量sb是作为返回值,可能会被多个其他线程拿到并同时修改 public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; }
2.3.4 栈内存溢出
Java.lang.stackOverflowError 栈内存溢出
发生原因
- 虚拟机栈中,栈帧过多(比如无限递归)
- 每个栈帧所占用内存过大
2.3.5 线程运行诊断
CPU 占用过高。Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
- top 命令,查看哪个进程占用CPU过高
- 拿到占用CPU过高的进程ID后,再通过 ps 查看哪个线程占用 CPU 过高。"
ps H -eo pid, tid, %cpu | grep 查到的占用CPU过高的进程ID
" - 通过 "
jstack 进程id
" 命令查看进程中线程ID(nid),与上一步查出的线程ID(tid)来对比定位有问题的线程以及源码行号。注意jstack查找出的线程ID(nid)是16进制的需要转换。
程序运行很长时间没有结果 。一般可能是发生了线程死锁,这时需要找到死锁的线程
- 找到当前运行的java程序的进程ID(也可以使用
jps
命令) - 通过 "
jstack 进程id
" 命令查看进程中是否有死锁线程 演示
class A{} class B{} public class Demo004 { static A a = new A(); static B b = new B(); public static void main(String[] args) throws InterruptedException { new Thread(()->{ synchronized (a) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("我获得了 a 和 b"); } } }).start(); Thread.sleep(1000); new Thread(()->{ synchronized (b) { synchronized (a) { System.out.println("我获得了 a 和 b"); } } }).start(); } }
- 找到当前运行的java程序的进程ID(也可以使用
2.4 本地方法栈
一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法。
本地方法栈也是 线程私有的
2.5 堆
2.5.1 定义
通过new关键字创建的对象都会被放在堆内存
2.5.2 特点
- 所有线程共享。堆内存中的对象都需要考虑线程安全问题
- 有垃圾回收机制
- 存在堆内存溢出
2.5.3 堆内存溢出
java.lang.OutofMemoryError :java heap space 堆内存溢出
2.5.4 堆内存诊断工具
- jps工具:查看当前系统中有哪些java进程
- jmap工具:java内存映像工具。如查看堆内存占用情况 ,
jmap -heap pid
- jconsole工具:图形界面的,Java监视与管理控制台
- jvisualvm工具:图形界面的,多合一故障处理工具
2.6 方法区
2.6.1 定义
方法区用于存储每个类的结构,比如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法。方法区域可以是固定大小,也可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以收缩。
2.6.2 组成
方法区是一个抽象概念,JDK1.8以前方法区由永久代实现,JDK1.8以后方法区由元空间实现
2.6.3 方法区内存溢出
JDK1.8之前会导致永久代内存溢出
- 设置永久代最大内存参数:-XX:MaxPerSize=<size>
- 异常信息:java.lang.OutOfMemoryError: PerGen space
JDK1.8之后会导致元空间内存溢出
- 设置元空间最大内存参数:-XX:MaxMetaspaceSize=<size>
- 异常信息:java.lang.OutOfMemoryError: Metaspace
演示
/** * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
*/
public class Demo006 extends ClassLoader { // 继承ClassLoader,可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo006 test = new Demo006();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
```
![image](/img/bVbZOEr)
虽然我们自己编写的程序没有大量使用动态加载类,但如果我们在使用外部一些框架时,可能大量动态加载类,就可能会导致元空间内存溢出。如 spring、mybatis 的动态代理
2.6.4 常量池
- 常量池:Class文件中除了类的版本、字段、方法、接口等描述信息,还有常量池,用于存放编译期生成的各种字面量和符号引用。就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
- 运行时常量池:常量池是class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址翻译为真实地址
较于Class文件常量池,运行时常量池具有动态性,在运行期间也可以将新的常量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是String类的intern()方法
常量池与JVM字节码演示
public class Demo007 {
public static void main(String[] args) {
System.out.println("hello world");
}
}
通过 "javap -v <class文件>
" 反编译,查看二进制字节码,其中包含了类基本信息,常量池,类方法定义(包含虚拟机指令)。
进入Demo007.class 所在目录执行,javap -v Demo007.class
,输出结果如下,
Classfile /E:/codes/java_demos/out/production/jvm/indi/taicw/jvm/structure/methodarea/constantpool/Demo007.class
Last modified 2020-10-7; size 622 bytes
MD5 checksum 1356d96bb349e8a95e8c5df1cb36040c
Compiled from "Demo007.java"
public class indi.taicw.jvm.structure.methodarea.constantpool.Demo007
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 // indi/taicw/jvm/structure/methodarea/constantpool/Demo007
#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 Lindi/taicw/jvm/structure/methodarea/constantpool/Demo007;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Demo007.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 indi/taicw/jvm/structure/methodarea/constantpool/Demo007
#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 indi.taicw.jvm.structure.methodarea.constantpool.Demo007();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lindi/taicw/jvm/structure/methodarea/constantpool/Demo007;
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 8: 0
line 9: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "Demo007.java"
2.6.5 字符串常量池 StringTable
字符串常量池 是常量池中专门用来存放常量字符串的一块区域,使用 StringTable 进行存储,底层数据结构是一个哈希表。
StringTable 位置
- JDK1.6版本中,字符串常量池是在永久代
- JDK1.7版本及以后版本,JVM 已经将字符串常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放字符串常量池
StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象并放入字符串常量池
- 利用字符串常量池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder
- 字符串常量拼接的原理是编译期优化
可以使用
String#intern()
方法,主动将当前字符串加入到字符串常量池中,并返回常量池中该字符串对象的引用- JDK1.7及以后版本中调用
intern()
方法将这个字符串对象尝试放入串池,如果串池有则不会放入,如果没有则放入串池,并把串池中的对象引用返回 - JDK1.6 版本中调用
intern()
方法将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份放入串池,并把串池中的对象引用返回,而之前那个字符串对象还是存在的
- JDK1.7及以后版本中调用
字符串拼接与字符串常量池相关问题
public class Demo008 {
public static void main(String[] args) {
String s1 = "a"; //字符串 "a" 放入串池,StringTable ["a"]
String s2 = "b"; //字符串 "b" 放入串池,StringTable ["a", "b"]
String s3 = "ab"; //字符串 "ab" 放入串池,StringTable ["a", "b", "ab"]
//因为s1、s2是变量,只有运行时期才能确定具体值,所以底层实现是 new StringBuilder().append("a").append("b").toString(),返回一个新的"ab"字符串对象
String s4 = s1 + s2;
// javac 在编译期间的优化,结果已经在编译期确定为 "ab",所以等价于 s5 = "ab",此时常量池已存在不用再创建新的"ab"对象
String s5 = "a" + "b";
// s4调用intern()方法尝试把"ab"放入串池中,此时串池已存在"ab", 并返回串池中的"ab"对象引用赋值给s6
String s6 = s4.intern();
//false,原因:s3 是串池中"ab"字符串对象的引用,s4是堆中"ab"对象的引用
System.out.println(s3 == s4);
//true,原因:s3、s5 都是串池中"ab"字符串对象的引用
System.out.println(s3 == s5);
//true,原因:s3、s6 都是串池中"ab"字符串对象的引用
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
x2.intern(); //把字符串"cd"放入串池,StringTable ["a", "b", "ab", "cd"]
String x1 = "cd"; //直接把串池中的"cd"对象引用赋值给 x1
//JDK1.7及之后版本,结果为true, 原因:x2调用intern()方法尝试把"cd"放入串池,此时串池不存在"cd"放入成功,此时串池中的"cd"对象与堆中的是同一个
//JDK1.6版本,结果为false, 原因:x2调用intern()方法尝试把"cd"放入串池,此时串池不存在"cd"字符串,会复制一个新的"cd"对象放入串池中,此时串池中的"cd"对象与堆中的不是同一个
System.out.println(x1 == x2);
String x4 = new String("e") + new String("f");
String x3 = "ef"; //把字符串"cd"放入串池,StringTable ["a", "b", "ab", "cd", "ef"]
x4.intern();
//结果为false, 原因:x4调用intern()方法尝试把"ef"放入串池,此时串池已存在"cd"放入失败,此时x3是串池中"ef"对象的引用,而x4是堆中的"ef"对象引用
System.out.println(x3 == x4);
}
}
2.6.6 StringTable 垃圾回收
因为在jdk1.7版本以后,字符串常量池是放在堆中,如果堆空间不足,字符串常量池也会进行垃圾回收。如下演示代码,设置最大堆内存为10m 并打印 gc 信息
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo009 {
public static void main(String[] args) {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
2.6.7 StringTable 性能调优
StringTable 的数据结构是哈希表,所以可以适当增加哈希表桶的大小,来减少字符长放入StringTable所需的时间
- 设置StringTable桶大小参数:
-XX:StringTableSize=<桶个数>
- 设置StringTable桶大小参数:
- 对于有些重复的字符串可以考虑放入常量池,可以减少内存占用
2.7 直接内存
2.7.1 定义
直接内存并不是JVM内存区域的一部分,不受JVM内存回收管理,常见于NIO操作用于数据缓存区,分配回收成本高但读写性能高。
java程序资源文件读取过程中,未使用直接内存和使用直接内存区别如下:
未使用直接内存
- 需要从用户态向内核态申请资源,内核态会创建系统缓冲区,用户态会创建一个java 缓冲区byte[],然后数据从系统缓存区复制到java缓存区
使用直接内存
- 需要从用户态向内核态申请资源,即内核态会创建一块直接内存direct memory,这块direct memory内存可以在用户态、内核态使用。直接内存是操作系统和Java代码都可以访问的一块区域,无需将数据从系统内存复制到Java堆内存,从而提高了效率
使用普通内存方式与使用直接内存方式读取大文件效率比较,演示代码如下:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 演示 普通内存与直接内存读取文件效率
*/
public class Demo011 {
static final String FROM = "C:\\Users\\taichangwei\\Downloads\\CentOS-6.10-x86_64-bin-DVD2.iso"; //一个2.03G的文件
static final String TO = "E:\\a.iso";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io();
directBuffer();
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
/*
执行三次分别用时
io 用时:4944.8368
directBuffer 用时:1927.7912
io 用时:4989.7547
directBuffer 用时:2287.7028
io 用时:5022.2618
directBuffer 用时:2453.5307
*/
2.7.2 直接内存溢出
直接内存虽然不受JVM内存管理,但是物理内存是有限的,所以直接内存还是会存在内存溢出,java.lang.OutOfMemoryError: Direct buffer memory
演示直接内存溢出,代码如下:
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* 演示 直接内存溢出
*/
public class Demo012 {
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
}
}
循环了 36 次,使用了3.6G的内存导致内存溢出了
2.7.3 直接内存的分配及回收原理
底层使用了UnSafe对象完成直接内存的分配及回收,并且回收需要主动调用freeMemory()方法
直接内存分配及回收的底层原理演示,代码如下:
import sun.misc.Unsafe; import java.io.IOException; import java.lang.reflect.Field; public class Demo014 { static int _1Gb = 1024 * 1024 * 1024; public static void main(String[] args) throws IOException { Unsafe unsafe = getUnsafe(); // 分配内存 long base = unsafe.allocateMemory(_1Gb); unsafe.setMemory(base, _1Gb, (byte) 0); //暂定等待。此时查看任务管理器(windows系统),会发现有一个占用1G内存的java程序 System.in.read(); // 释放内存 unsafe.freeMemory(base); //暂定等待。此时查看任务管理器(windows系统),会发现刚才占用1G内存的java程序内存释放了 System.in.read(); } public static Unsafe getUnsafe() { try { //Unsafe 构造方法是私有化的,只能通过反射获得 Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null); return unsafe; } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } } }
通常情况是我们是使用 "
ByteBuffer.allocateDirect(int capacity)
" 来申请直接内存就可以了。ByteBuffer的实现类内部使用了Cleaner(虚引用)来检测ByteBuffer,一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler线程来调用Cleaner的clean()方法调用freeMemory()来释放内存allocateDirect(int capacity) 方法内部实现:
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
DirectByteBuffer(int cap) 方法内部实现
DirectByteBuffer(int cap) { super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
这里调用了一个Cleaner的create()方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里指的是DirectByteBuffer对象)被回收以后,就会调用Cleaner的clean()方法,来清除直接内存中占用的内存
public void clean() { if (remove(this)) { try { this.thunk.run(); } catch (final Throwable var2) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (System.err != null) { (new Error("Cleaner terminated abnormally", var2)).printStackTrace(); } System.exit(1); return null; } }); } } }
其中 "
this.thunk
" 是上一步传入的Deallocator
对象,它的run方法内部实现如下:public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。