内存结构
Program Counter Register 程序计数器(寄存器):
作用: 是记住下一条jvm指令的执行地址
特点
每一个线程都对应一个程序计数器, 用于存储下一个指令的地址, 不存在内存溢出的原因是 :
首先,我们熟悉的栈和堆,都是可以通过运行时对内存需求的扩增导致内存不够用的情况
比如某个线程递归调用,随着调用层次的加深,可能会出现栈空间不足的情况,这时候如果可以动态扩增,jvm就会向申请更多的内存空间来扩充栈,当没有更多的内存空间得以申请的时候,就会发生OutOfMemoryError。
但是,程序计算器仅仅只是一个运行指示器,它所需要存储的内容仅仅就是下一个需要待执行的命令的地址,无论代码有多少,最坏情况下死循环也不会让这块内存区域超限,因为程序计算器所维护的就是下一条待执行的命令的地址,所以不存在OutOfMemoryError
package cn.knightzz.register; import java.io.PrintStream; public class TestProgramCounter { public static void main(String[] args) { PrintStream out = System.out; out.println(1); out.println(2); out.println(3); out.println(4); } }
使用 jclassib 插件得到的字节码如下
0 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> 3 astore_1 4 aload_1 5 iconst_1 6 invokevirtual #3 <java/io/PrintStream.println : (I)V> 9 aload_1 10 iconst_2 11 invokevirtual #3 <java/io/PrintStream.println : (I)V> 14 aload_1 15 iconst_3 16 invokevirtual #3 <java/io/PrintStream.println : (I)V> 19 aload_1 20 iconst_4 21 invokevirtual #3 <java/io/PrintStream.println : (I)V> 24 return
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
Java中每个线程运行的时候,需要给每个线程划分一个内存空间。虚拟机栈其实是线程运行时需要的内存空间。一个线程,就需要一个栈,如果是多个线程,就需要多个虚拟机栈
比如说我一个线程使用的是一个栈内存,那一个线程假如用了一兆内存,假如总共的物理内存假设有500兆,那理论上可以有五百个线程同时运行,但假如把每个线程的栈内存设置了2M内存,那么理论上只能同时运行250个线程。所以栈内存并不是划分的越大越好。把他设置大了,通常只是能够进行更多次的方法递归调用,而不会增强运行的效率,反而会影响到线程数目的变少。所以不建议设置为太大,一般采用系统默认的即可
看一个变量是不是线程安全,要看多个线程对这个变量是共享的还是这个变量被每个线程是私有的 :
public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); print(sb.toString()); } public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); print(sb.toString()); } public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; }
如上面的代码, 虽然 sb 是局部变量, 但是它通过返回值在m2和m3中共享, 多个线程之间操作有可能会出现并发问题
栈帧过多导致栈内存溢出
栈帧过大导致栈内存溢出
定位程序的步骤 :
ps H -eo pid,tid,%cpu | grep 进程id
(用ps命令进一步定位是哪个线程引起的cpu占用过高)依然使用上面的方法去定位, 另外使用 jstack 命令去查看返回的信息
本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别 :
Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法
当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。
然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
本地方法栈的特点 :
本地方法栈是一个后入先出(Last In First Out)栈。
由于是线程私有的,本地方法栈生命周期随着线程,线程启动而产生,线程结束而消亡。
本地方法栈会抛出 StackOverflowError 和 OutOfMemoryError 异常
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放**对象实例和数组**
package cn.knightzz.heap; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; @Slf4j public class HeadMemoryOutTest { public static void main(String[] args) { int count = 0; try { List<String> list = new ArrayList<>(); String str = "hello"; while (true){ list.add(str); str += str; count++; } } catch (Exception e) { log.debug("count : {} " , count); e.printStackTrace(); } } }
结果如下 :
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3332) at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448) at java.lang.StringBuilder.append(StringBuilder.java:136) at cn.knightzz.heap.HeadMemoryOutTest.main(HeadMemoryOutTest.java:27)
在经过多次循环后, JVM堆出现内存溢出
jmap -heap 进程id
package cn.knightzz.heap; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; @Slf4j public class HeadMemoryOutTest { public static void main(String[] args) { int count = 0; try { List<String> list = new ArrayList<>(); String str = "hello"; while (true){ list.add(str); log.debug("current count value : {}" , count); count++; } } catch (Exception e) { log.debug("count : {} " , count); e.printStackTrace(); } } }
上面的代码会不断的向 list 中添加字符串对象, 因为new关键字创建的对象和数组存储在堆中的,
首先是使用 jconsole
, 可以如下图可以看到, JVM堆占用内存随着时间在逐渐增加
另外, 我们也可以通过先使用 jps 命令查看 java 进程 :
$ jps 17828 RemoteMavenServer36 34292 Launcher 7668 21176 HeadMemoryOutTest 3704 32236 Jps
可以看到 21176 HeadMemoryOutTest
就是我们的当前的进程, 进程 id 是 21176 , 然后我们通过 jmap -heap 21176
:
$ jmap -heap 21176 Attaching to process ID 21176, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.152-b16 using thread-local object allocation. Parallel GC with 10 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 8556380160 (8160.0MB) NewSize = 178257920 (170.0MB) MaxNewSize = 2852126720 (2720.0MB) OldSize = 356515840 (340.0MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB) Heap Usage: PS Young Generation Eden Space: capacity = 2851078144 (2719.0MB) used = 0 (0.0MB) free = 524288 (0.5MB) 0.0% used PS Old Generation capacity = 1687683072 (1609.5MB) used = 1336502720 (1274.5883178710938MB) free = 351180352 (334.91168212890625MB) 79.19156992054015% used 6704 interned Strings occupying 538872 bytes.
通过这种方式可以看到JVM堆内存的占用情况 , 我们也可以在控制台使用 jvisualvm
命令 :
查看当前堆内存占用情况
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
栈因为是运行单位,因此里面存储的信息都是跟当前线程相关的信息。包括:局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
在方法中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。堆内存用于存放由new创建的对象和数组。
在Java中一个线程就会相应有一个线程栈与之对应,这点保证了程序的并发运行
而堆则是所有线程共享的,也可以理解为多个线程访问同一个对象,比如多线程去读写同一个对象的值
栈内存溢出包括StackOverflowError和OutOfMemoryError。
StackOverflowError
:线程请求的栈深度大于虚拟机所允许的深度OutOfMemoryError
:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存;堆内存溢出是OutOfMemoryError
。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出OutOfMemoryError
异常
JVM 有一个跨所有线程通用的方法区。它包含每个类的元素,如常量池、字段、方法本地数据、方法代码、构造函数代码等,用于类和对象/接口的初始化。
这个方法区是在 JVM 启动期间创建的。它通常是堆区域的一部分。它可以是固定大小或变化的。它的内存可能不是连续的。
JVM 实现可以让程序员控制方法区域的创建、其大小等。
如果方法区域内存不足以满足分配请求,则 JVM 会抛出 OutOfMemoryError
。
1.8版本的方法区已经是一个概念上的东西, 它被元空间所替代, 并且从JVM内存空间移除, 放入本地内存空间(操作系统进程)
package cn.knightzz.method.area; import jdk.internal.org.objectweb.asm.ClassWriter; import jdk.internal.org.objectweb.asm.Opcodes; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "MetaSpaceOutMemoryTest") public class MetaSpaceOutMemoryTest extends ClassLoader { public static void main(String[] args) { // 需要设置参数, 减小方法区的大小 : -XX:MaxMetaspaceSize=8m int count = 0; try { MetaSpaceOutMemoryTest test = new MetaSpaceOutMemoryTest(); for (int i = 0; i < 100000; i++) { // ClassWriter的作用是生成类字节码 ClassWriter cw = new ClassWriter(0); // 版本号, public, 类名, 包名, 父类, 接口 cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 返回 bytes[] byte[] codes = cw.toByteArray(); test.defineClass("Class" + i, codes, 0, codes.length); count++; } } finally { log.debug("count : {}", count); } } }
上面的代码是在不断的加载类字节码 , 在运行的时候我们还需要设置 VM option 参数 : -XX:MaxMetaspaceSize=8M
运行结果如下 :
09:15:23.677 [main] DEBUG MetaSpaceOutMemoryTest - count : 4501 Exception in thread "main" java.lang.OutOfMemoryError: Metaspace at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.lang.ClassLoader.defineClass(ClassLoader.java:642) at cn.knightzz.method.area.MetaSpaceOutMemoryTest.main(MetaSpaceOutMemoryTest.java:33)
在 Spring 和 Mybatis 场景下, 在运行的时候会出现大量动态生成的类, 有可能会出现元空间的内存溢出
每一个class文件都有一个常量池,常量池保存着class的常量信息:字面量和符号引用。通俗来说,常量池就是class文件中的资源仓库,保存了文件运行时需要的常量信息,这些常量都是开发者定义出来的。 我们知道,每一个class文件都是javac编译来的,在编译过程中,java并没有保存方法、字段的内存布局,在运行时必须加载解析这些常量信息才能够将其翻译到具体的内存地址加以使用。
*.class
文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址常量池和运行时常量池的区别 :
首先我们定义一个 HelloWorld 的java 文件
package cn.knightzz.method.constant.pool; /** * @author 王天赐 * @title: HelloWorld * @projectName hm-jvm-codes * @description: * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-01 09:38 */ public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } }
然后编译完成以后, 执行 javap -v HelloWorld.class
使用 -v
显示详细信息
$ javap -v HelloWorld.class Classfile /F:/JavaCode/hm-jvm-codes/jvm-chapter-01/target/classes/cn/knightzz/method/constant/pool/HelloWorld.class Last modified 2022-9-1; size 599 bytes MD5 checksum 7a129246b07e9399e7d7b537560133f6 Compiled from "HelloWorld.java" public class cn.knightzz.method.constant.pool.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 // cn/knightzz/method/constant/pool/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 Lcn/knightzz/method/constant/pool/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 cn/knightzz/method/constant/pool/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 cn.knightzz.method.constant.pool.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 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/knightzz/method/constant/pool/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 16: 0 line 18: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; } SourceFile: "HelloWorld.java"
简单介绍下 : 这些都是类的基本信息, 类名以及版本信息
Classfile /F:/JavaCode/hm-jvm-codes/jvm-chapter-01/target/classes/cn/knightzz/method/constant/pool/HelloWorld.class Last modified 2022-9-1; size 599 bytes MD5 checksum 7a129246b07e9399e7d7b537560133f6 Compiled from "HelloWorld.java" public class cn.knightzz.method.constant.pool.HelloWorld minor version: 0 major version: 52 // 内部版本信息
其次是常量池 :
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 // cn/knightzz/method/constant/pool/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 Lcn/knightzz/method/constant/pool/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 cn/knightzz/method/constant/pool/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
#20
是对应的符号, 后面是实际的方法或者类 , 例如 getstatic #2
, #2
在常量池中就是 #2 = Fieldref #21.#22
然后再往下面就是方法指令定义 :
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 16: 0 line 18: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;
ldc #3
的作用就是加载 字符串 Hello World
JVM解释器在解释指令的时候, 看到的只有上面的指令, 具体的一些常量属性需要去常量池中去找
package cn.knightzz.method.string_table; import lombok.extern.slf4j.Slf4j; /** * @author 王天赐 * @title: StringTableQuestion * @projectName hm-jvm-codes * @description: * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-01 10:12 */ @Slf4j(topic = "StringTableQuestion") @SuppressWarnings("all") public class StringTableQuestion { public static void main(String[] args) { String s1 = "a"; String s2 = "b"; String s3 = "a" + "b"; // 编译期会直接优化成 "ab" String s4 = s1 + s2; // 等价于 new String("ab") , s4 = new StringBuilder("c").append("d").toString(), 对象是存储在堆中的 String s5 = "ab"; // 返回与s4的字符串是同一对象的字符串,并且来自同一个常量池 // 当调用 intern 方法时,如果池中已经包含一个等于该String对象的字符串,由equals(Object)方法确定,则返回池中的字符串。 // 否则,将此String对象添加到池中并返回对该String对象的引用。 // 因此,对于任何两个字符串s和t ,当且仅当s.equals(t)为true时, s.intern() == t.intern()才为true 。 // return : 与此字符串具有相同内容的字符串,但保证来自唯一字符串池。 String s6 = s4.intern(); // 先查看s4对应的字符串:"ab" 是是否在常量池中, 如果在,直接返回, 如果不在就把 "ab" 放到字符串常量池中, 然后再返回字符串中"ab"的引用 // question System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6); // 通过new关键字创建的对象都放在堆中了 String x2 = new String("c") + new String("d"); // 等价于 new String("cd") ==> x2 = new StringBuilder("c").append("d").toString() String x1 = "cd"; String x3 = x2.intern(); // question : 如果调换最后两行的顺序, 下面的结果是什么 ? , 如果在JDK1.6 环境下呢? System.out.println(x1 == x2); // 这个结果是 false, 原因是 x1 是存放在常量池中, x2 是存在于堆中, 二者不是同一个对象 System.out.println(x1 == x3); } }
s4.intern()
: 首先去看
s3 == s4
: false, 因为 s3在编译器就会被优化成 "ab", 然后存储在常量池中, 但是 s4 是直接存储在堆中的
s3 == s5
: true, 因为s3在编译器被优化成"ab"
, 然后会被到StringTable中(因为字符串常量池中没有"ab"), 执行到s5的时候, JVM将"ab"
符号创建为字符串对象, 然后去StringTable中找, 因为s3已经将"ab"
放到了字符串常量池中
s3 == s6
: true , 因为 s4.intern()
: s4 的对象是存储在堆中的, 调用 intern() 方法, 会先去 StringTable中查看是否已经有了 "ab"
, 如果有的话直接返回常量池中的"ab"
对象, 如果没有的话, 就把 s4 对应的字符串变量放入常量池中, 然后再返回常量池中对象的引用, 如果是 JDK1.6的话, 会直接复制一个副本, 然后将字符串对象存到常量池中
x1 == x2
: false , x2 存储在堆中, x1 存储在常量池中
x1 == x3
: true , x2.intern() 方法先查看StringTable中是否有"cd" , 但是因为 x1先执行, StringTable中已经有了 "cd" , 所以会直接返回StringTable中的"cd", x3 == x1
如果调换 String x1 = "cd";
和 x2.intern();
的话, x1 == x2 的结果是 ture, 因为 x2.intern();
先执行以后, 原本存在于堆中的 String对象会被放到StringTable中, 然后
String x1 = "cd" 执行时, 因为StringTable中已经有了"cd" 的String对象, 所以,会直接返回StringTable的对象
我们使用一个简单的小案例 :
package cn.knightzz.method.string_table; public class ConstantPoolAndStringPool { public static void main(String[] args) { // 常量池 String s1 = "a"; String s2 = "b"; String s3 = "c"; } }
使用 javap -v ConstantPoolAndStringPool.class
命令生成对应的class信息 :
$ javap -v ConstantPoolAndStringPool.class Classfile /F:/JavaCode/hm-jvm-codes/jvm-chapter-01/target/classes/cn/knightzz/method/string_table/ConstantPoolAndStringPool.class Last modified 2022-9-1; size 590 bytes MD5 checksum 9fd394fc2f79e13141c181a727bed9d6 Compiled from "ConstantPoolAndStringPool.java" public class cn.knightzz.method.string_table.ConstantPoolAndStringPool minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#24 // java/lang/Object."<init>":()V #2 = String #25 // a #3 = String #26 // b #4 = String #27 // c #5 = Class #28 // cn/knightzz/method/string_table/ConstantPoolAndStringPool #6 = Class #29 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcn/knightzz/method/string_table/ConstantPoolAndStringPool; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 s1 #19 = Utf8 Ljava/lang/String; #20 = Utf8 s2 #21 = Utf8 s3 #22 = Utf8 SourceFile #23 = Utf8 ConstantPoolAndStringPool.java #24 = NameAndType #7:#8 // "<init>":()V #25 = Utf8 a #26 = Utf8 b #27 = Utf8 c #28 = Utf8 cn/knightzz/method/string_table/ConstantPoolAndStringPool #29 = Utf8 java/lang/Object { public cn.knightzz.method.string_table.ConstantPoolAndStringPool(); 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 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/knightzz/method/string_table/ConstantPoolAndStringPool; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC 3 7 1 s1 Ljava/lang/String; 6 4 2 s2 Ljava/lang/String; 9 1 3 s3 Ljava/lang/String; } SourceFile: "ConstantPoolAndStringPool.java" knight'z'z@DESKTOP-VAQG1TR MINGW64 /f/JavaCode/hm-jvm-codes/jvm-chapter-01/target/classes/cn/knightzz/method/string_table (master) $ javap -v ConstantPoolAndStringPool.class Classfile /F:/JavaCode/hm-jvm-codes/jvm-chapter-01/target/classes/cn/knightzz/method/string_table/ConstantPoolAndStringPool.class Last modified 2022-9-1; size 591 bytes MD5 checksum 028e1fb80be1ce076cb7ee1dd5b94bd6 Compiled from "ConstantPoolAndStringPool.java" public class cn.knightzz.method.string_table.ConstantPoolAndStringPool minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#24 // java/lang/Object."<init>":()V #2 = String #25 // a #3 = String #26 // b #4 = String #27 // ab #5 = Class #28 // cn/knightzz/method/string_table/ConstantPoolAndStringPool #6 = Class #29 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcn/knightzz/method/string_table/ConstantPoolAndStringPool; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 s1 #19 = Utf8 Ljava/lang/String; #20 = Utf8 s2 #21 = Utf8 s3 #22 = Utf8 SourceFile #23 = Utf8 ConstantPoolAndStringPool.java #24 = NameAndType #7:#8 // "<init>":()V #25 = Utf8 a #26 = Utf8 b #27 = Utf8 ab #28 = Utf8 cn/knightzz/method/string_table/ConstantPoolAndStringPool #29 = Utf8 java/lang/Object { public cn.knightzz.method.string_table.ConstantPoolAndStringPool(); 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 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/knightzz/method/string_table/ConstantPoolAndStringPool; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC 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 LineNumberTable: line 17: 0 line 18: 3 line 19: 6 line 20: 9 LocalVariableTable: Start Length Slot Name Signature 0 10 0 args [Ljava/lang/String; 3 7 1 s1 Ljava/lang/String; 6 4 2 s2 Ljava/lang/String; 9 1 3 s3 Ljava/lang/String; } SourceFile: "ConstantPoolAndStringPool.java"
我们重点看 Code 部分的指令 :
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
ldc :
#2
对应的常量,我们去常量池中找到 #2 -> #25
, #25
对应的是 a
符号ldc #2
会把 a
符号变为 "a"
字符串对象, 然后会去 StringTable => [] (StringTable(字符串池)是HashTable,并且不可扩容) 去找key == "a"
的字符串常量,ldc #2
的时候, 只有 "a" 被创建了"b"
和 "ab"
现在还仅仅是常量池中的符号, 并没有被创建, 字符串的创建是懒惰的astore_1 :
ldc #2
创建完成字符串对象以后, astore_1
将对象存入 LocalVariableTable
中的局部变量 s1
, 可以看到 Slot 1
对应的是 S11
是局部变量表 slot 位置的 valueLocalVariableTable: Start Length Slot Name Signature 0 10 0 args [Ljava/lang/String; 3 7 1 s1 Ljava/lang/String; 6 4 2 s2 Ljava/lang/String; 9 1 3 s3 Ljava/lang/String;
小总结 :
a, b, ab
都只是运行时常量池中的符号, 注意还没成为String对象字符串拼接有两种方式 :
"a" + "b"
.方式一是在编译的时候会把字符串编译成 : "ab"
, 然后放入 StringTable(常量池中)
方式二是创建两个StringBuilder对象, 然后使用append方法拼接, 然后调用toString方法返回一个Stirng对象, 注意 : 这个对象是在堆中保存的, 当调用 intern() 时, 会将当前String对象放入StringTable中, 假设StringTable没有对应字符串对象的情况下.
小总结 :
intern()
方法, 返回值就在常量池中package cn.knightzz.method.string_table; public class StringContact { public static void main(String[] args) { // 常量池 String s1 = "a"; String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; } }
String s4 = s1 + s2;
其实就等价于 new StringBuilder().append("a").append("b").toString()
, 我们可以查看编译后的字节码指令 :
$ javap -v StringContact.class Classfile /F:/JavaCode/hm-jvm-codes/jvm-chapter-01/target/classes/cn/knightzz/method/string_table/StringContact.class Last modified 2022-9-1; size 739 bytes MD5 checksum 4750ee591a31c2348a6fa119ca934e76 Compiled from "StringContact.java" public class cn.knightzz.method.string_table.StringContact minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #10.#29 // java/lang/Object."<init>":()V #2 = String #30 // a #3 = String #31 // b #4 = String #32 // ab #5 = Class #33 // java/lang/StringBuilder #6 = Methodref #5.#29 // java/lang/StringBuilder."<init>":()V #7 = Methodref #5.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = Methodref #5.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String; #9 = Class #36 // cn/knightzz/method/string_table/StringContact #10 = Class #37 // java/lang/Object #11 = Utf8 <init> #12 = Utf8 ()V #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 LocalVariableTable #16 = Utf8 this #17 = Utf8 Lcn/knightzz/method/string_table/StringContact; #18 = Utf8 main #19 = Utf8 ([Ljava/lang/String;)V #20 = Utf8 args #21 = Utf8 [Ljava/lang/String; #22 = Utf8 s1 #23 = Utf8 Ljava/lang/String; #24 = Utf8 s2 #25 = Utf8 s3 #26 = Utf8 s4 #27 = Utf8 SourceFile #28 = Utf8 StringContact.java #29 = NameAndType #11:#12 // "<init>":()V #30 = Utf8 a #31 = Utf8 b #32 = Utf8 ab #33 = Utf8 java/lang/StringBuilder #34 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #35 = NameAndType #40:#41 // toString:()Ljava/lang/String; #36 = Utf8 cn/knightzz/method/string_table/StringContact #37 = Utf8 java/lang/Object #38 = Utf8 append #39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #40 = Utf8 toString #41 = Utf8 ()Ljava/lang/String; { public cn.knightzz.method.string_table.StringContact(); 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 13: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/knightzz/method/string_table/StringContact; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC 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 LineNumberTable: line 18: 0 line 19: 3 line 20: 6 line 22: 9 line 24: 29 LocalVariableTable: Start Length Slot Name Signature 0 30 0 args [Ljava/lang/String; 3 27 1 s1 Ljava/lang/String; 6 24 2 s2 Ljava/lang/String; 9 21 3 s3 Ljava/lang/String; 29 1 4 s4 Ljava/lang/String; } SourceFile: "StringContact.java"
可以看到对应的字节码指令 :
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
String s4 = s1 + s2;
:
先执行 new #5
指令, 创建 StringBuilder对象
dup 复制一个StringBuilder对象的副本
invokespecial #6
调用初始化方法 <init>
aload_1
加载 s1
invokevirtual #7
调用 StringBuilder 对象的append
方法
aload_2
加载 s2
invokevirtual #7
调用 StringBuilder 对象的append
方法 , #7
在常量池中是append方法的引用
invokevirtual #8
调用 StringBuilder 对象的toStirng
方法
package cn.knightzz.method.string_table; /** * @author 王天赐 * @title: StringContact * @projectName hm-jvm-codes * @description: 字符串拼接 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-01 11:12 */ @SuppressWarnings("all") public class StringContactOptimizer { public static void main(String[] args) { // 常量池 String s1 = "a"; String s2 = "b"; String s3 = "ab"; // 等价于 new StringBuilder().append("a").append("b").toString(); String s4 = s1 + s2; // javac 会对其进行优化. 编译器会将 "a" + "b" 编译成 "ab", 这样的话, 在加载的时候会直接从StringTable中去找 String s5 = "a" + "b"; System.out.println("s3 == s5 : " + (s3 == s5)); } }
编译器优化 :
String s5 = "a" + "b"
javac 会对其进行优化. 编译器会将 "a" + "b" 编译成 "ab", 这样的话, 在加载的时候会直接从StringTable中去找package cn.knightzz.method.string_table; /** * @author 王天赐 * @title: StringLazyLoad * @projectName hm-jvm-codes * @description: 字符串延迟加载 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-04 21:25 */ public class StringLazyLoad { public static void main(String[] args) { int x = args.length; System.out.println(x); String a = "a"; String b = "b"; String c = "a"; String d = "b"; } }
初始状态时, StringTable中的字符串个数是 2082 :
并且StringTable中是没有 "a"
和 "b"
字符串的, 新增了两个字符串,
而后面的 "a"
和 "b"
两个字符串因为StringTable中已经有了相应的字符串:
Jdk1.7 及以上版本
直接赋值 :
String s = "tom"; // 只会在字符串常量池中,s指向常量池中的引用
创建对象时,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象, 有则直接返回该对象在常量池中的引用,没有则会在常量池中创建一个新对象,再返回引用。
new String()
String s = new String("tom"); // 字符串常量池和堆中都有这个对象,s指向堆内存中的对象引用
创建对象时,先检查字符串常量池中是否有字符串,有则直接去堆内存中创建一个字符串对象,没有则先在字符串常量池里创建一个字符串对象,再去内存中创建一个字符串对象,最后将内存中的引用返回。
案例如下 :
package cn.knightzz.method.string_table; /** * @author 王天赐 * @title: InternTest * @projectName hm-jvm-codes * @description: * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-07 15:10 */ @SuppressWarnings("all") public class InternTest { public static void main(String[] args) { // InternTest // 通过 new String 方法创建的对象, 常量池和堆中都会有, s1 是堆内存中引用 String s1 = new String("a"); // s1.intern() 返回的是常量池中对象的引用 和 堆中的不是同一个 String s2 = s1.intern(); System.out.println(s1 == s2); // false } }
并且debug时, 初始状态的String对象是2478个, 执行完 String s1 = new String("a");
后是 2480个
String.intern()方法
String s1 = new String("tom"); String s2 = s1.intern(); System.out.println(s1 == s2); //false
是一个 native 的方法,当调用 intern方法时,如果字符串常量池已经包含一个等于此字符串对象的字符串(用equals(oject)方法确定),则返回常量池中的字符串。否则,将intern返回的引用指向当前字符串 s1(jdk1.6版本需要将 s1 复制到字符串常量池里)。
打印JVM GC信息 : -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
打印结果如下 :
i = 100 Heap PSYoungGen total 2560K, used 1958K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000) eden space 2048K, 95% used [0x00000000ffd00000,0x00000000ffee9808,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000) object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000) Metaspace used 3158K, capacity 4496K, committed 4864K, reserved 1056768K class space used 343K, capacity 388K, committed 512K, reserved 1048576K SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 13127 = 315048 bytes, avg 24.000 Number of literals : 13127 = 564288 bytes, avg 42.987 Total footprint : = 1039424 bytes Average bucket size : 0.656 Variance of bucket size : 0.658 Std. dev. of bucket size: 0.811 Maximum bucket size : 6 StringTable statistics: Number of buckets : 60013 = 480104 bytes, avg 8.000 Number of entries : 1843 = 44232 bytes, avg 24.000 Number of literals : 1843 = 161408 bytes, avg 87.579 Total footprint : = 685744 bytes Average bucket size : 0.031 Variance of bucket size : 0.031 Std. dev. of bucket size: 0.175 Maximum bucket size : 2 Process finished with exit code 0
Number of buckets : 桶的数量
Number of entries : key的数量
Number of literals : 键值对的数量 (等于字符串的数量)
策略一 : 通过-XX:StringTableSize=桶个数调整
StringTable的数据结构是HashTable来实现的。即,当我们的空间足够大的时候,我们的数据就会比较分散,查询的效率也会因此降低,反之,当我们的空间比较小的时候,我们的数据就会比较集中,查询的效率也会因此提高。当然了,StringTable 的空间大小并不是越小越好,太小了,一直进行垃圾回收,导致经常要删除老数据,添加新数据等问题也很难受。要是那些个老数据刚删掉没多久就要用到了呢。
小案例 : 以读取linux.words文件数据为例,里面包含了48w+个单词,,我们利用循环读取每一个单词并存入串池中,直到读取结束退出循环。最终输出花费的时间(单位是秒,ms)。
通过调整桶的个数来查看具体花费的时间
package cn.knightzz.method.string_table; import lombok.extern.slf4j.Slf4j; import java.io.*; /** * @author 王天赐 * @title: StringTablePoolTest * @projectName hm-jvm-codes * @description: 演示串池大小对性能的影响 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-08 10:03 */ @Slf4j(topic = "c.StringTablePoolTest") public class StringTablePoolTest { public static void main(String[] args) throws IOException { // 参数 : -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009 String path = StringTablePoolInternTest.class.getClassLoader().getResource("linux.words").getPath(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "utf-8"))) { String line = null; long start = System.nanoTime(); while (true) { line = reader.readLine(); if (line == null) { break; } line.intern(); } log.debug("cost: {} ns" , (System.nanoTime() - start) / 1000000); } } }
JVM参数 : -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
, 通过调整 StringTableSize的大小, 可以提高StringTable查询的效率, 因为StringTable越小, 出现hash碰撞的几率越小
参数 : -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
结果如下 :
10:09:17.002 [main] DEBUG c.StringTablePoolTest - cost: 4726 ns SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 21367 = 512808 bytes, avg 24.000 Number of literals : 21367 = 852264 bytes, avg 39.887 Total footprint : = 1525160 bytes Average bucket size : 1.068 Variance of bucket size : 1.065 Std. dev. of bucket size: 1.032 Maximum bucket size : 7 StringTable statistics: Number of buckets : 1009 = 8072 bytes, avg 8.000 Number of entries : 483657 = 11607768 bytes, avg 24.000 Number of literals : 483657 = 29881192 bytes, avg 61.782 Total footprint : = 41497032 bytes Average bucket size : 479.343 Variance of bucket size : 433.555 Std. dev. of bucket size: 20.822 Maximum bucket size : 547 Process finished with exit code 0
可以看到, 消耗的时间是 4076, 我们通过调整StringTableSize的大小可以提高运行的速度
当我们设置 StringTableSize=2009
10:30:52.863 [main] DEBUG c.StringTablePoolTest - cost: 2692 ns SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 21367 = 512808 bytes, avg 24.000 Number of literals : 21367 = 852264 bytes, avg 39.887 Total footprint : = 1525160 bytes Average bucket size : 1.068 Variance of bucket size : 1.065 Std. dev. of bucket size: 1.032 Maximum bucket size : 7 StringTable statistics: Number of buckets : 2009 = 16072 bytes, avg 8.000 Number of entries : 483657 = 11607768 bytes, avg 24.000 Number of literals : 483657 = 29881192 bytes, avg 61.782 Total footprint : = 41505032 bytes Average bucket size : 240.745 Variance of bucket size : 251.876 Std. dev. of bucket size: 15.871 Maximum bucket size : 302
可以看到上面的结果, 消耗时间由大幅度的减少
策略二 : 考虑是否将字符串对象入池
假如现在让我们存储地球上每个人的家庭住址,我们知道,其实有很多人是住一起的,他们的住址其实没有必要重复存取对吧,而且,我们的家庭住址信息明显就是一个字符串,如果我们让字符串对象入池,那么其实可以省去很多的内存, 我们可以在字符串放入List中之前执行intern方法来减少字符的重复存储.
当我们的系统数据会出现重复字符串的情况下,我们可以让我们对应的字符串对象入池,来减少内存的占用。
package cn.knightzz.method.string_table; import java.io.*; import java.util.ArrayList; import java.util.List; /** * @author 王天赐 * @title: StringTablePoolInternTest * @projectName hm-jvm-codes * @description: 演示 intern 减少内存占用 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-08 10:33 */ public class StringTablePoolInternTest { public static void main(String[] args) throws IOException { // -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics // -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000 String path = StringTablePoolInternTest.class.getClassLoader().getResource("linux.words").getPath(); List<String> address = new ArrayList<>(); System.in.read(); for (int i = 0; i < 10; i++) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "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(); } }
我们可以使用 jvisualvm
查看内存占用情况, win + r 输入 jvisualvm
即可
直接内存是操作系统的内存, 并不是JVM的内存 , 直接内存也叫堆外内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现 : 《深入理解 Java 虚拟机 第三版》2.2.7 小节
直接内存 :
常见于 NIO 操作时,用于数据缓冲区
分配回收成本较高,但读写性能高
不受 JVM 内存回收管理
Direct Memory 并不是虚拟机运行时数据区的一部分;
由于在 JDK 1.4 中引入了 NIO 机制,为此实现了一种通过 native 函数直接分配对外内存的,而这一切是通过以下两个概念实现的:
通过存储在 Java 堆里面的 DirectByteBuffer
对象对这块内存的引用进行操作;
因避免了 Java 堆和 Native
堆(native heap)
中来回复制数据,所以在一些场景中显著提高了性能;
直接内存出现 OutOfMemoryError
异常的原因是物理机器的内存是受限的,但是我们通常会忘记需要为直接内存在物理机中预留相关内存空间;
系统的内存 Java是无法访问到的, Java仅仅只能访问到JVM内存, 但是在 IO 时, 系统会专门划分一片区域作为系统缓存区, 这个区域的内存Java是可以直接访问的
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
直接内存也是会被 JVM
虚拟机管理进行完全不被引用的对象回收处理 , 但是直接内存中的对象并不是如普通对象样被 GC
管理
直接内存的最大大小可以通过 -XX:MaxDirectMemorySize=200M
来设置,默认是 64M。
在 Java 中分配内存的方式一般是通过 sun.misc.Unsafe
类的公共 native 方法实现的(比如 文件以及网络 IO 类,但是非常不建议开发者使用,使用时一定要确保安全),而类 DirectByteBuffer 类的也是借助于此向物理内存(比如 JVM 运行于 Linux 上,那么 Linux 的内存就被称为物理内存)。
Unsafe 是位于
sun.misc
包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。
package cn.knightzz.direct.memory; import lombok.extern.slf4j.Slf4j; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; /** * @author 王天赐 * @title: OutOfDirectMemoryTest * @projectName hm-jvm-codes * @description: 演示直接内存溢出 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-08 14:38 */ @SuppressWarnings("all") @Slf4j(topic = "c.OutOfDirectMemoryTest") public class OutOfDirectMemoryTest { 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); } // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代 // jdk8 对方法区的实现称为元空间 } }
如上面的代码所示, 我们通过修改 ByteBuffer
的内存来使直接内存出现内存泄漏, 因为直接内存的默认大小是 64M, 我们的缓冲去是 100M 就会出现内存泄漏, 结果如下 :
72 Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:694) at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at cn.knightzz.direct.memory.OutOfDirectMemoryTest.main(OutOfDirectMemoryTest.java:29) Process finished with exit code 1
使用了 Unsafe
对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory
方法
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调
用 freeMemory 来释放直接内存
package cn.knightzz.direct.memory; import java.io.IOException; import java.nio.ByteBuffer; /** * @author 王天赐 * @title: MemoryAllocateTest * @projectName hm-jvm-codes * @description: 禁用显式回收对直接内存的影响 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-08 19:22 */ public class MemoryAllocateTest { static int _1Gb = 1024 * 1024 * 1024; public static void main(String[] args) throws IOException { // -XX:+DisableExplicitGC 显式的 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb); System.out.println("分配完毕..."); System.in.read(); System.out.println("开始释放..."); byteBuffer = null; System.gc(); // 显式的垃圾回收,Full GC System.in.read(); } }
可以看到啊, ByteBuffer分配了1G的内存, 任务管理器中 Java子线程出现了1G的内存占用, 当我们继续执行的时候, 开始释放 :
可以看到啊原本占用1G的内存被释放后占用17.5M
显式的禁用GC以后 -XX:+DisableExplicitGC
可以让 System.gc()
无效
package cn.knightzz.direct.memory; import sun.misc.Unsafe; import java.io.IOException; import java.lang.reflect.Field; import java.nio.ByteBuffer; /** * @author 王天赐 * @title: MemoryAllocateTest * @projectName hm-jvm-codes * @description: 禁用显式回收对直接内存的影响 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-09-08 19:22 */ public class MemoryAllocateByUnsafeTest { static int GB_1 = 1024 * 1024 * 1024; public static void main(String[] args) throws IOException { Unsafe unsafe = getUnsafe(); // unsafe 分配内存 long base = unsafe.allocateMemory(GB_1); unsafe.setMemory(base, GB_1, (byte) 0); System.in.read(); // 使用unsafe释放内存 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); } } }
我们查看 DirectByteBuffer 类的 run方法 :
public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); }
也是通过 unsafe 对象来释放和申请内存
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)); att = null; }
DirectByteBuffer 的实现类内部,使用了 Cleaner
(虚引用)来监测 ByteBuffer
对象,一旦ByteBuffer对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
显式的禁用GC以后 -XX:+DisableExplicitGC
可以让 System.gc()
无效
package cn.knightzz.direct.memory; import java.io.IOException; import java.nio.ByteBuffer; public class MemoryAllocateTest { static int _1Gb = 1024 * 1024 * 1024; public static void main(String[] args) throws IOException { // -XX:+DisableExplicitGC 显式的 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb); System.out.println("分配完毕..."); System.in.read(); System.out.println("开始释放..."); byteBuffer = null; System.gc(); // 显式的垃圾回收,Full GC, System.in.read(); } }
当我们显式的禁用 GC 以后, 虽然后面 ByteBuffer对象已经没有别的对象引用了, 但是它依然没有被回收, 依然占用1G内存
显式的调用 System.gc()
是 full GC , 影响效率, 我们可以使用 unsafe.freeMemory
来释放直接内存
本文作者:王天赐
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!