本文是个人JVM学习笔记整理,学习顺序主要基于 黑马程序员的JVM视频 相关知识点参考 深入理解Java虚拟机(第三版) 欢迎大家讨论交流并指正错误。
本篇笔记仅涉及JVM基本概念和结构。
1. Jvm
1.1. 定义
Java虚拟机(Java Virtual Machine)是一个抽象的计算机,JVM像一个真正的计算机一样,它有一个指令集,并在运行时操纵各种内存区域。JVM是Java字节码的运行环境,JVM在执行字节码时,会把字节码解释成不同平台上的机器指令执行。
1.2. 好处
- 可以跨平台运行
在一个操作系统上编写后可以在别的操作系统中运行
- 自动内存管理,垃圾回收功能
(相比较于C语言)
- 数组下标越界检查
(相比较于C语言,不会覆盖其他内容)
- 多态
面向对象编程的基石
1.3. JRE JDK比较
-
JVM(Java Virual Machine):是Java虚拟机,它是JRE的一部分,一个虚构出来的计算机,它支持跨平台。
JVM + 基础类库 = JRE -
JRE(Java Runtime Environment):是Java运行环境,所有的Java程序都要在JRE下才能运行。
JRE + 编译工具 = JDK -
JDK(Java Development
Kit):是Java开发工具包,它是程序开发者用来编译、调试Java程序,它也是Java程序,需要JRE才能运行。
1.4. Jvm结构
1、类从java源代码编译后通过classloader类加载器加载到jvm中
2、类放在方法区,类的实例对象放在堆中,调用方法时会用到虚拟机栈、程序计数器、本地方法栈
3、类执行时,每行代码由解释器逐行执行,热点代码(执行次数多的代码)会由即时编译器进行编译(优化后执行),垃圾回收会对堆中不再被引用的对象进行回收
4、本地方法接口负责调用操作系统的方法
2. 程序计数器
2.1. 定义、作用
程序计数器的作用就是记录下一条jvm指令执行的地址
指令的执行过程:
Java源代码 -> 编译为二进制字节码(jvm指令)-> 交给解释器编译为机器码 -> cpu执行
在第一条指令执行时,下一条指令的地址会被放入程序计数器,执行完毕后从程序计数器中取得下条指令的地址。
程序计数器通过寄存器实现(读写指令地址很频繁,因此用寄存器作为程序计数器)。
2.2. 特点
(1)线程私有
每个线程都有自己的程序计数器
线程切换时,会把线程1的下一条未执行指令地址存储于计数器中,只属于线程1
多线程执行时,会根据时间片切换线程,当从线程2切换回线程1时,会从线程1的程序计数器中取得程序地址。
(2)不存在内存溢出
3. 虚拟机栈
3.1. 定义
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
3.2. 容易混淆问题
(1)垃圾回收不涉及栈内存,只回收堆内存
栈内每次方法结束后,会自动出栈,不需要垃圾回收
(2)栈内存不需要过大,否则会影响线程数量
栈内存设置大只是可以进行更多次的方法内存调用,不会增强效率,反而会减少线程数目
栈内存可以通过 -Xss参数指定大小
(3)方法内局部变量是否线程安全
-
主要在于变量是否为方法内私有的,私有变量是线程安全的(没有逃离方法的作用范围)。
-
static变量多线程同时访问时,非线程安全,需要安全保护。
-
如果变量作为参数或者返回值(基本类型变量除外,引用对象则会出问题),超出了方法的作用范围,则非线程安全。
3.3. 栈内存溢出原因
报错为:StackOverflowError
(1)栈帧过多导致栈内存溢出
如:递归调用中不正确的结束条件,多个类之间的循环引用
(2)栈帧过大导致栈内存溢出
不常见,栈内局部变量一般不会超过栈内存大小(1M)
4. 本地方法栈
4.1. 定义
本地方法指,不是由java编写的代码(如c/c++),带有native的方法,
本地方法栈就是本地方法运行时所需要的内存空间。
如 Object中 的clone() 方法。
protected native Object clone() throws CloneNotSupportedException;
5. 堆
5.1. 定义
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
通过 new 关键字,创建对象时都会使用堆内存.
5.2. 特点
-
它是线程共享的,堆中对象都需要考虑线程安全的问题
-
有垃圾回收机制
5.3. 堆内存溢出
如果对象一直有程序调用并不断实例化对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会 产生内存溢出异常。
报错信息:
OutOfMemoryError: Java heap space
可以通过 -Xmx 参数控制堆内存大小
5.4. 堆内存诊断
(1)jps 工具
查看当前系统中有哪些 java 进程
(2)jmap 工具
查看堆内存占用情况 : jmap - heap 进程id (先用jps查看进程号)
只能查询某个时刻的使用情况,不能连续监测
(3)jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
6. 方法区
6.1. 定义
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区在虚拟机启动时被创建。
方法区在逻辑上是堆的一部分,但没有规定方法区的位置或用于管理编译代码的策略。
hotspot在JDK 8 之前使用永久代(堆的一部分)作为方法区,1.6之后使用元空间(本地内存)作为方法区。
6.2. 组成
hotspot在JDK 8 之前使用永久代(堆的一部分)作为方法区,这样使得 HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的 工作。
字符串表在方法区的常量池中
JDK 8 之后使用元空间(本地内存)作为方法区。
字符串表在堆中
6.3. 方法区内存溢出
jdk8 以前会导致永久代内存溢出
报错: java.lang.OutOfMemoryError: PermGen space
可以通过 -XX:MaxPermSize=8m 设置内存大小
jdk 8之后会导致元空间内存溢出
报错: java.lang.OutOfMemoryError: Metaspace
可以通过 -XX:MaxMetaspaceSize=8m 设置内存大小
6.4. 运行时常量池
方法区的一部分。
Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用(要执行的类名、方法名、参数类型、字面量等信息),这部分内容将在类加载后存放到方法区的运行时常量池中,并把里面的符号地址变为真实地址。
编译的二进制字节码包括:类基本信息、常量池、类方法定义(包含虚拟机指令)
对class文件反编译后可以查看以上信息:
开头是类方法信息
Constant pool是常量池
大括号中是类的方法定义,构造函数和其他方法
Code中是虚拟机指令
Code中的#1编号对应的是常量池中的元素
6.5. StringTable
6.5.1. 定义
用于存储字符串。
StringTable底层是通过 哈希表 实现的。
注意 String的创建方式和存储位置:
String s1 = "a";
String s2 = "b";
- 通过常量声明的字符串,会直接存入StringTable,如果StringTable中有此字符串,会直接返回
String s3 = new String("a")
- 通过 new 创建的字符串,会存入堆中
因此 s3==s1
为false
String s4 = "a" + "b";
String s5 = "ab"
- 字符串常量进行拼接时,java在编译期进行优化,它默认常量直接的拼接不会发生变化,结果在编译期间确定为”ab”。因此直接将“ab” 存入StringTable
因此s4==s5
为true
String s6 = s1 + s2; // new String("ab")
- 如果将两个String对象进行拼接,java处理中会通过StringBuilder对象进行拼接,之后返回sb.toString(),此方法会new一个String对象存放在堆中
因此s4==s6
为false
可以使用 String的intern()方法,主动将StringTable中还没有的字符串对象放入StringTable。
注意在jdk8 之后,如果StringTable中有此String则并不会放入,直接返回StringTable的string对象;如果没有则会放入StringTable, 并把StringTable中的对象返回。
String x1 = "ab";
String x2 = s1 + s2; //因为x1先执行,StringTable中已经有"ab",x2不会存入StringTable
String x3 = x2.intern();
此处 x1==x2
为false,x1==x3
为true
如果顺序变为:
String x2 = s1 + s2; //因为x1先执行,StringTable中没有"ab",x2存入StringTable
String x3 = x2.intern();
String x1 = "ab";
x1==x2
为true,x1==x3
为true
在jdk8 之前,如果StringTable 中 没有此String,会把此对象复制一份放入StringTable ,并返回StringTable中的对象。
因此不论顺序如何,x1==x2
均为false
6.5.2. StringTable 位置
jdk8之前在永久代的常量池中
jdk8之后在 堆中
原因:
永久代垃圾回收触发很晚,内存回收效率很低,但stringtable使用频繁,回收效率低会占用大量内存;因此转移到堆中,垃圾回收效率高
6.5.3. StringTable垃圾回收
当内存空间不足时,StringTable也会被垃圾回收。
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
可以查看StringTable大小和垃圾回收信息
6.5.4. StringTable 性能调优
(1)调整StringTable底层 哈希表 桶的个数
通过 -XX:StringTableSize= 调整桶个数
如果系统中字符串常量比较多,可以将桶大小设置大一点,以寻求更好的哈希分布,提升性能
(2) 考虑将字符串对象放入StringTable
使用 intern 方法,将StringTable中还没有的字符串对象放入StringTable;程序运行中有很多重复的字符串对象,不放入stringtable ,只在堆中会占用很大空间,放入stringtable后相同的字符串只有一份。
8. 直接内存
8.1. 定义
是操作系统的内存,不属于jvm管理。
- 常见于 NIO(New Input/Output) 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
直接内存进行IO操作,通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作,相比较于传统Java IO更快
传统Java IO:
复制文件时 Java IO需要从系统内存中的缓冲区中读取数据到java的缓冲区;
DirectByteBuffer 则会直接在系统内存中划出一个直接内存,java复制时候,系统将文件数据读取到直接内存,java也可以从直接内存中读取数据,少了一次数据读取操作。
8.2. 直接内存的内存溢出
报错: java.lang.OutOfMemoryError: Direct buffer memory
8.3. 直接内存的分配和回收原理
-
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
-
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存