一、什么是JVM?
定义
Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)
好处
一次编写,到处运行
自动内存管理,垃圾回收机制
数组下标越界检查
比较
JVM JRE JDK的区别

二、内存结构
0、整体架构

1、程序计数器
作用
用于保存JVM中下一条所要执行的指令的地址
特点
2、虚拟机栈
定义
演示
代码
public class Main {public static void main(String[] args) {method1();}private static void method1() {method2(1, 2);}private static int method2(int a, int b) {int c = a + b;return c;}}1234567891011121314
在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点
问题辨析
垃圾回收是否涉及栈内存?
栈内存的分配越大越好吗?
方法内的局部变量是否是线程安全的?
Java.lang.stackOverflowError 栈内存溢出
发生原因
虚拟机栈中,栈帧过多(无限递归)
每个栈帧所占用过大
线程运行诊断
CPU占用过高
3、本地方法栈
一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法
4、堆
定义
通过new关键字创建的对象都会被放在堆内存
特点
堆内存溢出
java.lang.OutofMemoryError :java heap space. 堆内存溢出
堆内存诊断
jps
jmap
jconsole
jvirsalvm
5、方法区
定义

结构
方法区是概念上的,具体实现:JDK1.6是永久代,JDK1.8是元空间。

注:图片中【常量池】应该改为【运行时常量池】。
JDK1.8之前的永久代
Class:类的元信息、 例如上图中:field、methods、constructors
ClassLoader: 类加载器也在永久代存着。
运行时常量池:其中有个重要的StringTable(串池)
JDK1.8 元空间
JDK1.8 中永久代废弃了,方法区当然还是概念上的,实现变成了元空间,也包含Class、ClassLoader、运行时常量池,不过它已经不占用堆内存了,也就是说不归JVM来管理它的内存结构了,移出到本地内存当中。本地内存也是操作系统内存,同时还会跑其它的进程。
还有个不一样的地方,串池不放在元空间,移出到堆内存中。
方法区内存溢出
常量池
二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)
二进制字节码看不懂,需要 通过反编译来查看类的信息
获得对应类的.class文件
1
在JDK对应的bin目录下运行cmd,也可以在IDEA控制台输入

