目录
5、元空间/方法区(-XX:MaxMetaspaceSize=8m)
5.3.2 生产环境出现元空间内存溢出问题,应该锁定这些方面
5.9.1 使用-XX:StringTableSize=大小参数增加桶的数量使StringTable性能增加案例
5.9.2 使用字符串常量池对字符串较多的场景减少内存占用案例
什么是JVM?
Java Virtual Machine - java程序的运行环境(java二进制字节码的运行环境)
JVM好处?
- 一次编写,到处运行的基石【重点】
- 自动内存管理,垃圾回收功能【重点】
- 数据下标越界检查
- 多态,面向对象编程
JVM、JRE、JDK三者比较:
学习JVM有什么用?
- 面试
- 理解底层的实现原理
- 中高级程序员的必备技能
JVM组成有哪些?
常见的JVM
JAVA 内存结构组成
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
1、程序计数器
1.1 程序计数器定义
Program Counter Register程序计数器(寄存器)
1.2 程序计数器作用
作用:是记住下一条jvm指令的执行地址
特点:线程私有的; 不存在内存溢出,也是JVM规范中唯一没有OutOfMemoryError的区域
二进制字节码:JVM指令 —> 解释器 —> 机器码 —> CPU
程序计数器:记住下一条jvm指令的执行地址,硬件方面通过【寄存器】实现
示例: 二进制字节码:jvm指令 java 源代码
2、虚拟机栈(-Xss256k)
先了解一程数据结构
- 栈Stack,先进后出FILO
- 栈-线程运行需要的内存空间
- 栈帧-每个方法运行时需要的内存
2.1 栈定义
Java Virtual Machine Stacks (Java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
2.2 栈问题
- 垃圾回收是否涉及栈内存? 答案:栈内存不涉及垃圾回收
- 栈内存分配越大越好吗? 答案:栈内存不是越大越好,如果设置过大,会影响可用线程的数量;比如-Xss1m、-Xss2m,在总内存不变的情况下,可用线程数量会减少
- 方法内的局部变量是否线程安全? 答案:方法内的局部变量是线程安全,因为方法内的局部变量各自在自已独立的内存中;如果是static int 就是线程共享的,就不是线程安全;主要看变量是否是线程共享、还是线程私有
核心1:如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
核心2:如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
2.3 栈内存溢出(-Xss256k)
栈帧过多导致栈内存溢出,比如:递归,我们生产环境推荐尽量不使用递归
栈帧过大导致栈内存溢出
2.3 线程运行诊断(附案例)
2.3.1 cpu占用过高,如何诊断案例
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消耗过高的文件及行号
2.3.2 程序运行很长时间没有结果,如何诊断案例
nohup java -cp /root/JvmLearn-1.0-SNAPSHOT.jar com.jvm.stack.T07_StackDeadLock &
3、本地方法栈(不是Java编写的代码,通过C/C++)
4、堆(-Xmx8m)
4.1 堆的定义
- 通过new 关键字,创建对象都会使用堆内存
- 特点:
- 它是线程共享的,堆中对象需要考虑线程安全的问题
- 有垃圾回收机制
4.2 堆内存溢出问题及生产建议
代码参考: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);
- }
- }
-
- }
生产环境建议:如果内存比较大,内存溢出不会那么快的暴露;这时,我们可以将堆内存调小,让内存溢出尽早暴露
4.3 堆内存诊断工具介绍,及实操
- jps工具:查看当前系统中有哪些java进程
- jmap工具:查看堆内存占用情况 jmap -heap pid
- jstack 工具:线程监控
- jconsole工具:图形界面的,多功能的检测工具,可以连续监测
- jvisualvm工具:图形界面的,多功能的检测工具,可以连续监测;还有dump
代码参考: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);
- }
- }
- Jps
- Jmap -head pid 查看堆内存占用
- 在控制台上使用jconsole
4.3.1 垃圾回收后,内存占用仍然很高,排查方式案例
代码参考: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工具查看,占用最多的内存的对象是哪些。
5、元空间/方法区(-XX:MaxMetaspaceSize=8m)
5.1 JVM方法区定义
- 线程共享
- 在JVM启动时创建,在逻辑上属于堆的一部分(看厂商实现)
- 方法区也可能会内存溢出
5.2 方法区组成
5.3 方法区内存溢出
1.8 以前会导致永久代内存溢出
- 演示永久代内存溢出 java.lang.OutOfMemoryError: PerGen space
- -XX:MaxPerSize=8m
1.8 之后会导致元空间内存溢出(系统内存)
- 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
- -XX:MaxMetaspaceSize=8m
5.3.1 元空间内存溢出演示案例
Jdk1.8 参考代码:com.jvm.t03_metaspace.T01_MetaspaceOutOfMemoryError
- 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
- -XX:MaxMetaspaceSize=8m
5.3.2 生产环境出现元空间内存溢出问题,应该锁定这些方面
虽然我们自己编写的程序没有大量使用动态加载类,但如果我们在使用外部一些框架时,可能大量动态加载类,就可能会导致元空间内存溢出。
场景(动态加载类),如果框架使用不合理也会导致方法区内存溢出
- spring
- mybatis
5.4 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.4.1 字符串常量池JVM字节码方面原理演示
- // 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
- 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
5.5 StringTable
5.5.1 StringTable常量池与串池的关系
代码参考: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 字符串延迟加载
5.6 StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (JDK1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放放串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回
StringTable_intern_1.8
代码参考:com.jvm.t03_metaspace.T05_TestString02
5.7 StringTable位置
- JDK1.6版本,字符串常量池是在永久代中;
- JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
- JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。
JDK1.6 与 JDK1.8字符串常量池对比
5.7.1 JDK1.8 字符串常量池在堆中实例验证
代码参考: com.jvm.t03_metaspace.T07_StringTablePosition
5.8 StringTable垃圾回收
因为在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);
- }
- }
- }
5.9 StringTable 性能调优(案例)
代码参考:com.jvm.t04_stringtable.T09_StringTableSizeForPerformance
- 调整 -XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
5.9.1 使用-XX:StringTableSize=大小
参数增加桶的数量使StringTable
性能增加案例
序号 | StringTableSize大小 | 运行耗时(单位毫秒) |
1 | 1009 | 11444 ![]() |
2 | 10009 | 1765 ![]() |
3 | 100009 | 430 ![]() |
将StringTable桶调小些,示例操作如下:
将StringTable桶调大些,示例操作如下:
5.9.2 使用字符串常量池对字符串较多的场景减少内存占用案例
- /**
- * 演示串池大小对性能的影响
- * -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:放入字符串常量池中,运行情况截图
6、直接内存Direct Memory
6.1 直接内存定义
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
6.2 原理讲解
- 普通内存
- 需要从用户态向内核态申请资源,即用户态会创建一个java 缓冲区byte[],内核态会创建系统缓冲区。
- 直接内存
- 需要从用户态向内核态申请资源,即内核态会创建一块直接内存direct memory,这块direct memory内存可以在用户态、内核态使用。
通常使用内存(未使用直接内存) VS 直接内存,原理对比图
6.3 直接内存与传统方式读取大文件耗时对比案例
接下来,我们将对一个大约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 | 有缓存 |
6.4 直接内存溢出案例
- /**
- * 演示直接内存溢出 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 对方法区的实现称为元空间
- }
- }
6.5 分配和使用原理
代码参考: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();
- }
- }
6.6 分配和回收原理及案例演示
- 使用了UnSafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
- /**
- * 直接内存分配的底层原理: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);
- }
- }
- }
文章最后,给大家推荐一些受欢迎的技术博客链接:
- JAVA相关的深度技术博客链接
- Flinak 相关技术博客链接
- Spark 核心技术链接
- 设计模式 —— 深度技术博客链接
- 机器学习 —— 深度技术博客链接
- Hadoop相关技术博客链接
- 超全干货--Flink思维导图,花了3周左右编写、校对
- 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
- 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
- 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
- 深入聊聊Java 垃圾回收机制【附原理图及调优方法】
欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!
评论记录:
回复评论: