运行时数据区
JVM运行java程序,会在内存执行。它会把内存分为多个部分,每个部分都有不同的生命周期与职责。我们这些部分称为运行时数据区。
运行时数据区分为5个部分: 堆、方法区、栈、本地方法栈、程序计数器
程序计数器
它相当于当前线程所执行的字节码行号指示器。用来存储下一个指令的地址。相当于指针。它是线程独享的。内存很小,每个线程都维护了一个程序计数器。程序计数器和线程进行绑定
Java栈
栈
主管JAVA程序的运行,它是线程独享的,随着线程的创建而创建,线程结束后,栈空间就会被释放。栈是一种数据结构,先进后出。
栈随着线程的创建而创建,随着线程的消失而消失,栈的内存空间就会被释放。所以栈空间是不会被GC优化的。
栈帧随着方法的调用而创建,随着方法结束而消
栈帧
栈中存放的是保存方法运行状态的栈帧。栈帧中存储着局部变量表(基本数据类型+引用类型)、操作数栈、方法出口、类型指针、动态链接等相关信息。
局部变量表存储的是方法内部的变量,在编译时期就已经完成分配,当执行一个方法时,这个方法在栈中分配的内存是固定的。
局部变量表是以slot(槽位)为基础单位,32位以内的数据占1个槽位(byte、short、int、float),64以内的数据占两个槽位(long、double)。引用类型的数据JVM没有规定其长度,可以占1个槽位,也可占2个槽位。
操作数栈
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。
和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用
JVM字节码指令
java的指令是以字节为单位,比如icost就占一个字节。所以java的指令不会超过256条。指令虽然是一个字节,但是它也可以带自己的操作数。
指令的操作数可以分为两种,一种是嵌在指令中的(嵌入式操作数),一种是在操作数栈中的(栈内操作数)。
嵌入式操作数和栈内操作数的区别?
嵌入式操作数是在编译时就已经确定的,运行时不会改变,它和指令一样存放
于类文件方法表的 Code 属性中;而操作数是运行时确定的,即程序在执行过
程中动态生成的。
局部变量表第一个变量
静态方法与实例方法本质上的区别是实例方法在类编译时,编译期会在实例方法内部中的局部变量表中第0个槽位引入this变量,指向当前对象。而静态方法没有this这个的引入。
int a = 1 + 2; int b = a + 3;,编译为字节码指令就是这样的:
iconst_1 //把整数 1 压入操作数栈
iconst_2 //把整数 2 压入操作数栈
iadd //栈顶的两个数出栈后相加,结果入栈;实际上前三步会被编译器优化为:iconst_3
istore_1 //把栈顶的内容放入局部变量表中索引为 1 的 slot 中,也就是 a 对应的空间中
iload_1 // 把局部变量表索引为 1 的 slot 中存放的变量值(3)加载至操作数栈
iconst_3
iadd //栈顶的两个数出栈后相加,结果入栈
istore_2 // 把栈顶的内容放入局部变量表中索引为 2 的 slot 中,也就是 b 对应的空间中
return // 方法返回指令,回到调用点
栈的操作(入栈、压栈、出栈):
每执行一个方法,就会在创建一个栈帧,保存在栈的顶部。顶部的栈帧就是当前执行的方法。方法从执行完毕,会自动将该方法对应的栈帧出栈。方法从开始执行到执行完毕,对应着个一个栈帧的压栈到出栈的动作。
方法区
方法区被所有线程共享。
方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
方法区存储着类文件对象(描述类的相关信息:类的版本、字段、方法、接口、常量 ) 以及符号引用。符号引用存储在运行时常量池中。
运行时常量池:存放着编译期生成的各种字面量(int i=1,1就是字面量)以及符号引用(变量名,包名,类名等),string类的常量等,
堆
一个JVM实例,只有一块堆内存。堆内存的大小是可以调节的。存储了引用对象的真实数据。
根据JVM的规范,堆内存在逻辑上分为新生代、老年代、方法区。在物理上,堆内存分为新生代和老年代。
【逻辑上方法区是堆内存的一部分,意味着逻辑上内存地址连续,实际上物理上方法区与堆内存的内存地址不连续】
方法区由多种实现。JDK1.7的永久代和JDK1.8元空间都是方法区的实现。JDK1.8之前,方法区的实现为永久代。JDK1.8之后,方法区的实现变为元空间。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
运行时常量池位置变化:
JDK 1.6 永久代 ,常量池在永久代中
JDK1.7 永久代,被弱化了。常量池在堆内存
JDK1.8 元空间,常量池在元空间。
JDK1.8:
1.新生代:Eden+From Survivor(s0)+To Survivor(s1)
2.老年代:OldGen
3.永久代(方法区的实现) : PermGen(永久代)----->替换为Metaspace(本地内存中)
新生代:
新生代分为三个区域:eden、s0 、s1。
栈中变量存储的是堆内存中对象的地址。
堆内存对象还维护了字节码文件在方法区中的内存地址。
元空间与永久代的历史背景:
BA-JRockit
sun-HotSpot
BA公司和sun公司都提出了jvm虚拟机的实现。Oracle收购了BA和sun。两个团队在内部进行PK。最终BA团队替换掉了JVM中的永久代,转化为元空间。