输入 javac 对应类的绝对路径
F:\JAVA\JDK8.0\bin>javac F:\Thread_study\src\com\nyima\JVM\day01\Main.java
输入完成后,对应的目录下就会出现类的.class文件
在控制台输入 javap -v 类的绝对路径
javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.class
然后能在控制台看到反编译以后类的信息了
Classfile /D:/Code/Huiwei/target/classes/com/jzt/jvm/HelloWorld.class Last modified 2022-5-28; size 557 bytes
MD5 checksum 8b8fb71f533f8ee3463924248f75dabc Compiled from "HelloWorld.java"public class com.jzt.jvm.HelloWorld
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/jzt/jvm/HelloWorld
#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/jzt/jvm/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.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/jzt/jvm/HelloWorld
#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.jzt.jvm.HelloWorld();
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 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/jzt/jvm/HelloWorld; 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 5: 0
line 6: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;}12345678910111213141516171819202122232425262728293031
运行时常量池
常量池与串池的关系
先看几道面试题
String s1 = "a";String s2 = "b";String s3 = "a" + "b";String s4 = s1 + s2;String s5 = "ab";String s6 = s4.intern();// 问System.out.println(s3 == s4);System.out.println(s3 == s5);System.out.println(s3 == s6);String x2 = new String("c") + new String("d");String x1 = "cd";x2.intern();// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢System.out.println(x1 == x2);123456789101112131415串池StringTable
特征
用来放字符串对象且里面的元素不重复
public class StringTableStudy {public static void main(String[] args) {String a = "a";
String b = "b";String ab = "ab";}}1234567常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串
0: ldc #2 // String a2: astore_13: ldc #3 // String b5: astore_26: ldc #4 // String ab8: astore_39: return1234567
当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)
当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中
当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中
最终StringTable [“a”, “b”, “ab”]
注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。
使用拼接字符串变量对象创建字符串的过程
public class StringTableStudy {public static void main(String[] args) {String a = "a";String b = "b";String ab = "ab";//拼接字符串对象来创建新的字符串String ab2 = a+b;
}}123456789反编译后的结果
Code:
stack=2, locals=5, args_size=1 0: ldc #2 // String a 2: astore_1 3: ldc #3 // String b 5: astore_2 6: ldc #4 // String ab 8: astore_3 9: new #5 // class java/lang/StringBuilder 12: dup 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 16: aload_1 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: aload_2 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: astore 4 29: return123456789101112131415161718192021
通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中
String ab = "ab";String ab2 = a+b;//结果为false,因为ab是存在于串池之中,ab2是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中System.out.println(ab == ab2);1234
使用拼接字符串常量对象的方法创建字符串
public class StringTableStudy {public static void main(String[] args) {String a = "a";String b = "b";String ab = "ab";String ab2 = a+b;//使用拼接字符串的方法创建字符串String ab3 = "a" + "b";}}12345678910反编译后的结果
Code:
stack=2, locals=6, args_size=1 0: ldc #2 // String a 2: astore_1 3: ldc #3 // String b 5: astore_2 6: ldc #4 // String ab 8: astore_3 9: new #5 // class java/lang/StringBuilder 12: dup 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 16: aload_1 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: aload_2 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: astore 4 //ab3初始化时直接从串池中获取字符串 29: ldc #4 // String ab 31: astore 5 33: return123456789101112131415161718192021222324
intern方法 1.8
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
如果串池中没有该字符串对象,则放入成功
如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
例1
public class Main {public static void main(String[] args) {//"a" "b" 被放入串池中,str则存在于堆内存之中String str = new String("a") + new String("b");//调用str的intern方法,这时串池中没有"ab",则会将该字符串对象放入到串池中,此时堆内存与串池中的"ab"是同一个对象String st2 = str.intern();//给str3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回String str3 = "ab";//因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为trueSystem.out.println(str == st2);System.out.println(str == str3);}}12345678910111213例2
public class Main {public static void main(String[] args) { //此处创建字符串对象"ab",因为串池中还没有"ab",所以将其放入串池中String str3 = "ab"; //"a" "b" 被放入串池中,str则存在于堆内存之中String str = new String("a") + new String("b"); //此时因为在创建str3时,"ab"已存在与串池中,所以放入失败,但是会返回串池中的"ab"String str2 = str.intern(); //falseSystem.out.println(str == str2); //falseSystem.out.println(str == str3); //trueSystem.out.println(str2 == str3);}}12345678910111213141516intern方法 1.6
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
无论放入是否成功,都会返回串池中的字符串对象
注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

StringTable 垃圾回收
StringTable在内存紧张时,会发生垃圾回收
StringTable调优
6、直接内存
属于操作系统,常见于NIO操作时,用于数据缓冲区
分配回收成本较高,但读写性能高
不受JVM内存回收管理
看个例子:文件的拷贝。缓冲区大小都是1MB
使用两个方法:



上述两种读写的性能:传统阻塞IO3秒左右,而directBuffer不到1秒。结论:使用ByteBuffer(直接内存)IO性能较高。
文件读写流程
传统阻塞IO

上图解释:首先Java的应用程序是不能直接读文件的,需要调用操作系统的读文件函数。CPU进入内核态,由操作系统去读文件。操作系统将磁盘中的文件读到系统缓存区,而系统缓冲区中的数据,Java的代码是不能直接运行的。所以Java会在堆内存中分配一块Java缓冲区(对应于new byte[])。刚才的代码要访问流中的数据,还需要再从系统缓冲区间接地读入到Java缓冲区。到了下一个状态,再调用输出流的写入操作。反复进行读写读写。把整个文件复制到目标位置。
问题:不必要的数据复制。因为Java应用程序访问不到系统缓冲区数据,所以数据还要再拷贝,导致不必要的数据复制,效率降低。
使用了DirectBuffer

上图解释:当调用ByteBuffer.allocateDirect()函数时,会分配一块直接内存。会在操作系统划出一块缓冲区(上图的direct memory),跟之前不一样的是:操作系统这块内存缓冲区,Java代码可以直接访问。直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率。
释放原理


直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放

通过
//通过ByteBuffer申请1M的直接内存 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);12
申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?
allocateDirect的实现
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }123DirectByteBuffer类
DirectByteBuffer(int cap) { // package-private
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)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象
att = null; }12345678910111213141516171819202122232425这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存
public void clean() { if (remove(this)) { try { this.thunk.run(); //调用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; } }); }12345678910111213141516对应对象的run方法
public void run() { if (address == 0) { // Paranoia return; }
unsafe.freeMemory(address); //释放直接内存中占用的内存
address = 0; Bits.unreserveMemory(size, capacity);}123456789直接内存的回收机制总结
为了防止Full GC对性能产生影响,我们打开开关:DisableExplicitGC,会产生如下问题:

上图:导致直接内存长时间得不到释放。
怎么解决?
去掉DisableExplicitGC?那不行,别的代码System.gc()会Full GC导致垃圾回收对性能产生影响。
使用unsafe直接释放即可,程序员手动管理这块内存。
三、常见面试题
谈谈对 OOM 的认识?如何排查 OOM 的问题?
除了程序计数器,其他内存区域都有 OOM 的风险。
栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;
堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;
方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等;
直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。
排查 OOM 的方法:
增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。
参考
一、什么是JVM
二、解密JVM【黑马程序员出品】