视频链接:黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓_哔哩哔哩_bilibili
跟着视频做的笔记
1. 入门
1.1 什么是JVM
1.1.1 定义
Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。
1.1.2 好处
-
一次编译,处处执行
-
自动的内存管理,垃圾回收机制
-
数组下标越界检查
1.1.3 JVM、JRE、JDK 的关系
JDK(Java Development Kit):是Java开发工具包,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。
JRE ( Java Runtime Environment):是Java的运行环境,包含JVM标准实现及Java核心类库。
JVM (Java Virtual Machine):是Java虚拟机,是整个Java 实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。所有的Java程序会首先被编译为.class 的类文件,这种类文件可以在虚拟机上执行。
1.2 JVM有什么用
-
面试必备
-
中高级程序员必备
-
想走的长远,就需要懂原理,比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事等待,JVM 是必须掌握的。
1.3 常见的JVM
我们主要学习的是 HotSpot 版本的虚拟机。
1.4 学习路线
-
ClassLoader(类加载器):Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
-
Method Area(方法区):类是放在方法区中。
-
Heap(堆):类的实例对象。
-
当类调用方法时,会用到 JVM Stacks(虚拟机栈)、PC Register(程序计数器)、Native Method Stacks(本地方法栈)。
-
方法执行时的每行代码是有执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口。
2. 内存结构
2.1 程序计数器(PC Register)
2.1.1 定义
Program Counter Register 程序计数器(寄存器)
2.1.2 特点
-
不会存在内存溢出
-
是
线程私有
的-
每个线程都有自己的程序计数器
-
线程之间采用时间片轮转的方式抢占cpu,此时各自的程序计数器会记录自己的执行到第几行的地址。
-
2.1.3 作用
作用:是记录下一条 jvm 指令的执行地址行号。
-
解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
-
多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。
2.2 虚拟机栈(JVM Stacks)
2.2.1 定义
-
每个线程运行需要的内存空间,称为
虚拟机栈
。 -
每个
栈
都由多个栈帧
(Frame,每次调用方法
时所占用的内存,存放的都是方法中的局部变量,包括方法的参数、方法内部变量)组成。 -
每个线程只能有一个
活动栈帧
(当前正在执行的方法)。
2.2.2 问题辨析
-
垃圾回收是否涉及栈内存? 不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。
-
栈内存分配越大越好吗? 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
-
方法的局部变量是否线程安全
-
如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
-
如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。
-
2.2.3 栈内存溢出(stackOverflowError)
-
栈帧过大
-
栈帧过多
-
第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError 。
案例
案例:第三方类库操作造成栈内存溢出
报错原因:不停的调用--员工信息里有部门信息,但是部门里又有员工信息,不停地调用。
2.3.4 使用 -Xss256k 指定栈内存大小!
2.2.5 线程运行诊断
案例一: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 Method Stacks)
一些带有 native
关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。
例如常用的源码Object类、HashMap中有的方法就是。
2.4 堆(Heap)
2.4.1 定义
通过new关键字创建的对象都会被放在堆内存
2.4.2 特点
-
它是
线程共享
,堆内存中的对象都需要考虑线程安全问题 -
有垃圾回收机制
2.4.3 栈内存溢出
java.lang.OutofMemoryError :java heap space. 堆内存溢出
2.3.4 可以使用 -Xmx8m
来指定堆内存大小。
2.4.5 栈内存诊断
-
jps 工具 查看当前系统中有哪些 java 进程
-
jmap 工具 查看堆内存占用情况 jmap - heap 进程id
-
jconsole 工具 图形界面的,多功能的监测工具,可以连续监测
-
jvisualvm 工具
案例:
package cn.itcast.jvm.t1.heap;
/**
* 演示堆内存
*/
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查看进程号
jmap查看内存占用情况
2.5 方法区(Method Area)
2.5.1 定义
-
方法区是各个线程共享的内存区域
-
方法区在jvm启动时就被创建,并且它的实际物理内存空间是可以不连续的
-
关闭jvm就会释放这个区域的内存
-
方法区的大小,可以选择固定大小或者扩展
-
方法区中存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的
2.5.2 Hotspot虚拟机的内存结构图
永久代和元空间可以理解为是方法区的实现
jdk1.6
jdk1.8
2.5.3 方法区内存溢出
-
1.8 之前会导致永久代内存溢出
-
使用 -XX:MaxPermSize=8m 指定永久代内存大小
-
-
1.8 之后会导致元空间内存溢出
-
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
-
-
在下图位置添加
2.5.4 常量池
概念:常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
二进制字节码.class(类基本信息,常量池,类方法定义【包含了虚拟机指令】)
通常情况下,我们看不到二进制字节码,所以我们可以通过javap工具对响应类的.class文件进行反编译,此时可以勉强看懂
javap -v HelloWorld.class
public class Test {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
类的基本信息,当中的注释是java解释器通过#2、#3在常量池中进行查找进行解释。
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/PrintStr
eam.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;
}
虚拟机指令,包含在类方法里
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/PrintStr
2.5.5 运行时常量池
运行时常量池:常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
2.5.6 StringTable
-
常量池的字符串仅是符号,只有在被用到时才会转化为对象
-
利用串池的机制,避免重复创建字符串对象
-
字符串变量拼接的原理是SpringBuilder
-
字符串常量拼接的原理是编译器优化
-
可以使用intern()方法,主动将串池中还么有的字符串放到串池当中
2.5.6.1 intern方法(jdk1.8)
调用字符串对象的 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 Main {
public static void main(String[] args) {
// 此处创建字符串对象 "ab" ,因为串池中还没有 "ab" ,所以将其放入串池中
String str3 = "ab";
// "a" "b" 被放入串池中,str 则存在于堆内存之中
String str = new String("a") + new String("b");
// 此时因为在创建 str3 时,"ab" 已存在与串池中,所以放入失败,但是会返回串池中的 "ab"
String str2 = str.intern();
System.out.println(str == str2);//false,str 则存在于堆内存之中
System.out.println(str == str3);//false,str2是常量池中str对象的返回值
System.out.println(str2 == str3);//true,str3是存在常量池中
}
}
2.5.6.2 intern方法 (jdk1.6)
调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中
-
如果串池中没有该字符串对象,则放入成功,并且返回串池中的字符串对象,此时堆内存与串池中的字符串对象是同一个对象
-
如果有该字符串对象,则放入失败,并且会把此对象复制一份,放入串池,返回串池中的字符串对象,此时堆内存与串池中的字符串对象不是同一个对象
案例:放入成功
package cn.itcast.jvm;
public class Demo1_23 {
// ["a", "b", "ab"]
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,x和s2存放的是串池
System.out.println( s == x );//flase,s存放在堆中,是堆中对象
}
}
2.5.6.3 面试题
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"; //"ab"已经存在于字符串常量池中了,所以此时s5指的是常量池中的"ab"
String s6 = s4.intern(); //常量池中已经有"ab",返回常量池中的"ab"
// 问
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(); //jdk1.8:常量池中么有"cd",因此存放到常量池中;jdk1.6:常量池中么有"cd",放入成功,所以此时x1是常量池,x2的堆
String x1 = "cd";//常量池中已经有"cd",返回常量池中的"cd"
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
//jdk1.8:true;如果调换了【最后两行代码】的位置,返回flase;
//jdk1.6:false;如果调换了【最后两行代码】的位置,返回false;
}
}
2.5.6.4 StringTable 的位置
jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。
2.5.6.5 StringTable 垃圾回收
-
-Xmx10m 指定堆内存大小
-
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-
-XX:+PrintGCDetails
-
-verbose:gc 打印 gc 的次数,耗费时间等信息
-
在下图位置添加
2.5.6.6 StringTable 性能调优
考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=桶个数(最少设置为 1009 )
在下图中添加
2.6 直接内存(Direct Memory)
-
常见于 NIO 操作时,用于数据缓冲区
-
分配回收成本较高,但读写性能高
-
不受 JVM 内存回收管理
-
读取视频时,速度非常快
-
直接内存,既可以被java堆内存读取,也可以被系统内存读取