目录
1.程序计数器(Program Counter Register)(寄存器)
4.4.1 jps工具:(java process statistic Java进程统计)
4.4.4 jvisualvm可以查看对象个数,以及对象的详细信息
1.什么是JVM
定义:
Java程序的运行环境(Java二进制字节码的运行环境也就是被编译过后的.class文件的运行环境)
好处:
1.一次编写到处运行(只要安装了jdk)
2.自动内存管理,垃圾回收机制
3.数组下标越界检查
4.多态
关系:
jvm、jre、jdk
2.学习JVM的作用
1.理解底层的实现原理
2.向更高级的程序员看齐
3.常见的JVM
由于JVM是一种规范,所以实现它的公司和团体有很多,我们较常用的还是(HotSpot,Oracle JDK edition )
4.学习路线
一、内存结构
1.程序计数器(Program Counter Register)(寄存器)
1.1 定义
JVM规范—程序计数器:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1
当执行一条指令时,首先需要根据PC中存放的指令地址,将指令读取到指令寄存器中,称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。
1.2 作用
记住下一条jvm指令的执行地址。
过程:二进制字节码-->解释器-->机器码-->cpu
解释器从程序计数器中获取下一条jvm指令的执行地址。
1.3 特点
1.是线程私有的,每个线程都有自己的程序计数器。
2.不会存在内存的溢出问题,仅仅存储指令的地址,有一定的大小限制
2.虚拟机栈(JVM Stacks)
2.1 定义
JVM规范—虚拟机栈:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1
每个jvm都有一个私有的jvm栈,与线程同时创建。一个Jvm栈存储栈帧。jvm栈类似于传统的C语言的栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。因为jvm栈从来不直接操作,除了入栈和出栈,栈帧可能被分配。jvm堆栈的内存不需要是连续的。
在jvm规范第一版中,jvm栈被称为Java栈。
该规范允许jvm栈具有固定的大小,或者根据计算需要动态地扩展和收缩。如果jvm栈是固定大小的,则在创建栈时可以独立的选择每个Jvm的大小。
一个Jvm实现可以为程序员或用户提供对 Jvm栈初始大小的控制,以及在动态扩展或收缩 Jvm栈的情况下,对最大和最小大小的控制。
以下异常情况和jvm虚拟机栈相关联:
·如果一个线程中的计算需要一个比允许的更大的jvm栈,则jvm抛出一个StackOverflow Error。
·如果可以动态扩展jvm栈,并且尝试了扩展,但可以提供的内存不足,无法实现扩展,或者,如果没有足够的内存可用于为新线程创建初始jvm栈,则jvm抛出Out of Memory Error。
2.2 作用
2.3 特点
1.是线程私有的,每个线程都有自己的虚拟机栈。
2.每个栈由多个栈帧(Frame)组成,对应着每个方法调用时所占用的内存
3.每个线程只能有一个活动栈帧,对应着当前执行的那个方法
4.会出现内存溢出的情况.
(1)栈内存过多导致内存溢出,比如递归调用
(2)栈帧过大导致栈内存溢出
2.4 问题辨析
1.垃圾回收是否涉及栈内存?
栈是由多个栈帧组成,栈帧按照先进后出的执行策略,每一个栈帧执行完成会自动弹出虚拟机栈,从而释放内存,所以栈内存不需要垃圾回收。
2.栈内存分配越大越好吗?
虚拟机栈设置越大,相应的线程数会变小
https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html
3. 方法内的局部变量是否线程安全?
如果方法内的局部变量没有逃离方法的作用范围,它是线程安全的
如果局部变量引用了对象,或者逃离了方法的作用范围,需要考虑线程安全问题
3.本地方法栈(Native Method Stacks)
3.1 定义
JVM规范—本地方法栈: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1
jvm的实现可以使用常规的栈(俗称“C栈”)来支持方法(用Java编程语言以外的语言编写的方法)。本地方法栈也可以由jvm指令集的解释器的实现使用,例如C无法加载方法并且本身不依赖于常规栈的jvm实现不需要提供本地方法栈。如果提供,则在创建每个线程的时候,通常为每个线程分配本地方法栈。nativenative
此规范允许本地方法栈具有固定的大小,或者根据计算的需要动态扩展和收缩。如果本地方法栈的大小是固定的,则在创建该栈时,可以独立选择每个本地方法栈的大小。
jvm实现可以为程序员或者用户提供对本地方法栈初始大小的控制,以及在大小不同的本地方法栈的情况下,对最大和最小方法栈大小的控制。
以下异常情况和本地方法栈相关联:
·如果一个线程中的计算需要的本地方法栈大于允许的范围,则jvm抛出一个StackOverflow Error。
·如果本地方法栈可以动态扩展,并且尝试了扩展,但可以提供的内存不足,无法实现扩展,或者,如果没有足够的内存可用于为新线程创建初始本地方法栈,则jvm抛出Out of Memory Error。
3.2 特点
1.本地方法栈与虚拟机栈的区别是,虚拟机栈执行的的是Java方法,本地方法栈执行的是本地方法(Native Method),其他的基本上一致
2.线程私有的
3.会内存溢出
4.例如
Object中的clone()
FileInputStream中的read()
FileOutputStream中的write(int b)
4.堆(Heap)
4.1 定义
JVM规范—堆:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1
jvm具有一个在所有jvm线程之间共享的堆。堆是从中分配所有类实例和数组的内存的运行时数据区域。
堆创建于jvm启动时。对象的堆存储由自动存储管理系统(称为垃圾回收器)回收,从不显示解除分配对象。jvm假定没有特定类型的自动存储管理系统,存储管理技术可以根据实施者的系统要求进行选择。堆可以是固定的大小,也可以根据计算的需要进行扩展,如果不需要更大的堆。堆的内存不需要是连续的。
jvm实现可以为程序员或用户提供对堆的初始大小的控制,以及如果堆可以动态扩展或收缩,则可以控制最大和最小堆大小。
以下异常情况与堆相关联:
·如果计算需要的堆比自动存储管理系统可用的堆多,jvm将抛出一个OutOfMemoryError
4.2 作用及特点
1.通过new关键字,创建对象都会使用堆内存
2.堆是线程共享的,堆中对象都需要考虑线程安全的问题
3.堆有垃圾回收机制
4.3 堆内存溢出以及如何调整堆内存的大小
package Heap;
import java.util.ArrayList;
import java.util.List;
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m
*/
public class Heap_4_2_1 {
public static void main(String[] args) {
int i = 0;
try{
List<String> list = new ArrayList<>();
String s = "hello";
while(true){
list.add(s);
s += s; // hello hellohello hellohellohellohello
i++;
}
}catch(Throwable e){
e.printStackTrace();
System.out.println(i);
}
}
}
-Xmx8m可以调整堆内存的大小为8M
4.4 堆内存诊断
package Heap;
public class Heap_4_3_1 {
public static void main(String[] args) throws InterruptedException{
// 先使用jps命令,然后再在1 2 3输出之后分别使用jmap命令查看堆内存使用情况
System.out.println("1...................");
Thread.sleep(30000);
byte[] bs = new byte[1024*1024*10]; // 10MB
System.out.println("2...................");
Thread.sleep(40000);
bs = null;
System.gc();
System.out.println("3...................");
Thread.sleep(1000000L);
}
}
4.4.1 jps工具:(java process statistic Java进程统计)
查看当前系统中有哪些java进程 ,右键进入Terminal可以直接进到路径下面
4.4.2 jmap工具(相当于给进程拍照片)
查看堆内存占用情况,jmap - heap 进程id 查看堆内存使用变化
ps:前面的这些都是基本的堆内存的配置情况
接下来我们看堆内存的使用情况:
下面这张图片是上述程序刚运行的时候堆内存的使用情况:
new byte[]之后:
将对象的引用置为null并且进行显示的gc之后:
可以明显地看到堆内存的变化情况:
4.4.3 jconsole工具(连续监测)
图形界面的,多功能的监测工具,可以连续监测,堆内存变化。
自动弹出:
选择你要监测的程序:链接的时候可能会提示不安全的链接,直接连接就行
可以清晰的看到内存的动态变化过程。
4.4.4 jvisualvm可以查看对象个数,以及对象的详细信息
package Heap;
import java.util.ArrayList;
import java.util.List;
/**
* 演示查看对象个数
*/
public class Heap_4_3_2 {
public static void main(String[] args) throws InterruptedException{
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
}
Thread.sleep(1000000L);
}
}
class Student{
private byte[] big = new byte[1024*1024];
}
我们首先依然使用jconsole查看一下内存使用情况,我们中间进行一下gc(右上角)发现内存使用减少的不明显,我就想知道到底是谁占用内存这么多,并且gc回收不掉。
也是会自动弹出来:选择要查看的程序,双击
点击抽样器,点击内存。
我们可以看到字节数组占用内存很大,我们点击右上角的堆Dump:
可以查找前多少个最大的对象:
双击还可以更加详细的查看细节:
5.方法区(Method Area)
5.1 定义
jvm规范-方法区定义:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1
jvm具有一个在所有jvm线程之间共享的方法区。方法区类似于常规语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法。
方法区是在jvm启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾回收或压缩它。此规范不要求方法区的位置或用于管理已编译代码的策略。方法区可以是固定大小的,也可以根据计算的需要进行扩展,如果不需要更大的方法区,则可以收缩。方法区的内存不需要是连续的。
jvm实现可以为程序员或用户提供对方法区的初始大小的控制,以及在方法区大小不同的情况下,对最大和最小方法区大小的控制。
以下异常情况与方法区相关联:
·如果方法区中的内存无法用于满足分配请求,jvm将抛出OutOfMemoryError。
5.2 特点
1.是线程共享的
2.存在内存溢出的情况
5.3 组成
1.jvm1.6
2.jvm1.8 (方法区被分配到本地内存)
5.4 方法区内存溢出
·1.8以前会导致永久代内存溢出(ps:这个就不做演示了)
java.lang.OutOfMemoryError:PermGen space
调整大小:-XX:MaxPermSize=??
·1.8之后会导致元空间内存溢出
package Heap;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=10m 最大元空间大小
* -XX:-UseCompressedOops 关闭压缩指针
*/
public class Method_5_3_1_8 extends ClassLoader{
public static void main(String[] args) {
int count = 0;
try{
Method_5_3_1_8 clz = new Method_5_3_1_8();
for (int i = 0; i < 30000; i++,count++) {
// 每一次for循环,做了一个类加载过程
// ClassWriter 作用是生成类的二进制字节码 byte[] --> Class对象
ClassWriter cw = new ClassWriter(0);
// 参数:版本号 访问权限 类名 包名 父类 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "class" + i, null, "java/lang/Object", null);
// 生成二进制字节码
byte[] code = cw.toByteArray();
// 执行类的加载
clz.defineClass("class" + i, code, 0, code.length); // Class对象
}
}finally {
System.out.println(count);
}
}
}
首先:最大元空间大小是很大的,所以先进行一下限制。其次,如果你不关闭压缩指针的话它会报:压缩类空间不足,这是因为在metaspace里面有一片空间专门存放压缩类文件。(网上说默认大小是1G)压缩指针默认是开启的。
关闭之后就可以看到元空间内存不足:
5.5 运行时常量池(区别于常量池)
5.5.1 两者区别
常量池:就是一张表,虚拟指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息。
运行时常量池:常量池是*.class文件中的,当该类被加载时,它的常量池信息就会放入运行时常量池,并把里面的符号变为真实地址。
查看一下常量池:
package method;
public class Method_5_4_1 {
// 二进制字节码(类基本信息、常量池、类方法定义(包含了虚拟机指令))
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
找到编译后的代码:
右键包名(是包名,不是类名):
执行javap -v命令:
看一下常量池:
还可以看一下main函数的底层执行过程:(具体的指令可以查看jvm指令手册)
5.6 StringTable
package method;
public class Method_5_5_1 {
// StringTable 串池,Hashtable结构,不能扩容
// ["a","b","ab"]
// 常量池中的信息,都会被加载到运行时常量池中
// 这时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()
String s5 = "a" + "b"; // javac在编译期间的优化,自动拼接字符串常量
// intern()方法会尝试将字符串字面量放入串池,如果存在,不放入,返回串池中的字面量
// 如果没有,则将该字面量放入串池,返回串池中的字面量
String s6 = s4.intern();//由于串池里面本身存在ab的地址,所以放不进去,返回串池的字面量,s6指向串池
System.out.println(s3==s4);//s3的指向在串池,s4的指向是堆里面的一块空间
System.out.println(s3==s5);
System.out.println(s4==s6);
}
}
运行结果: