什么是 JVM ?定义好处

jvm内存溢出服务就挂了吗(VM概述内存结构)(1)

常见的 JVM

jvm内存溢出服务就挂了吗(VM概述内存结构)(2)

整体结构

jvm内存溢出服务就挂了吗(VM概述内存结构)(3)

内存结构程序计数器

jvm内存溢出服务就挂了吗(VM概述内存结构)(4)

定义

jvm内存溢出服务就挂了吗(VM概述内存结构)(5)

虚拟机栈

jvm内存溢出服务就挂了吗(VM概述内存结构)(6)

定义

jvm内存溢出服务就挂了吗(VM概述内存结构)(7)

问题栈内存溢出

public static void main(String[] args) throws Exception{ try { method(); } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(count); } } public static void method() { count ; method(); } Exception in thread "main" java.lang.StackOverflowError

本地方法栈定义

// Object 类中有大量的本地方法 public final native Class<?> getClass(); public native int hashCode(); protected native Object clone() throws CloneNotSupportedException; public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException;

jvm内存溢出服务就挂了吗(VM概述内存结构)(8)

定义堆内存溢出

public static void main(String[] args) throws Exception { String s = "a"; ArrayList<String> array = new ArrayList<>(); int count = 0; try { while (true) { s = "a"; array.add(s); count ; } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(count); } } Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

堆内存诊断

实例:

public static void main(String[] args) throws Exception { System.out.println("1..."); Thread.sleep(30000); byte[] bytes = new byte[1024 * 1024 * 10]; System.out.println("2..."); Thread.sleep(30000); bytes = null; System.gc(); System.out.println("3..."); Thread.sleep(1000000L); }

三次输入 jmap -heap pid 之后输出的部分内容如下

1️⃣ 第一次:程序刚开始

Eden Space: capacity = 66584576 (63.5MB) used = 7990344 (7.620185852050781MB) free = 58594232 (55.87981414794922MB) 12.000292680394931% used

2️⃣ 第二次:创建 10 MB byte 数组之后

Eden Space: capacity = 66584576 (63.5MB) used = 18476120 (17.620201110839844MB) free = 48108456 (45.879798889160156MB) 27.748348206046998% used

注意到 used 大小扩大了 10 MB

3️⃣ 第三次:垃圾回收之后

Eden Space: capacity = 66584576 (63.5MB) used = 1331712 (1.27001953125MB) free = 65252864 (62.22998046875MB) 2.0000307578740157% used

发现 used 减小明显。

还可以使用 jconsole 图形化工具

程序运行之后终端输入 jconsole 即可

jvm内存溢出服务就挂了吗(VM概述内存结构)(9)

使用 jvisualvm 获取更详细的堆内存描述:

jvisualvm // 终端输入

jvm内存溢出服务就挂了吗(VM概述内存结构)(10)

使用 堆 Dump 可以查看堆内具体信息。

方法区定义

jvm内存溢出服务就挂了吗(VM概述内存结构)(11)

jvm内存溢出服务就挂了吗(VM概述内存结构)(12)

方法区内存溢出

因为虚拟机默认使用本机内存作为元空间,内存较大,所以要调小一下元空间的大小。

jvm内存溢出服务就挂了吗(VM概述内存结构)(13)

jvm内存溢出服务就挂了吗(VM概述内存结构)(14)

jvm内存溢出服务就挂了吗(VM概述内存结构)(15)

输入参数

-XX:MaxMetaspaceSize=10m public class Test extends ClassLoader { public static void main(String[] args) { int j = 0; try { Test test = new Test(); 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 对象 } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(j); } } } Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space

和预想的不太一样,Compressed class space 是什么呢?

在 64 位平台上,HotSpot 使用了两个压缩优化技术,Compressed Object Pointers (“CompressedOops”) 和 Compressed Class Pointers。压缩指针,指的是在 64 位的机器上,使用 32 位的指针来访问数据(堆中的对象或 Metaspace 中的元数据)的一种方式。这样有很多的好处,比如 32 位的指针占用更小的内存,可以更好地使用缓存,在有些平台,还可以使用到更多的寄存器。

-XX: UseCompressedOops 允许对象指针压缩。

-XX: UseCompressedClassPointers 允许类指针压缩。

它们默认都是开启的,可以手动关闭它们。

在VM options中输入

-XX:-UseCompressedOops -XX:-UseCompressedClassPointers

再次运行结果如下

9344 Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

表明元空间内存溢出。

  • jdk1.6 永久代内存溢出

