目标:
1.JVM是什么?
2.JVM内存怎么分区?按照什么方式分区?为什么要进行分区管理?
3.分区数据怎么管理?
一、JVM是什么
1.1 JVM规范
JVM是一种规范,是JVM是虚拟机内存管理的规范和标准。Oracle公司提供了Java虚拟机规范文档,符合规范的语言都可以运行的JVM上。当然,用户也可以自己按照JVM的规范,设计自己的虚拟机,然后通过Oracle官方认证,就可以成为认可的JVM。
因此,JVM具有以下特性:
JVM跨平台型:可以运行的在不同的设备终端,包括Windows\Linux\Unix\Android\Mac等;
JVM语言无关性:遵循JVM规范的语言都可以运行在JVM上。例如Java\Scala\Kotlin\groovy等。
常见的JVM实现:
1.2 Java程序运行过程
Java程序从编译到执行的过程
编译:使用javac 将Java程序编译为字节码文件,helloWorld.java --》helloWorld.class文件
加载:通过类加载器(ClassLoader)将字节码加载到方法区,
执行:然后交给JVM执行引擎进行进行。JVM执行引擎会把字节码文件翻译为 操作系统OS识别的机器码进行执行。
1.3 JDK、JRE与JVM关系
JDK是一个大的合集,包括JRE和JVM。包括编译工具javac、Java运行时环境JRE.
JRE:Java运行时环境。它包含了Java虚拟机(JVM)和Java核心类库,但不包括Java开发工具(如编译器和调试器)。这意味着JRE是Java程序运行时的最小环境,而JDK(Java开发工具包)则提供了开发、编译和调试Java应用程序所需的完整工具集。
JVM:Java虚拟机,主要负责类加载和执行,垃圾回收。
1.4 解释执行和JIT
解释执行:字节码解释器,将class文件翻译为操作系统认识的机器码(一般是汇编程序或者二进制执行程序),交由操作系统执行。因为需要经过一次翻译,效率相对于直接编译为机器码的语言要低。
JIT执行:热点代码是指程序中的一些代码、方法,执行次数达到一定后(后端一般是1万+),作为热点代码。JIT编译器会将Java代码(class代码)直接翻译为机器码执行,提高效率。翻译面板存在于JVM的Codecache。Oracle HotSpot虚拟机支持JIT。
二、JVM分区管理
2.1 运行时数据区
什么是运行是数据区?Java虚拟机在执行Java程序的过程中会把它所管理的内存(JVM)划分不同的数据区域。
运行时数据库采用的是JVM内存分区管理的机制。分区管理使内存管理更加规范化。
2.2 JVM内存分区
运行时数据区按照功能划分为:线程共享(方法区和堆)和线程私有(虚拟机栈、本地方法栈、程序计数器)
方法区:方法区主要存储了类的结构信息(Clazz)、运行时常量、静态变量, HotSpot中称为永久代、元空间(MataSpace)。
堆:存储对象实体,JVM中空间最大的一部分。堆是容易发生内存溢出的主要分区。
虚拟机栈:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的。Java程序的执行对应的一系列方法的调用,一个方法对应栈中一个栈帧,因此虚拟机栈也称为方法栈。
本地方法栈:Java本地方法(Native调用的方法,如C++/C程序)执行的栈。Native方法的执行在本地方法栈中执行(也是方法栈帧的方式)。
程序计数器:是一个较小的内存空间,用于存储指向下一条指令的地址。程序计数器分区永远不会发生内存溢出。
2.3 直接内存
进程中还有一部分,没有经过JVM进行虚拟化的部分--直接内存,不属于 JVM统一管理。
三、Java方法运行与虚拟机栈
3.1 虚拟机栈
用于存储当前线程运行Java方法所需的数据、指令、返回地址。
3.1.1 修改栈大小限制-Xss
默认1M大小
3.1.2 栈溢出
栈内存不足时会发生栈溢出。栈溢出常见的两种情况:
方法调用死循环:方法调用出现死循环,一直不停的创建栈帧,直到栈溢出
创建大对象:创建的栈对象超出了最大栈空间大小。例如栈限制大小2M,创建一个3M的栈数组。
3.2 栈帧
Java执行方法时,会生成一个栈帧,压入虚拟机栈。
线程执行方法 main-->A()-->B(),执行时栈内存中情况
栈帧的管理先进后出的规则(栈存储的特点)
3.2.1 栈帧结构
3.3 Java方法运行内存
简单的程序演示java程序运行流程
我们看work方法的执行过程
3.3.1 方法执行流程
1) woke方法栈帧入栈
方法区work方法的代码,创建一个栈帧,用于work方法的执行,并且将栈帧压入虚拟机栈。
字节码左侧数字: 字节码地址,字节码指令在work方法中的偏移量,
字节码指令:数字后面是字节码指令
2) 执行第一条指令 iconst_1: 程序计数器修改指令地址为0,执行0号指令。
创建一个int类型的常量,值为1,
3)执行第1号指令 istore_1: 将操作数栈中的数据弹出来(POP出栈),放入局部变量表 1 号位置。程序计数器指向方法 1 位置。
istore是写入一个整数值。
4)同样的方法执行指令2号指令,程序计数器变为2,创建一个整型常量,值为2,压入操作数栈;
执行指令3号指令,程序计数器变为3,操作数栈数据出栈,存入局部变量表 2 号位置。
5)执行第4号指令,程序计数器变为4,iload_1指令,把局部变量表下标为1的整数,压入操作数栈;
执行5号指令,将局部变量表2号位置的整数压入操作数栈,程序计数器变为5,操作数栈压入局部表量表2号位置的整数,值为2,位于操作数栈栈顶。
6)指令6号指令,程序计数器变为6,执行指令iadd, 先把两个操作数分别出栈;
然后计算,2+1=3,
将计算结果重新压入操作数栈
7) 执行7号指令,bipush 10,将整数10 压入操作数栈,这个是双字节指令,程序计数器变为7
8)执行9号指令,程序计数器变为9,imul两个整数相乘,操作数栈中的两个整数分别出栈,计算得到结果:10*3=30;将计算结果重新压入操作数栈。
9)执行第10号指令,程序计数器变为10,istore_3, 将操作数栈中的整数出栈,存入局部变量表3号位置。
10)执行11号指令,程序计数器变为10,iload_3,将局部变量表3号位置的整型数(30),加载到操作数栈中。
11)执行指令12号,程序计数器变为12,ireturn, 操作数栈中的整型数(30)出栈,work栈帧出栈,返回main-栈帧,返回值30,返回位置在完成出口记录(main方法的3号位置),在main方法
好位置继续接着执行。work方法栈帧结束生命周期,释放work栈帧内存。
3.2.2 本地方法栈
Java直接操作不了线程,需要通过本地方法,调用线程操作。运行到本地方法时,因为不是字节码程序,不能得到对应的指令地址,程序计数器置位null
3.4 运行时数据区其他区域
3.5 对象在JVM中的分配
四、从底层深入理解运行时数据区
JHSDB工具,位于SDK/lib/sa-jdi.jar
启动方法:
java -cp [sa-jdi.jar路径] sun.jvm.hotspot.HSDB
1) attach需要查看的进程
2)查看堆的话,能够看到对内存分区的物理地址范围
Java新生代进阶老年代,15岁进阶老年代,因为表示年龄的4个bit位,1111,最大为15.
面试题
1.JVM为什么需要采用分区管理模式?