共计 12795 个字符,预计需要花费 32 分钟才能阅读完成。
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); }
-