相同的代码和虚拟机参数配置,结果如下

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

表明永久代内存溢出

运行时常量池
  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

反编译字节码命令(终端先 cd 进入 out 目录下相应字节码文件的目录)

javap -v Class.class

  • 二进制字节码:由类基本信息、常量池、类方法定义、虚拟机指令组成

public class test02 { public static void main(String[] args) { System.out.println("hello world"); } } :\Project\JavaProject\Practice\out\production\Practice\demo04>javap -v test02.class Classfile /E:/Project/JavaProject/Practice/out/production/Practice/demo04/test02.class Last modified 2021-11-18; size 535 bytes MD5 checksum 6da0b7066cec4b7beb4be01700bf3897 Compiled from "test02.java" public class demo04.test02 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 // demo04/test02 #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 Ldemo04/test02; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 test02.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 demo04/test02 #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 demo04.test02(); // 构造方法 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 Ldemo04/test02; 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; } SourceFile: "test02.java"

  • 常量池可以给虚拟机指令提供一些常量符号,可以通过查表的方式查到。
StringTableStringTable 的数据结构
  • hash表(数组 链表)
  • 不可扩容
  • 存字符串常量,唯一不重复
  • 每个数组单元称为一个哈希桶
  • 大小至少是 1009
面试题

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呢 // x2.intern(); // String x1 = "cd"; System.out.println(x1 == x2); false true true false // 调换后,true

解析
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
  • jdk1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
  • jdk1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
字符串常量

String s1 = "a"; String s2 = "b"; String s3 = "ab";

反编译后的执行过程:

Constant pool: #1 = Methodref #6.#24 // java/lang/Object."<init>":()V #2 = String #25 // a #3 = String #26 // b #4 = String #27 // ab ... Code: stack=1, locals=4, 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: return ... 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象 ldc #2 会把 a 符号变为 "a" 字符串对象 ldc #3 会把 b 符号变为 "b" 字符串对象 ldc #4 会把 ab 符号变为 "ab" 字符串对象

字符串延迟加载

字符串变量拼接

String s1 = "a"; // 懒惰的 String s2 = "b"; String s3 = "ab"; String s4 = s1 s2;

反编译结果

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: return

字符串拼接的过程 new StringBilder().append("a").append("b").toString(),而StringBuilder的toString()方法又在底层创建了一个String对象

@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }

所以 s3 == s4 为 false

字符串常量拼接

String s1 = "a"; // 懒惰的 String s2 = "b"; String s3 = "ab"; String s4 = s1 s2; String s5 = "a" "b";

反编译结果

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 29: ldc #4 // String ab 31: astore 5 33: return

注意 29: ldc #4 // String ab 和 6: ldc #4 // String ab指向的是字符串常量池中相同的字符串常量 #4,说明 javac 在编译期间进行了优化,结果已经在编译期确定为 ab

所以 s3 == s5 为 true

intern 方法

String s = new String("a") new String("b");

反编译结果

Constant pool: ... #5 = String #30 // a ... #8 = String #33 // b ... Code: stack=4, locals=2, args_size=1 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V 7: new #4 // class java/lang/String 10: dup 11: ldc #5 // String a 13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V 16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: new #4 // class java/lang/String 22: dup 23: ldc #8 // String b 25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V 28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 34: astore_1 35: return ...

可以发现,创建了三个对象,"a","b" 以及StringBuilder.toString()创建的 "ab"。

字符串常量 "a","b" 进入串池,"ab" 是动态拼接出的一个字符串,没有被放入串池。

s 是一个变量指向堆中的 "ab" 字符串对象

调用 String.intern() 方法可以将这个字符串对象尝试放入串池,如果有则并不会放入,把串池中的对象返回;如果没有则放入串池, 再把串池中的对象返回。

注意这里说的返回是指调用 String.intern() 方法后返回的值。比如 String ss = s.intern() , ss 接收返回的对象,与 s 无关。而 s 只与对象本身有关,与返回值无关。

String x = "ab"; String s = new String("a") new String("b"); String s2 = s.intern(); System.out.println(s2 == x); System.out.println(s == x);

过程:

  • 字符串常量 "ab" 放入串池
  • "a""b" 放入串池
  • s 指向堆中创建的 "ab" 对象
  • 串池中已经有 "ab" 对象,则返回串池中的对象引用给变量 s2s 依然指向堆中的 "ab" 对象
  • s2 == xtrue
  • s == xfalse

如果调换一下位置

