一、前言
1.1、什么是 JVM ?
1)定义
Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。
2)好处
- 一次编译,处处执行
- 自动的内存管理,垃圾回收机制
- 数组下标越界检查
3)比较
JVM、JRE、JDK 的关系如下图所示
1.2、学习 JVM 有什么用?
面试必备
中高级程序员必备
想走的长远,就需要懂原理,比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事等待,JVM 是必须掌握的。
1.3、常见的 JVM
一套规范,可以自己实现jmv的
我们主要学习的是 HotSpot 版本的虚拟机。
HotSpot VM是Sun JDK和OpenJDK中所带的虚拟机。
1.4、学习路线
ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
Method Area:类是放在方法区中。
Heap:类的实例对象。
当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。
方法执行时的每行代码是有执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口(调用操作系统方法)。
二、内存结构
2.1、程序计数器
1)定义
Program Counter Register 程序计数器(寄存器)
作用:是记录下一条 jvm 指令的执行地址行号。
特点:
- 是线程私有的
- 不会存在内存溢出
2)作用
计数器是java对物理硬件(寄存器)的屏蔽和抽象
解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。
2.2、虚拟机栈
1)定义
每个线程运行需要的内存空间,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
栈顶的那个栈帧,调用一次方法,把方法的栈帧放入栈,方法执行完,弹出栈帧;方法调用方法,在放入另一个栈帧。
问题辨析:
垃圾回收是否涉及栈内存?
不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。
栈内存分配越大越好吗?
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。因为一个线程对应一个栈,即栈是线程私有的,所以栈大,那么栈数目少,线程数就少。
方法的局部变量是否线程安全?
如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题(函数参数、 函数返回值等的情况)。
public class Demo1_17 { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); sb.append(4); sb.append(5); sb.append(6); new Thread(()->{ m2(sb); }).start(); } public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; } }
m1线程安全,私有的引用局部变量
m2线程不安全,sb是方法参数传递的,说明与其他线程共享
m3线程不安全,作为返回值,也共享了
2)栈内存溢出
栈帧过大(局部变量一般占用内存比较少,不太容易出现)、
过多(方法调用太多,且没有返回,递归没有终止)、
或者第三方类库操作(两个类的循环引用,json循环依赖),
都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!
3)线程运行诊断
案例一:cpu 占用过多
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程
top 命令,查看是哪个进程占用 CPU 过高
ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。
2.3、本地方法栈
一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。
Object中有很多本地方法,clone/wait/..
2.4、堆
1)定义
Heap 堆
通过new关键字创建的对象都会被放在堆内存
特点
- 它是线程共享,堆内存中的对象都需要考虑线程安全问题
- 有垃圾回收机制,堆中不再引用的对象会被释放内存
2)堆内存溢出
java.lang.OutofMemoryError :java heap space. 堆内存溢出,堆中的对象太多,也没被回收
可以使用 -Xmx8m 来指定堆内存大小(大小指定8M)。
先是将hello对象创建堆,将对象引用加入list集合
然后不断做字符串拼接,将hello*****对象创建堆,将对象引用加入list集合
。。。。。死循环,对象也无法回收,爆了。
3)堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程
- jmap 工具
查看堆内存占用情况 jmap - heap 进程id
- jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
- jvisualvm 工具
先运行演示堆内存的程序
/** * 演示堆内存 */ public class Demo1_4 { 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); } }
idea Terminal中运行jps
查看当前系统中有哪些 java 进程
I:\网课资料\资料-解密JVM\代码\jvm>jps 22080 Jps 21556 23380 Demo1_4 5812 RemoteMavenServer36 8460 Launcher
查看堆内存占用情况
内存快照信息
I:\网课资料\资料-解密JVM\代码\jvm>jmap -heap 23380 Attaching to process ID 23380, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.231-b11 using thread-local object allocation. Parallel GC with 8 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 4261412864 (4064.0MB) NewSize = 88604672 (84.5MB) MaxNewSize = 1420296192 (1354.5MB) OldSize = 177733632 (169.5MB) 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 = 66584576 (63.5MB) used = 17145656 (16.35137176513672MB) free = 49438920 (47.14862823486328MB) 25.750191756120817% used From Space: capacity = 11010048 (10.5MB) used = 0 (0.0MB) free = 11010048 (10.5MB) 0.0% used To Space: capacity = 11010048 (10.5MB) used = 0 (0.0MB) free = 11010048 (10.5MB) 0.0% used PS Old Generation capacity = 177733632 (169.5MB) used = 0 (0.0MB) free = 177733632 (169.5MB) 0.0% used 3170 interned Strings occupying 280952 bytes.
jconsole 工具
idea Terminal中运行jconsole
可以看到堆内存空间先增后降 符合代码
2.5、方法区
2.5.1 定义
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。
方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。
它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和接口的实例初始化。
方法区域是在虚拟机启动时创建的。
尽管方法区在逻辑上是堆的一部分(不同厂商实现不一样,HotSpots 1.8前是永久代,堆的一部分,1.8时把永久代移除了,元空间,本地系统内存),但简单的实现可能不会选择垃圾收集或压缩它。
方法区是规范,什么永久代、元空间是实现。
此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。
方法区域的内存不需要是连续的!
2.5.2 组成
Hotspot 虚拟机 jdk1.6 1.7 1.8 内存结构图
ClassLoader用来加载类的字节码。
2.5.3 方法区内存溢出
1.8 之前会导致永久代内存溢出
使用 -XX:MaxPermSize=8m 指定永久代内存大小
1.8 之后会导致元空间内存溢出
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
演示内存溢出
import jdk.internal.org.objectweb.asm.ClassWriter; import jdk.internal.org.objectweb.asm.Opcodes; /** * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace * -XX:MaxMetaspaceSize=8m */ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 public static void main(String[] args) { int j = 0; try { Demo1_8 test = new Demo1_8(); 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 对象 } } finally { System.out.println(j); } } }
2.5.4 运行时常量池
运行一段程序,将程序编译为二进制字节码:
二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)
首先看看常量池是什么,编译如下代码:
public class HelloWorld { public HelloWorld() { } public static void main(String[] args) { System.out.println("hello world"); } }
然后使用 javap -v Test.class 命令反编译查看结果。
Classfile /I:/网课资料/资料-解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class Last modified 2021-9-24; size 567 bytes MD5 checksum 8efebdac91aa496515fa1c161184e354 Compiled from "HelloWorld.java" public class cn.itcast.jvm.t5.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/itcast/jvm/t5/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/itcast/jvm/t5/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/itcast/jvm/t5/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.itcast.jvm.t5.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 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/t5/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 6: 0 line 7: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; } SourceFile: "HelloWorld.java"
其中Constant pool那部分是常量池表
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/itcast/jvm/t5/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/itcast/jvm/t5/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/itcast/jvm/t5/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
每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。
Code后是jvm指令,指令地址 操作方式 常量池对应地址
常量池:
就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
运行时常量池:
常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池(内存中),并把里面的符号地址变为真实地址(内存地址)
2.5.5 StringTable
String table又称为String pool,字符串常量池,其存在于堆中(jdk1.7之后改的)。最重要的一点,String table中存储的并不是String类型的对象,存储的而是指向String对象的索引,真实对象还是存储在堆中。
此外String table还存在一个hash表的特性,里面不存在相同的两个字符串。
此外String对象调用intern()方法时,会先在String table中查找是否存在于该对象相同的字符串,若存在直接返回String table中字符串的引用,若不存在则在String table中创建一个与该对象相同的字符串。
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容 public class Demo1_22 { // 常量池中的信息,都会被加载到运行时常量池中, 这时 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 == s5); } }
反编译
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, 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: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_3 37: aload 5 39: if_acmpne 46 42: iconst_1 43: goto 47 46: iconst_0 47: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 50: return
常量池中的字符串仅是符号,只有在被用到时才会将符号转化为对象(懒汉),放入StringTable,放入时会先在StringTable中查找,如果对象存在就无法放入,不存在放入,最后返回串池中对象。
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder(线程安全,效率低)
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
s5==s3 true
字符串常量拼接的原理是编译器优化,s5是常量,去常量池中查找,还特么找到了, 常量是确定,可以在编译期间确定为ab,而引用相加不确定,只能运行时确定
可以使用方法,主动将串池中还没有的字符串对象放入串池中
(懒汉行为,延迟实例化,遇到一个常量,用时将常量池符号变对象,再放入StringTable)
intern方法 1.8
调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池StringPooling中
- 如果串池中没有该字符串对象,则放入成功
- 如果有该字符串对象,则放入失败
- 无论放入是否成功,都会返回串池中的字符串对象
注意:此时如果调用 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" 是同一个对象,所以以下两条语句打印的都为 true System.out.println(str == st2);//true System.out.println(str == str3);//true } }
例2:
public class Demo1_23 { // ["ab", "a", "b"] public static void main(String[] args) { String x = "ab"; //此处创建字符串对象 "ab" ,因为串池中还没有 "ab" ,所以将其放入串池中 String s = new String("a") + new String("b"); // "a" "b" 被放入串池中,s则存在于堆内存之中 String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回 // 此时因为在创建x时,"ab" 已存在与串池中,所以放入失败,但是会返回串池中的 "ab" System.out.println( s2 == x); // true System.out.println( s == x ); // false } }
当java1.6时 当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象拷贝添加到字符串常量池中,并且返回该字符串对象的引用。
1.8不拷贝,1.6要拷贝,当常量池无对象时,1.8返回的引用和堆引用一样,因为放入的是引用不是拷贝,而1.6则是常量池引用,放入的是拷贝
面试题
/** * 演示字符串相关面试题 */ public class Demo1_21 { public static void main(String[] args) { String s1 = "a"; String s2 = "b"; String s3 = "a" + "b"; // ab String s4 = s1 + s2; // new String("ab") String s5 = "ab"; String s6 = s4.intern(); // 问 System.out.println(s3 == s4); // false System.out.println(s3 == s5); // true System.out.println(s3 == s6); // true String x2 = new String("c") + new String("d"); // new String("cd") x2.intern(); String x1 = "cd"; // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢 System.out.println(x1 == x2); } }
1.8 x2==x1 false
1.6 x2==x1 false
串池中已经存在“cd”了,x2不会再放入串池 x2的“cd”存在于堆中
1.8 x2==x1 true x2一开始堆,然后将其应用放入StringPooling,x1放入后得到返回引用和x2引用一样
1.6 x2==x1 false x2一开始堆,然后拷贝对象放入StringPooling,x1放入后得到返回引用和x2(此时的x2还是之前的 没有更新)不一样
2.5.6 StringTable 的位置
jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。
因为永久代的回收效率很低,永久代只有fullGC的时候才会垃圾回收
堆中只需要minGC就可以垃圾回收,大大减少String常量对内存的占用
/** * 演示 StringTable 位置 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit * 在jdk6下设置 -XX:MaxPermSize=10m */ public class Demo1_6 { public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<String>(); int i = 0; try { for (int j = 0; j < 260000; j++) { list.add(String.valueOf(j).intern()); i++; } } catch (Throwable e) { e.printStackTrace(); } finally { System.out.println(i); } } }
实验 对比1.6和1.8StringPool位置
设置永久代参数,内存大小
花了98%的时间进行垃圾回收,但是垃圾回收不足2%,说明救不活了!哈哈哈 ,直接报堆溢出
2.5.7 StringTable 垃圾回收
-Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息
/** * 演示 StringTable 垃圾回收 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc */ public class Demo1_7 { 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); } } }
堆空间
内存不足,触发一次垃圾回收,垃圾回收速度很快,
新生代的垃圾回收快
2.5.8 StringTable 性能调优
* 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶(对象数组长度)的个数,减少hash碰撞的可能性,链的长度较短,来减少字符串放入串池所需要的时间,哈希桶的长度太小的话,如果String常量对象很多,哈希碰撞更严重,链表插入、扩容、红黑树费时
* 考虑是否需要将字符串对象入池
* 可以通过 intern 方法减少重复入池,不同对象(相同)指向池中同一String
设置桶的长度:
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
2.6、直接内存
2.6.1 定义
Direct Memory -----是操作系统的内存 ---java和系统都可以访问,避免了内存重复
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
2.6.2 使用直接内存的好处
文件读写流程:
java本身不具备磁盘读写的能力,需要调用操作系统的方法,本地方法--CPU状态由用户态(java)切换到内核态(System);
缓存,分次读取
因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。
使用了 DirectBuffer 文件读取流程
直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。磁盘文件读取到直接内存后,可以让java直接访问,少了缓冲区的copy操作,所以高效,内存不浪费。
2.6.3 直接内存回收原理
1.直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
2.ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过通过Clean方法调用unsafe.freeMemory 是(守护线程)来释放内存
直接内存的分配:ByteBuffer.allocateDirect();
/** * 禁用显式回收对直接内存的影响 */ public class Demo1_26 { 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(); } }
这里的直接内存被释放,不是因为GC,因为JVM管不了
但是,有虚引用
public class Code_06_DirectMemoryTest { public static int _1GB = 1024 * 1024 * 1024; public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException { // method(); method1(); } // 演示 直接内存 是被 unsafe 创建与回收 private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException { Field field = Unsafe.class.getDeclaredField("theUnsafe");//用反射拿到unsafe对象 field.setAccessible(true); Unsafe unsafe = (Unsafe)field.get(Unsafe.class); //分配内存,,用unsafe分配的内存,由unsafe对象方法释放掉 long base = unsafe.allocateMemory(_1GB); unsafe.setMemory(base,_1GB, (byte)0); System.in.read(); //释放内存 unsafe.freeMemory(base); System.in.read(); } // 演示 直接内存被 释放 private static void method() throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB); System.out.println("分配完毕"); System.in.read(); System.out.println("开始释放"); byteBuffer = null; System.gc(); // 手动 gc System.in.read(); } }
直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
第一步:allocateDirect 的实现
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
底层是创建了一个 DirectByteBuffer 对象。
第二步:DirectByteBuffer 类
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为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。 att = null; }
这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。
public void clean() { if (remove(this)) { try { // 都用函数的 run 方法, 释放内存 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; } }); } } }
可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法,
public void run() { if (address == 0) { // Paranoia return; } // 释放内存 unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); }
注意:
/** * -XX:+DisableExplicitGC 显示的 */ private static void method() throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB); System.out.println("分配完毕"); System.in.read(); System.out.println("开始释放"); byteBuffer = null; System.gc(); // 手动 gc 失效 System.in.read(); }
一般用 jvm 调优时,会加上下面的参数:
-XX:+DisableExplicitGC // 静止显示的 GC
意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。