黑马程序员JVM完整教程
- 前言
- 引言
- 1. 内存结构
- 2. 垃圾回收
- 3. 类加载与字节码技术
- 4. 内存模型
前言
黑马程序员JVM完整教程
b站链接:https://www.bilibili.com/video/BV1yE411Z7AP?spm_id_from=333.337.search-card.all.click&vd_source=a1565026d150073a042e4cd0cc263851
配套资料:https://pan.baidu.com/s/167mob33EfeD-on6dWrEWGQ 提取码:bqob
引言
-
什么是JVM?
Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)。 -
好处:
一次编写,到处运行;
自动内存管理,垃圾回收功能;
数组下标越界检查;
多态。 -
比较:JVM JRE JDK
JVM:java虚拟机, 可以屏蔽java代码与底层虚拟机之间的关系
JRE:java的运行时环境,在JVM的基础上结合一些基础类库
JDK:java开发工具包,在JRE的基础上增加编译工具 -
常见的JVM:
-
JVM的整体架构:
1. 内存结构
1. 1 程序计数器
- Program Counter Register 程序计数器(寄存器)
- 作用:记住下一条jvm指令的执行地址
- 特点:
- 是线程私有的
- CPU会为每个线程分配时间片,当前线程的时间片使用完以后,CPU就会去执行另一个线程中的代码
- 程序计数器是每个线程所私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
- 不会存在内存溢出
- 是线程私有的
1.2 虚拟机栈
1.2.1 定义与作用
- Java Virtual Machine Stacks(Java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 问题辨析:
- 垃圾回收是否涉及栈内存?
不需要。栈内存是一次次方法调用所产生的栈帧内存,栈帧内存在每一次方法调用后都会弹出栈,也就是说会被自动的回收掉,因此不需要垃圾回收来进行管理。 - 栈内存分配越大越好吗?
不是。物理内存大小一定,栈内存分配的越大,所能划分的线程越少。 栈内存分配的大仅仅只能使得能够进行更多次的递归调用。 - 方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。如静态变量、方法参数、方法返回值等。
- 垃圾回收是否涉及栈内存?
1.2.2 栈内存溢出
- 栈内存溢出:
java.lang.StackOverflowError
-
栈帧过多导致栈内存溢出:如没有终止条件的方法递归调用等
-
栈帧过大导致栈内存溢出
-
可以通过参数-Xss来控制程序栈的大小
-
1.2.3 线程运行诊断
- 线程诊断:
- CPU占用高
- 用
top
定位哪个进程对cpu的占用过高 ps H -eo pid,tid,%cpu | grep 进程id
:用ps命令进一步定位是哪个线程引起的cpu占用过高(可以得到十进制的线程id)- jstack 进程id
可以根据线程id(此时的线程id为十六进制,在匹配时需要将上一步得到的十进制的换算为十六进制) 找到有问题的线程,进一步定位到问题代码的源码行号
- 用
- 迟迟得不到运行结果
使用jstack 进程id,在最后会有一些提示信息
- CPU占用高
1.3 本地方法栈
- 在java虚拟机调用一些本地方法时需要给本地方法提供的内存空间
- 本地方法:由于java有限制,不可以直接与操作系统底层交互,所以需要一些用c/c++编写的本地方法与操作系统底层的API交互,java可以间接的通过本地方法来调用底层功能
- 举例:Object的clone()、hashCode()、notify()、notifyAll()、wait()等
1.4 堆
1.4.1 定义
- Heap堆:通过new关键字创建对象都会使用堆内存
- 特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
1.4.2 堆内存溢出
- 堆内存溢出:
java.lang.OutofMemoryError :java heap space.
- 可以通过参数-Xmx来控制程序堆的大小
1.4.3 堆内存诊断
-
jps工具:查看当前系统中有哪些java进程
-
jmap工具:查看某一时刻堆内存占用情况
-
jconsole工具:图形界面的,多功能的监测工具,可以连续监测
-
jvisualvm工具
可以查看堆内存的具体信息:占用200多M内存
查看占用内存最大的对象:ArrayList
ArrayList中的元素Student对象中包含的big属性占用1M内存,共有200个元素,占用200M内存
对应源码:
1.5 方法区
1.5.1 定义
- 所有java虚拟机线程的共享区域
- 存储类的结构的相关信息,如运行时常量池、成员变量、方法数据、成员方法和构造器的代码等
- 方法区在虚拟机启动时创建,其逻辑上是堆的一个组成部分,但在实现时不同的JVM厂商可能会有不同的实现
1.5.2 组成
- 组成:以Oracle的HotSpot为例
- jdk1.6:永久代,占用JVM内存空间
- jdk1.8:元空间,移出JVM内存(除StringTable),放入系统内存
- jdk1.6:永久代,占用JVM内存空间
1.5.3 方法区内存溢出
- 1.8以前会导致永久代内存溢出**
java.lang.OutOfMemoryError: PernGen space
**
- 1.8之后会导致元空间内存溢出:
java.lang.OutOfMemoryError: Metaspace
1.5.4 运行时常量池
- 常量池:常量池,就是一张表,存储在*.class字节码文件中。虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
反编译字节码获取
-
方法一:
编译:javac -encoding utf-8 Demo1_22.java
,生成字节码文件
反编译字节码文件:javap -v Demo1_22.class
,在控制台打印出字节码文件的内容 -
方法二:
直接在IDEA中进行配置,可直接右键查看反编译好的字节码文件,方法见文章:Intellij IDEA 查看字节码
-
运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
1.5.5 StringTable
StringTable特性
-
常量池中的字符串仅是符号,第一次用到时才变为对象。
-
利用串池的机制,来避免重复创建字符串对象
-
字符串拼接
package cn.itcast.jvm.t1.stringtable; // 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 } }
右键查看反编译后的字节码文件如下:
Classfile /E:/00_learning/JAVA/资料-解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class Last modified 2022年7月17日; 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: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #11 // cn/itcast/jvm/t1/stringtable/Demo1_22 super_class: #12 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #12.#36 // java/lang/Object."<init>":()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."<init>":()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 <init> #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 // "<init>":()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: (0x0001) 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/t1/stringtable/Demo1_22; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) 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 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 /* full_frame */ 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 /* full_frame */ 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 /* same_locals_1_stack_item */ stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ 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"
-
对于字符串变量拼接
String s4 = s1 + s2;
:原理是 StringBuilder (1.8)
其过程为:new StringBuilder().append("a").append("b").toString()
,即`new String(“ab”),其应当存储在堆中,而不是常量池
-
对于字符常量拼接
String s5 = "a" + "b";
:原理是编译期优化
javac 在编译期间的优化,结果已经在编译期确定为"ab"
,因此会直接找到常量池中的值
-
-
字符串延迟加载:在执行时是执行一行创建一个对象放入串池
package cn.itcast.jvm.t1.stringtable; /** * 演示字符串字面量也是【延迟】成为对象的 */ public class TestString { public static void main(String[] args) { int x = args.length; System.out.println(); // 字符串个数 2177 System.out.print("1"); // 字符串个数 2178 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"); // 字符串个数 2187 System.out.print("1"); // 字符串个数 2187 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); // 字符串个数 } }
可以使用IDEA的Debug工具进行内存查看:
-
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
-
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 串池中有此字符串对象,不会放入
package cn.itcast.jvm.t1.stringtable; 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);//true System.out.println( s == x);//false } }
- 串池中没有此字符串对象,放入串池, 并把串池中的对象返回
package cn.itcast.jvm.t1.stringtable; public class Demo1_23 { // ["ab", "a", "b"] public static void main(String[] args) { String s = new String("a") + new String("b"); // 堆 new String("a") new String("b") new String("ab") String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回 String x = "ab"; System.out.println( s2 == x);//true System.out.println( s == x);//true } }
- 串池中有此字符串对象,不会放入
-
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
- 串池中有此字符串对象,不会放入
package cn.itcast.jvm.t1.stringtable; 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);//true System.out.println( s == x);//false } }
- 串池中没有此字符串对象,放入串池, 并把串池中的对象返回
package cn.itcast.jvm.t1.stringtable; public class Demo1_23 { // ["ab", "a", "b"] public static void main(String[] args) { String s = new String("a") + new String("b"); // 堆 new String("a") new String("b") new String("ab") String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回 //s 拷贝一份,放入串池 String x = "ab"; System.out.println( s2 == x);//true System.out.println( s == x);//false } }
- 串池中有此字符串对象,不会放入
-
-
面试题:
package cn.itcast.jvm.t1.stringtable; 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") String x1 = "cd"; x2.intern(); // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢//调换位置后:true//jdk1.6:false System.out.println(x1 == x2);//false } }
StringTable位置
- jdk1.6时,StringTable是常量池的一部分,随常量池存储在永久代中;jdk1.7/jdk1.8时StringTable从永久代转移到了堆中。
- 原因:
永久代内存回收效率很低,只有Full GC时才会触发永久代的垃圾回收,而Full GC只有在整个老年代的空间不足时才会触发,时机较晚。而StringTable使用频繁,其中存储着大量字符串常量,若其回收效率不高就会占用大量内