JVM的内存结构
JVM是什么,有什么作用
先说一下JRE,大家都知道,要想运行Java程序就必须要安装jre,jre则是由jvm和Java的核心类库组成的,那么jvm是是什么呢?jvm是Java Virtual Machine(Java虚拟机)的缩写,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。作用是将Java语言解释成计算机可识别的机器码文件,Java语言最重要的特点就是跨平台运行。使用jvm就是为了支持与操作系统无关,实现跨平台运行Java程序。
JVM 的内存结构
Java在执行Java程序时,会把他所管理的内存划分位若干个不同的数据区域,这些区域都有各自的用途、创建和销毁的时间,有的区域会随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁的。
1.程序计数器(寄存器)
程序计数器是一块较小的内存空间,你可以先将它看作是当前线程所执行字节码文件的行号指示器,在Java虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是流程控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
为什么需要记数呢?因为Java虚拟机是多线程的,多线程是通过线程流切换、分配处理器执行时间的方式来完成的,在任何一个确定的时间,一个处理器都只会执行一条线程种的指令,因此,为了线程切换后能回到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立储存,也就是所谓的 “线程私有”。
另外,如果线程执行的是本地的(Native)方法,则这个计数器的值应为空,此内存区域是唯一一个在《Java虚拟机规范》种没有规定任何OutofMemoryError情况的区域。
2.Java虚拟机栈
首先,与程序计数器一样,Java虚拟机器栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是方法执行的线程内存模型:每个方法被执行的时候Java虚拟机都会同步创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法被调用直至被执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
经常有人把Java的内存区域笼统的分为堆内存和栈内存,这种方法直接继承子传统的c、c++程序的内存接口,实际Java的内存区域划分要比这更复杂。
局部变量表中存放了编译器可知的Java虚拟机的基本数据类型(byte、short、int、long、char、float、double)、对象引用、和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中以局部变量槽来表示,其中long、double占两个槽,其余类型只占一个。
操作数栈在方法执行的过程中,根据字节码指令,向栈中写入数据或者提取数据,同时也会保存计算过程的中间结果,如果被调用方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中。
动态链接每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中方法的符号引用为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
方法出口指的是 执行完该行代码后 , 下一行要运行的代码,
当一个方法开始执行后,只有两种方式可以退出这个方法:
执行引擎遇到任意一个方法返回的字节码指令:传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法来返回指令决定,这种退出的方法称为正常完成出口。
方法执行过程中遇到异常: 无论是java虚拟机内部产生的异常还是代码中thtrow出的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出的方式称为异常完成出口,一个方法若使用该方式退出,是不会给上层调用者任何返回值的。无论使用那种方式退出方法,都要返回到方法被调用的位置,程序才能继续执行。方法返回时可能会在栈帧中保存一些信息,用来恢复上层方法的执行状态。一般方法正常退出的时候,调用者的pc计数器的值可以作为返回地址,帧栈中很有可能会保存这个计数器的值作为返回地址。方法退出的过程就是栈帧在虚拟机栈上的出栈过程,因此退出时的操作可能有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者的操作数栈每条整pc计数器的值指向调用该方法的后一条指令。
3.本地方法栈
本地方法栈与虚拟机方法栈的作用是十分相似的,其区别在于虚拟机栈位虚拟机执行Java方法服务,而本地方法栈则是为虚拟机用到的本地方法提供服务。
4.堆
堆是虚拟机所管理内存中最大的一块,Java堆是被所有线程所共享的一块区域,在虚拟机启动时创建。也是由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题。
存放哪些资源:
- 对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存
- 字符串常量池:
- 字符串常量池原本存放于方法区,JDK7 开始放置于堆中
- 字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table
- 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
- 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率
设置堆内存指令:-Xmx Size
内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常
5.方法区
用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式
方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)
方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现
为了避免方法区出现 OOM,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中
类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表
class文件中的类型信息、域信息、方法信息都会被类加载器加载到方法区中。
Last modified 2020-4-22; size 1626 bytes
MD5 checksum 69643a16925bb67a96f54050375c75d0
Compiled from "MethodInnerStrucTest.java"
//类型信息会被加载到方法区
public class com.atguigu.java.MethodInnerStrucTest extends java.lang.Object // 类的全限定名以及父类
implements java.lang.Comparable<java.lang.String>, java.io.Serializable //类实现的接口信息
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER // 类的权限修饰符
Constant pool:
#1 = Methodref #18.#52 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#53 // com/atguigu/java/MethodInnerStrucTest.num:I
#3 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream;
...
{
//域信息会被加载到方法区
public int num; // 域名称
descriptor: I // 域类型
flags: ACC_PUBLIC // 域权限
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
//方法信息会被加载到方法区
public com.atguigu.java.MethodInnerStrucTest(); // 方法名称
descriptor: ()V //方法参数及方法返回值类型
flags: ACC_PUBLIC //方法权限修饰符
Code: //方法对应的字节码
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 10: 0
line 12: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/atguigu/java/MethodInnerStrucTest;
......
}
Signature: #49 // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
SourceFile: "MethodInnerStrucTest.java"
常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的字面量、符号引用,JVM 为每个已加载的类维护一个常量池
- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等
- 符号引用:类、字段、方法、接口等的符号引用
运行时常量池是方法区的一部分
- 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池
- 类在解析阶段将这些符号引用替换成直接引用
- 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()
“青年俊秀,一时无二,谪仙人。
少年之美,风清月白,思无邪。”