Java Virtual Machine - java程序的运行环境(java二进制字节码的运行环境)
2. JVM的好处- 一次编写,到处运行的基石【重点】
- 自动内存管理,垃圾回收功能【重点】
- 数据下标越界检查
- 多态,面向对象编程
- 面试
- 理解底层的实现原理
- 中高级程序员的必备技能
JVM的内存结构
6. 常见的JVM JVM内存结构JVM类加载过程
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区(元空间)
Program Counter Register 程序计数器(寄存器)
2. 作用- 作用:记住下一条jvm指令的执行地址
- 特点:线程私有的; 不存在内存溢出,也是JVM规范中唯一没有OutOfMemoryError的区域
- 二进制字节码:JVM指令 —> 解释器 —> 机器码 —> CPU
- 程序计数器:记住下一条jvm指令的执行地址,硬件方面通过【寄存器】实现
2. 虚拟机栈 JVM Stacks示例:二进制字节码:JVM指令 java源代码
先了解一下栈的数据结构
- 栈Stack,先进后出FILO
- 栈-线程运行需要的内存空间
- 栈帧-每个方法运行时需要的内存
- 栈-由多个栈帧组成
Java Virtual Machine Stacks (Java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
栈内存不涉及垃圾回收
2. 栈内存分配越大越好?栈内存不是越大越好,如果设置过大,会影响可用线程的数量;比如-Xss1m、-Xss2m,在总内存不变的情况下,可用线程数量会减少
3. 方法内的局部变量是否线程安全?方法内的局部变量是线程安全(不逃离方法的作用域为前提),因为方法内的局部变量各自在自已独立的内存中;如果是static int 就是线程共享的,就不是线程安全;主要看变量是否是线程共享、还是线程私有
注意:
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
java.lang.StackOverflowError
注:可以使用 -Xss调整栈内存的大小,示例:-Xss256k
栈帧过多导致栈内存溢出,比如:递归没有设置结束条件,我们生产环境推荐尽量不使用递归
栈帧过大导致栈内存溢出,比如:方法写得太长,局部变量太多,导致栈帧撑爆了栈内存,这种情况比较少
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu 查看linux所有进程、线程、CPU消耗情况
- ps H -eo pid,tid,%cpu | grep 进程id 用ps命令进一步定位哪个线程引起的CPU占用过高
- jstack 进程pid 查看线程信息,jstack输出的线程id(nid)是16进制的,需要将十进制的线程id转成16进制; 可以根据16进制的线程id找到有问题的线程,进一步定位问题代码的源码行号
通过上述方式找到了源代码CPU消耗过高的文件及行号
nohup java -cp /root/JvmLearn-1.0-SNAPSHOT.jar com.jvm.stack.T07_StackDeadLock &
运行一个java程序,很长时间没有结果,使用jstack 线程id查看是否发生了死锁
本地方法栈不是Java编写的代码,是C/C++编写的,通过C/C++调用底层
java调用c或c++的代码来调用底层
- 通过new 关键字,创建对象都会使用堆内存
- 特点
- 它是线程共享的,堆中对象需要考虑线程安全的问题
- 有垃圾回收机制
java.lang.OutOfMemoryError
代码参考:cn.itcast.jvm.t1.heap.Demo1_5
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);
}
}
}
生产建议
生产环境建议:如果内存比较大,内存溢出不会那么快的暴露;这时,我们可以将堆内存调小,让内存溢出尽早暴露
-Xmx 设置堆内存空间的最大值 示例:-Xmx8m
-Xms 设置对内存空间的初始值 示例:-Xms8m
- jps工具:查看当前系统中有哪些java进程
- jmap工具:查看堆内存占用情况 jmap -heap pid(进程id)(只能查询某一个时刻,堆内存的占用情况)
- jstack 工具:线程监控
- jconsole工具:图形界面的,多功能的检测工具,可以连续监测
- jvisualvm工具:图形界面的,多功能的检测工具,可以连续监测;还有dump
代码参考:cn.itcast.jvm.t1.heap.Demo1_4
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);
}
}
- 运行程序,jps找到运行的该程序进程id
E:BaiduNetDiskDownload资料--jvm黑马代码jvm>jps 63904 RemoteMavenServer36 1348 48964 HbteSaasQmsApplication 51844 HbteSaasPurchaseApplication 51112 HbteSaasGatewayApplication 62728 Launcher 40812 Launcher 53228 HbteSaasProduceApplication 47504 Jps 54096 HbteSaasStoreApplication 57104 SharpApplication 48308 HbteSaasPlatformApplication 48628 HbteSaasAuthApplication 49204 Launcher 65172 Demo1_4 62908 Launcher
- 分别在打印1、2、3的时刻使用 jmap -heap pid(进程id)查看堆内存的变化
E:BaiduNetDiskDownload资料--jvm黑马代码jvm>jmap -heap 65172 Attaching to process ID 65172, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.181-b13 using thread-local object allocation. Parallel GC with 6 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 4246732800 (4050.0MB) NewSize = 88604672 (84.5MB) MaxNewSize = 1415577600 (1350.0MB) 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 = 5327016 (5.080238342285156MB) free = 61257560 (58.419761657714844MB) 8.000375342181348% 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 1736 interned Strings occupying 177872 bytes. E:BaiduNetDiskDownload资料--jvm黑马代码jvm>jmap -heap 65172 Attaching to process ID 65172, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.181-b13 using thread-local object allocation. Parallel GC with 6 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 4246732800 (4050.0MB) NewSize = 88604672 (84.5MB) MaxNewSize = 1415577600 (1350.0MB) 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 = 15812792 (15.080253601074219MB) free = 50771784 (48.41974639892578MB) 23.748430867833417% 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 1737 interned Strings occupying 177920 bytes. E:BaiduNetDiskDownload资料--jvm黑马代码jvm>jmap -heap 65172 Attaching to process ID 65172, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.181-b13 using thread-local object allocation. Parallel GC with 6 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 4246732800 (4050.0MB) NewSize = 88604672 (84.5MB) MaxNewSize = 1415577600 (1350.0MB) 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 = 2663424 (2.5400390625MB) free = 63921152 (60.9599609375MB) 4.0000615157480315% 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 = 766440 (0.7309341430664062MB) free = 176967192 (168.7690658569336MB) 0.43122958293003316% used 1723 interned Strings occupying 176928 bytes.
- 程序运行后,在控制台上使用jconsole,连接被监控的进程(jsonsole之间监控进程)
代码参考:cn.itcast.jvm.t1.heap.Demo1_13
public class Demo1_13 {
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工具查看,占用最多的内存的对象是哪些。
通过堆转储,下载dump文件并用jvisualvm分析dump文件,查看大对象,查找问题,排查原始代码
参考:
Linux下获取java应用的Dump文件
linux环境下导出项目的堆栈dump文件
- 线程共享
- 在JVM启动时创建,在逻辑上属于堆的一部分(看厂商实现)
- 方法区也有可能内存溢出
1.8 以前会导致永久代内存溢出
- 演示永久代内存溢出: java.lang.OutOfMemoryError: PerGen space
- -XX:MaxPerSize=8m
1.8 之后会导致元空间内存溢出(系统内存)
- 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
- -XX:MaxMetaspaceSize=8m
自己将元空间的大小设置得很小,动态加载的类又很多,导致元空间OOM
Jdk1.8 参考代码:cn.itcast.jvm.t1.metaspace.Demo1_8
自己在电脑上测试,要调到20M左右才会出现元空间溢出的问题,并且循环次数要调大
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 < 10000000; 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);
}
}
}
如果设置元空间的大小为8m会提示空间太小
如果设置为12m,会报OOM,提示压缩类空间
虽然我们自己编写的程序没有大量使用动态加载类,但如果我们在使用外部一些框架时,可能大量动态加载类,就可能会导致元空间内存溢出。
场景(动态加载类),如果框架使用不合理也会导致方法区内存溢出
- spring
- mybatis
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址(符号引用变为直接引用)
简单来说,类编译后,常量池是在class字节码文件中的,虚拟机指令通过符号地址找自己要使用到的常量信息(类名,方法名,参数类型,字面量),当class文件被jvm虚拟机加载后,就会将创建class类模板对象,并为其分配内存,这时calss文件里面虚拟机的指令要找常量池中的常量信息就需要在内存里面去找了,符号地址也就转换成真实的内存地址,直接通过内存地址去找加载到内存中的常量池信息
代码示例:cn.itcast.jvm.t5.HelloWorld
// 二进制字节码(类基本信息,常量池,类方法定义(包含了虚拟机指令))
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
将上述编译好的class文件进行反汇编:Javap -v HelloWord.class 反编译结果如下:
E:学习文档jvm>cd E:学习文档jvmoutproductionjvmcnitcastjvmt5 E:学习文档jvmoutproductionjvmcnitcastjvmt5>javap -v HelloWorld.class Classfile /E:/学习文档/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class Last modified 2022-7-6; 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."5. StringTable(串池-字符串常量池)":()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 #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 // " ":()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." ":()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"
StringTable面试题
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");
String x1 = "cd";
x2.intern();
//问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1==x2);
1. StringTable常量池与串池的关系
代码参考:cn.itcast.jvm.t1.stringtable.Demo1_22
// 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 == s4);//打印false
System.out.println(s3 == s5);//打印true
}
}
javap -v Demo1_22.class 反编译如下:
E:学习文档jvmoutproductionjvmcnitcastjvmt1stringtable>javap -v Demo1_22.class Classfile /E:/学习文档/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class Last modified 2022-7-6; size 1039 bytes MD5 checksum 8815b8f391f7f5387dc065e4053bf5d1 Compiled from "Demo1_22.java" public class cn.itcast.jvm.t1.stringtable.Demo1_22 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #12.#36 // java/lang/Object."":()V #2 = String #37 // a #3 = String #38 // b #4 = String #39 // ab #5 = Class #40 // java/lang/StringBuilder #6 = Methodref #5.#36 // java/lang/StringBuilder." ":()V #7 = Methodref #5.#41 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = Methodref #5.#42 // java/lang/StringBuilder.toString:()Ljava/lang/String; #9 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream; #10 = Methodref #45.#46 // java/io/PrintStream.println:(Z)V #11 = Class #47 // cn/itcast/jvm/t1/stringtable/Demo1_22 #12 = Class #48 // java/lang/Object #13 = Utf8 #14 = Utf8 ()V #15 = Utf8 Code #16 = Utf8 LineNumberTable #17 = Utf8 LocalVariableTable #18 = Utf8 this #19 = Utf8 Lcn/itcast/jvm/t1/stringtable/Demo1_22; #20 = Utf8 main #21 = Utf8 ([Ljava/lang/String;)V #22 = Utf8 args #23 = Utf8 [Ljava/lang/String; #24 = Utf8 s1 #25 = Utf8 Ljava/lang/String; #26 = Utf8 s2 #27 = Utf8 s3 #28 = Utf8 s4 #29 = Utf8 s5 #30 = Utf8 StackMapTable #31 = Class #23 // "[Ljava/lang/String;" #32 = Class #49 // java/lang/String #33 = Class #50 // java/io/PrintStream #34 = Utf8 SourceFile #35 = Utf8 Demo1_22.java #36 = NameAndType #13:#14 // " ":()V #37 = Utf8 a #38 = Utf8 b #39 = Utf8 ab #40 = Utf8 java/lang/StringBuilder #41 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #42 = NameAndType #53:#54 // toString:()Ljava/lang/String; #43 = Class #55 // java/lang/System #44 = NameAndType #56:#57 // out:Ljava/io/PrintStream; #45 = Class #50 // java/io/PrintStream #46 = NameAndType #58:#59 // println:(Z)V #47 = Utf8 cn/itcast/jvm/t1/stringtable/Demo1_22 #48 = Utf8 java/lang/Object #49 = Utf8 java/lang/String #50 = Utf8 java/io/PrintStream #51 = Utf8 append #52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #53 = Utf8 toString #54 = Utf8 ()Ljava/lang/String; #55 = Utf8 java/lang/System #56 = Utf8 out #57 = Utf8 Ljava/io/PrintStream; #58 = Utf8 println #59 = Utf8 (Z)V { public cn.itcast.jvm.t1.stringtable.Demo1_22(); 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 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/t1/stringtable/Demo1_22; 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." ":()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 4 39: if_acmpne 46 42: iconst_1 43: goto 47 46: iconst_0 47: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 50: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 53: aload_3 54: aload 5 56: if_acmpne 63 59: iconst_1 60: goto 64 63: iconst_0 64: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 67: return LineNumberTable: line 11: 0 line 12: 3 line 13: 6 line 14: 9 line 15: 29 line 17: 33 line 18: 50 line 19: 67 LocalVariableTable: Start Length Slot Name Signature 0 68 0 args [Ljava/lang/String; 3 65 1 s1 Ljava/lang/String; 6 62 2 s2 Ljava/lang/String; 9 59 3 s3 Ljava/lang/String; 29 39 4 s4 Ljava/lang/String; 33 35 5 s5 Ljava/lang/String; StackMapTable: number_of_entries = 4 frame_type = 255 offset_delta = 46 locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ] frame_type = 79 stack = [ class java/io/PrintStream ] frame_type = 255 offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ] } SourceFile: "Demo1_22.java"
执行到对应的虚拟机指令,需要加载class文件常量池中对应的字符串到内存中的运行时常量池中时,会到StringTable中去找这个需要加载的字符串在StringTable中有没有,如果没有就放进去,有就不再放入,使用已经存在的字符串常量
演示字符串字面量也是延迟成为对象
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println(); // 字符串个数 2238
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");
System.out.print("1"); // 字符串个数 2248
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");
System.out.print(x); // 字符串个数 2248
}
}
调试到此处 字符串个数是2238
运行到此处,加载了10个字符串到串池StringTable,字符串个数2248
6. StringTable特性运行到这里,串池StringTable中已经加载了刚才的10个字符串,所以没有继续加载了,字符串个数仍然是2248,也证明了字符串是延迟加载到串池中的
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (JDK1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放放串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回
StringTable_intern_1.8
代码参考:cn.itcast.jvm.t1.stringtable.Demo1_23
public class Demo1_23 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == x);
System.out.println( s == x );
}
}
7. StringTable位置
- JDK1.6版本,字符串常量池是在永久代中;
- JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
- JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。
1. JDK1.8 字符串常量池在堆中实例验证JDK1.6 与 JDK1.8字符串常量池对比
代码参考:cn.itcast.jvm.t1.stringtable.Demo1_6
public class Demo1_6 {
public static void main(String[] args) throws InterruptedException {
List list = new ArrayList();
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);
}
}
}
设置JVM的启动参数
-Xmx10m 设置堆内存的最大大小为10m
-XX:-UseGCOverheadLimit 不限制GC的运行时间
参考:
JVM -XX: 参数介绍
JVM优化之 -Xss -Xms -Xmx -Xmn 参数设置
只设置堆内存的大小为10m时,查看运行结果
只设置10M的堆内存空间,出现了oom,但是提示信息是超出了GC的开销限制
JVM官方文档对这个参数做了说明,-XX:+UseGCOverheadLimit 开启GC的开销限制,如果98%的时间回收的内存还不到内存占比的2%时,就会任务JVM以及无可救药了,提示超出了GC的开销限制
为了能看到堆溢出的提示,我们关闭GC的开销限制
关闭了GC的开销限制后,可以看到堆内存移溢出了
因为在jdk1.8中,字符串常量池是放在堆中,如果堆空间不足,字符串常量池也会进行垃圾回收
代码参考:cn.itcast.jvm.t1.stringtable.Demo1_7
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);
}
}
}
JVM参数设置
先循环100次,让100个字符串入池
1875个字符串出了程序遍历入池的100个,其他的是程序运行时用到的比如类名,方法名等字符串常量也存在于字符串常量池中,由于遍历的次数较少,入池的字符串不多,没有触发垃圾回收
现在遍历10000次,让10000个字符串入池
让10000个字符串入池后,10M的堆内存不够了,触发了垃圾回收,字符串常量池中没有用到的字符串就被垃圾回收了,所以在字符串常量池中只有8540个字符串常量了
代码参考:cn.itcast.jvm.t1.stringtable.Demo1_24
- 调整 -XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
public class Demo1_24 {
public static void main(String[] args) throws IOException {
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;
}
line.intern();
}
System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
}
}
}
将StringTable桶调小些,示例操作如下:
将StringTable桶调大些,示例操作如下:
结论:放入同样多的字符串,将StringTable的桶调大些,耗时较少
public class Demo1_25 {
public static void main(String[] args) throws IOException {
List 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();
}
}
说明一下:jvm/linux.words 大约单词量在479829个,上述代码运行结果截图,如下:
| 读取大约48万单词 | 堆内存占用大小 | 耗时 |
|---|---|---|
| 未放入字符串池 | 约300兆 | 较短 |
| 放入字符串池 | 约100兆 | 较长 |
运行结果1:未放入字符串常量池中,运行情况截图
address.add(line);
D:IDEAWorkSpacejvm_studyjvmlinux.words
运行结果2:放入字符串常量池中,运行情况截图
address.add(line.intern());
结论:字符串入池节约了堆内存的使用
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理,属于系统内存
- 普通内存
- 需要从用户态向内核态申请资源,即用户态会创建一个java 缓冲区byte[],内核态会创建系统缓冲区。
- 直接内存
- 需要从用户态向内核态申请资源,即内核态会创建一块直接内存direct memory,这块direct memory内存可以在用户态、内核态使用。
通常使用内存(未使用直接内存) VS 直接内存,原理对比图
分析:
- 普通内存:因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。
- 直接内存:直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。
接下来,我们将对一个大约1.29G大小的视频文件进行读取并写入指定文件中,即复制。代码如下:
代码参考:cn.itcast.jvm.t1.direct.Demo1_9
public class Demo1_9 {
static final String FROM = "E:BaiduNetDiskDownloadSpring视频-马士兵助理SpringBoot自动装配.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);
}
}
运行耗时对比表如下:
| 序 号 | 传统方式 IO | 直接内存directBuffer | 说明 |
|---|---|---|---|
| 测试1 | 18871.591 ms | 6335.745 ms | 没有缓存 |
| 测试2 | 5710.124 ms | 5497.707 ms | 有缓存 |
| 测试3 | 7355.304 ms | 5103.806 ms | 有缓存 |
代码参考:cn.itcast.jvm.t1.direct.Demo1_10
public class Demo1_10 {
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 对方法区的实现称为元空间
}
}
直接内存溢出
ByteBuffer一直分配直接内存,每次100M,36次后,直接内存溢出了
代码参考:cn.itcast.jvm.t1.direct.Demo1_26
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
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();
}
}
通过任务管理器查看分配了1G内存的java进程
6. 分配和回收原理及案例演示通过System.gc()显示回收了直接内存
代码参考:cn.itcast.jvm.t1.direct.Demo1_27
public class Demo1_27 {
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);
}
}
}
运行程序,由unsafe类分配直接内存和释放直接内存
unsafe类分配1G直接内存
unsafe类释放直接内存
直接内存的回收不是通过 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() {
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);
}
2. 直接内存的回收机制总结
- 使用了UnSafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
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 的方式释放内存。