String s = new String("a") new String("b"); String s2 = s.intern(); String x = "ab"; System.out.println( s2 == x); System.out.println( s == x );

过程:

  • "a""b" 放入串池
  • s 指向堆中创建的 "ab" 对象
  • 串池中没有 "ab" 对象,则返回串池中的对象引用给变量 s2s 指向串池中的 "ab" 对象
  • s2 == xtrue
  • s == xtrue
StringTable 的位置

jvm内存溢出服务就挂了吗(VM概述内存结构)(16)

  • jdk1.6 StringTable 放在永久代中,与常量池放在一起
  • jdk1.8 StringTable 放在堆中
StringTable 垃圾回收
  • StringTable 会发生垃圾回收

-Xmx10m -XX: PrintStringTableStatistics -XX: PrintGCDetails -verbose:gc public static void main(String[] args) throws InterruptedException { 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); } } [GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->676K(9728K), 0.0010489 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] ... StringTable statistics: Number of buckets : 60013 = 480104 bytes, avg 8.000 Number of entries : 4388 = 105312 bytes, avg 24.000 Number of literals : 4388 = 284264 bytes, avg 64.782 Total footprint : = 869680 bytes

可以看到 entries 的个数小于 10000,从第一行也可以看出发生了 GC。

StringTable 调优调整 StringTable 的大小

-XX:StringTableSize=桶个数

  • 哈希桶越多,分布越分散,发生哈希冲突的可能性越低,效率越高
  • 字符串常量多的话,可以调大 StringTable 的大小,能增加哈希桶的个数,提供效率
考虑字符串是否入池
  • 使用 String.intern() 方法使重复字符串常量入池,减少堆的内存占用

public static void main(String[] args) throws IOException { List<String> address = new ArrayList<>(); System.in.read(); for (int i = 0; i < 10; i ) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) { String line = null; long start = System.nanoTime(); while (true) { line = reader.readLine(); if(line == null) { break; } address.add(line.intern()); // 字符串常量放入串池 } System.out.println("cost:" (System.nanoTime()-start)/1000000); } } System.in.read(); }

直接内存定义
  • Direct Memory
  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

jvm内存溢出服务就挂了吗(VM概述内存结构)(17)

Java 本身不具有磁盘读写能力,需要调用操作系统提供的函数。

当 CPU 从用户态切换为内核态时,操作系统中会划分出一个系统缓冲区,Java 无法直接访问系统缓冲区,而堆中存在 Java 缓冲区,数据进入系统缓冲区再进入 Java 缓冲区就可以被 Java 访问。

两个缓冲区直接存在不必要的数据复制。

jvm内存溢出服务就挂了吗(VM概述内存结构)(18)

直接内存可以使系统缓冲区和 Java 缓冲区共享,使 Java 可以直接访问系统缓冲区的数据,减少了不必要的数据复制,适合文件的 IO 操作。

public class Demo1_9 { static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4"; static final String TO = "E:\\a.mp4"; static final int _1Mb = 1024 * 1024; public static void main(String[] args) { io(); // io 用时:1535.586957 1766.963399 1359.240226 directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592 } 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); } }

分配和回收原理
  • ByteBuffer 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleanerclean方法调用 freeMemory 来释放直接内存

ByteBuffer 的 allocateDirect 方法

public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }

DirectByteBuffer 对象

// Primary constructor // 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); // 调用了 unsafe 类的 allocateMemory 方法 } 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)); // Cleaner 虚引用监控 DirectByteBuffer 对象 att = null; }

Cleanr 对象的 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; } }); } } }

Deallocator 任务对象

private static class Deallocator implements Runnable { private static Unsafe unsafe = Unsafe.getUnsafe(); private long address; private long size; private int capacity; private Deallocator(long address, long size, int capacity) { assert (address != 0); this.address = address; this.size = size; this.capacity = capacity; } public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); } }

DirectByteBuffer 这个 Java 对象被垃圾回收器调用的时候,会触发虚引用对象 Cleaner 中的 clean 方法,执行任务对象 Deallocator,调用任务对象中的 freeMemory 去释放直接内存。

禁用显式垃圾回收

禁用显式垃圾回收

-XX: DisableExplicitGC // 禁用显式的 System.gc()

System.gc() 触发的是 Full GC,回收新生代和老年代,程序暂停时间长,JVM 调优的时候可能会禁用掉,防止无意使用 System.gc() 。

但是禁用显式的 System.gc() ,直接内存不能被即时释放,可以通过直接调用 Unsafe 的 freeMemory 方法手动管理回收直接内存。

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); } }

,