目录
什么是JVM?
JVM好处?
JVM、JRE、JDK三者比较:
学习JVM有什么用?
JVM组成有哪些?
常见的JVM
JAVA 内存结构组成
1、程序计数器
1.1 程序计数器定义
1.2 程序计数器作用
2、虚拟机栈(-Xss256k)
2.1 栈定义
2.2 栈问题
2.3 栈内存溢出(-Xss256k)
2.3 线程运行诊断(附案例)
2.3.1 cpu占用过高,如何诊断案例
2.3.2 程序运行很长时间没有结果,如何诊断案例
3、本地方法栈(不是Java编写的代码,通过C/C++)
4、堆(-Xmx8m)
4.1 堆的定义
4.2 堆内存溢出问题及生产建议
4.3 堆内存诊断工具介绍,及实操
4.3.1 垃圾回收后,内存占用仍然很高,排查方式案例
5、元空间/方法区(-XX:MaxMetaspaceSize=8m)
5.1 JVM方法区定义
5.2 方法区组成
5.3 方法区内存溢出
5.3.1 元空间内存溢出演示案例
5.3.2 生产环境出现元空间内存溢出问题,应该锁定这些方面
5.4 运行时常量池
5.4.1 字符串常量池JVM字节码方面原理演示
5.5 StringTable
5.5.1 StringTable常量池与串池的关系
5.6 StringTable特性
5.7 StringTable位置
5.7.1 JDK1.8 字符串常量池在堆中实例验证
5.8 StringTable垃圾回收
5.9 StringTable 性能调优(案例)
5.9.1 使用-XX:StringTableSize=大小参数增加桶的数量使StringTable性能增加案例
5.9.2 使用字符串常量池对字符串较多的场景减少内存占用案例
6、直接内存Direct Memory
6.1 直接内存定义
6.2 原理讲解
6.3 直接内存与传统方式读取大文件耗时对比案例
6.4 直接内存溢出案例
6.5 分配和使用原理
6.6 分配和回收原理及案例演示
Java Virtual Machine - java程序的运行环境(java二进制字节码的运行环境)
面试
理解底层的实现原理
中高级程序员的必备技能
Program Counter Register程序计数器(寄存器)
作用:是记住下一条jvm指令的执行地址
特点:线程私有的; 不存在内存溢出,也是JVM规范中唯一没有OutOfMemoryError的区域
二进制字节码:JVM指令 —> 解释器 —> 机器码 —> CPU
程序计数器:记住下一条jvm指令的执行地址,硬件方面通过【寄存器】实现
示例: 二进制字节码:jvm指令 java 源代码
先了解一程数据结构
Java Virtual Machine Stacks (Java虚拟机栈)
核心1:如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
核心2:如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈帧过多导致栈内存溢出,比如:递归,我们生产环境推荐尽量不使用递归
栈帧过大导致栈内存溢出
1. 用top定位哪个进程对cpu的占用过高
2. ps H -eo pid,tid,%cpu 查看linux所有进程、线程、CPU消耗情况
3. ps H -eo pid,tid,%cpu | grep 进程id 用ps命令进一步定位哪个线程引起的CPU占用过高
4. jstack 进程pid 需要将十进制的线程id转成16进制; 可以根据线程id找到有问题的线程,进一步定位问题代码的源码行号
通过上述方式找到了源代码CPU消耗过高的文件及行号
nohup java -cp /root/JvmLearn-1.0-SNAPSHOT.jar com.jvm.stack.T07_StackDeadLock &
代码参考:com.jvm.t02_heap.T01_HeapOutOfMemoryError
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m
*/
public class T01_HeapOutOfMemoryError {
public static void main(String[] args) {
int i = 0;
try {
List list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
TimeUnit.MILLISECONDS.sleep(2000);
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
生产环境建议:如果内存比较大,内存溢出不会那么快的暴露;这时,我们可以将堆内存调小,让内存溢出尽早暴露
代码参考:com.jvm.t02_heap.T02_HeapUseUpAndDown
/**
* 演示堆内存
*/
public class T02_HeapUseUpAndDown {
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);
}
}
代码参考:com.jvm.t02_heap.T03_HeapAfterGcMemStillHigh
/**
* 演示查看对象个数 堆转储 dump
*/
public class T03_HeapAfterGcMemStillHigh {
public static void main(String[] args) throws InterruptedException {
List students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
// Student student = new Student();
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024 * 1024];
}
解决方式:jvisualvm 可以使用dump,查找最大的对象堆转储 dump(基于上述问题,使用工具进行查看); 在测试环境下,我们可以开启dump文件记录,然后将dump文件导入到jvisualvm工具查看,占用最多的内存的对象是哪些。
1.8 以前会导致永久代内存溢出
1.8 之后会导致元空间内存溢出(系统内存)
Jdk1.8 参考代码:com.jvm.t03_metaspace.T01_MetaspaceOutOfMemoryError
虽然我们自己编写的程序没有大量使用动态加载类,但如果我们在使用外部一些框架时,可能大量动态加载类,就可能会导致元空间内存溢出。
场景(动态加载类),如果框架使用不合理也会导致方法区内存溢出
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class T02_StringHelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
将上述编译好的class文件进行反汇编:Javap -v HelloWord.class 反编译结果如下:
D:\software\Java\jdk1.8.0_211\bin\javap.exe -v com.jvm.t03_metaspace.T02_MetaspaceConstantPool
Classfile /D:/lei_test_project/idea_workspace/Jvm_Learn/target/classes/com/jvm/t03_metaspace/T02_MetaspaceConstantPool.class
Last modified 2020-7-29; size 623 bytes
MD5 checksum 6b5272fbb2c0ca06c0e460818756710d
Compiled from "T02_MetaspaceConstantPool.java"
public class com.jvm.t03_metaspace.T02_MetaspaceConstantPool
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."":()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/jvm/t03_metaspace/T02_MetaspaceConstantPool
#6 = Class #27 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/jvm/t03_metaspace/T02_MetaspaceConstantPool;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 T02_MetaspaceConstantPool.java
#20 = NameAndType #7:#8 // "":()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/jvm/t03_metaspace/T02_MetaspaceConstantPool
#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.jvm.t03_metaspace.T02_MetaspaceConstantPool();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/jvm/t03_metaspace/T02_MetaspaceConstantPool;
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: "T02_MetaspaceConstantPool.java"
Process finished with exit code 0
代码参考:com.jvm.t03_metaspace.T03_MetaspaceStringTable
// 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);
System.out.println(s3 == s5);
}
}
javap -v T03_MetaspaceStringTable.class 反编译如下:
5.5.2 StringTable 字符串延迟加载
StringTable_intern_1.8
代码参考:com.jvm.t03_metaspace.T05_TestString02
JDK1.6 与 JDK1.8字符串常量池对比
代码参考: com.jvm.t03_metaspace.T07_StringTablePosition
因为在jdk1.8中,字符串常量池是放在堆中,如果堆空间不足,字符串常量池也会进行垃圾回收
代码参考:com.jvm.t04_stringtable.T08_StringTableGc
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
// 因为在jdk1.8中,字符串常量池是放在堆中,如果堆空间不足,字符串常量池也会进行垃圾回收
public class T08_StringTableGc {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 10000; j++) { // 前后运行100次、10000次,进行对比。j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
代码参考:com.jvm.t04_stringtable.T09_StringTableSizeForPerformance
-XX:StringTableSize=大小
参数增加桶的数量使StringTable
性能增加案例序号 | StringTableSize大小 | 运行耗时(单位毫秒) |
1 | 1009 | 11444 ![]() |
2 | 10009 | 1765 ![]() |
3 | 100009 | 430 ![]() |
将StringTable桶调小些,示例操作如下:
将StringTable桶调大些,示例操作如下:
/** * 演示串池大小对性能的影响 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009 *
* 字符串常量池默认桶数组大小为:60013,对字符串常量池调优主要是调节桶数据大小;如果字符串数量较多,则需要将此调大些,以减少查询复杂度(hash碰撞机率) */ public class T09_StringTableSizeForPerformance { public static void main(String[] args) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("in/linux.words"), "utf-8"))) { String line = null; long start = System.nanoTime(); while (true) { line = reader.readLine(); if (line == null) { break; } line.intern(); } System.out.println("cost:" + (System.nanoTime() - start) / 1000000); } } }
说明一下:in/linux.words 大约单词量在479829个,上述代码运行结果截图,如下:
读取大约48万单词 | 堆内存占用大小 | 耗时 |
未放入字符串池 | 约300兆 | 较短 |
放入字符串池 | 约70兆 | 较长 |
运行结果1:未放入字符串常量池中,运行情况截图
运行结果2:放入字符串常量池中,运行情况截图
通常使用内存(未使用直接内存) VS 直接内存,原理对比图
接下来,我们将对一个大约1.29G大小的视频文件进行读取并写入指定文件中,即复制。代码如下:
package com.jvm.t05_direct;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class T01_IoVsDirectBuffer {
static final String FROM = "E:\\Flink CEP.mp4";
static final String TO = "E:\\a.mp4";
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 | 直接内存directBuffer | 说明 |
测试1 | 18871.591 ms | 6335.745 ms | 没有缓存 |
测试2 | 5710.124 ms | 5497.707 ms | 有缓存 |
测试3 | 7355.304 ms | 5103.806 ms | 有缓存 |
/**
* 演示直接内存溢出 java.lang.OutOfMemoryError: Direct buffer memory
*/
public class T02_DirectOutOfMemory {
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
List list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
// jdk8 对方法区的实现称为元空间
}
}
代码参考:com.jvm.t05_direct.T03_DirectMemoryGcBySystemGc
/** * 禁用显式回收对直接内存的影响 *
* 因为程序调用System.gc() 会触发full gc,可能会长时间在垃圾回收 *
* 为了避免程序员显示调用System.gc(), 我们一般禁用显式调用System.gc() * 禁用显式System.gc(),会对直接内存有影响,为此,我们需要通过unSafe类的freeMemory()方法来释放直接内存 */ public class T03_DirectMemoryGcBySystemGc { static int _1Gb = 1024 * 1024 * 1024; /* * -XX:+DisableExplicitGC 显式的 */ public static void main(String[] args) throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb); System.out.println("分配完毕..."); System.in.read(); System.out.println("开始释放..."); byteBuffer = null; System.gc(); // 显式的垃圾回收,Full GC System.in.read(); } }
/**
* 直接内存分配的底层原理:Unsafe
*
* 虚引用关联的对象被回收了,就会触发虚引用对象的clean方法,续而调用Unsafe的freeMemory() 方法
*
* 6.3 分配和回收原理
*
* 使用了UnSafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
*
* ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,
* 那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
*/
public class T04_DirectMemoryGcByUnsafe {
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);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
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);
}
}
}
文章最后,给大家推荐一些受欢迎的技术博客链接:
欢迎扫描下方的二维码或 搜索 公众号“10点进修”,我们会有更多、且及时的资料推送给您,欢迎多多交流!