目录
二,程序计数器(Program Counter Register)
一,前言:
JVM(Java Virtual Machine) 即java虚拟机,主要由字节码指令集、寄存器、栈、垃圾回收堆和存储方法域等构成。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码
(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行
我们都知道,java源文件通过编译器编译后能形成对应的class文件,即字节码文件,那什么是字节码文件呢?------->>也就是由Java编译器用javac命令编译、运行在JVM上的一种8位二进制字节流,这些字节流数据里包含了java类所有的相关信息,如常量池,接口,父类,子类信息等,而字节码文件再通过java虚拟机的解释,就将字节码编译成特定机器上的机器码。
Java程序运行的过程中,JVM会将其所管理的内存划分成若干个区域,统称为是运行时数据区。其中,一些线程间共享的区域,随着JVM的启动而创建,JVM的退出而销毁;另一些线程私有的区域,则随着线程的开始而创建,线程的结束而销毁。如图所示,运行时数据区由以下几个区域所组成:程序计数器、Java虚拟机栈、本地方法栈、方法区、堆。
先上一张图简单说明下JVM的内存模型:
(Ps:此图来源于网络 侵删)
注:堆和方法区是所有线程共有的,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。
二,程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,可以忽略不计,通俗地讲:就是在线程中指示字节码应当执行的位置 或者说在当前线程中执行字节码的行号指示器,JVM工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,另外,因为CPU需要不断的切换各个线程,这样来回切换以后 ,需要有计数器来保存各线程最初执行的位置,切换回来后便知道从哪开始执行。这也是为什么每条线程都有一个独立的程序计数器的原因。所以,我们称这类区域为“线程私有”的内存。
下面 我们在idea上用jclasslib插件 将class文件反编译成我们能看懂的文件来帮助我们理解程序计数器 如图:
三,虚拟机栈
与程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期与线程相同,描述的是Java方法执行的内存模型:每执行一个方法时,都会同时创建一个栈帧(Stack Frame)用来存储局部变量表,操作数栈,动态链接,方法出口等信息。同时,方法调用结束也对应着相关栈帧的出栈,释放在栈中的空间,空间结构如图所示:
(一),局部变量表(LVT)
1.1概念
是一个索引以0开始的字节数组,存储了一个方法的所有形参和局部变量。LVT所存储的类型都是编译期间可知的,包括各基础的数据类型(byte,short char float double等),对象引用(reference类型)
1.2LVT(局部变量表)具有的特点:
<1>第0个槽位(Slot)固定存储指向方法对象的this指针(注: 变量槽是Lvt最基本的存储单元)
<2>除了long和double 占用两个连续的Slot外,其他的类型只占用一个Slot
<3>局部变量表按照变量的声明顺序来进行存储
通过如下代码进行验证:
class Lvt{
public int showLvt(byte b,char c, int i, long l){
float f=0;
double d=0;
Object ref=new Object();
int ret=i;
return ret;
}
}
编译源文件后 在terminal终端 用javap -verbose Lvt.class #解析.class文件
1.3 解析局部变量表
在解析出的class文件中 我们可以看到 局部变量表(LocalVariableTable) 所展示的信息:如下
通过上图的结果,我们可以分析变量名所对应的签名(Signature)和Slot(槽位) 如第0个Slot的名字为this指针 ,其签名为 L作业/Lvt 表示是指向Lvt类型对象的this指针
(二)操作数栈(OS)
2.1操作数栈的概念
用于存储方法运算过程中所产生的结果或赋予局部变量的值,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中
2.2 解析操作数栈工作原理
JVM提供了对OS的出栈和入栈的命令,如load表示入栈指令,store表示出栈指令,下面编写一段代码帮助大家理解
public int showOs(int a, int b) {
int c = a + b;
return c;
}
编译源文件后 在terminal终端 用javap -verbose Lvt.class #解析.class文件
在解析的文件中找到对应的函数处(showOs() ) 如图所示
对以上解析文件进行解读:iload_1: 将局部变量表中Slot索引为1的变量a对应的值送入操作数栈,iload_2: Slot索引为2的变量b也送入操作数栈中;iadd:表示将两个数出栈相加后结果入栈,istore_3: 表示将得到的结果送入到局部变量表 变量c的位置处 iload_3:表示将索引为3的c对应的值入栈(OS) ;最后ireturn:表示方法的返回,结束操作
OS工作的原理图(对应解析文件):
(Ps 此图来源于网络 侵删)
(三)动态链接(Dynamic Linking):
3.1概念
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
class Cpr{
private int a=0;
public void cpr1(){}
public void cpr2(){
int b=a;
cpr1();
}
}
编写如上的代码 ,采用与操作数栈同样的方法对class源文件进行解析,如下图所示:
从中我们可以发现cpr2()函数的指令码中 使用到了两处符号引用#7 #13 ,对应的在常量池查找对应的符号 :
即#7代表的是字段的符号引用 #13代表的是方法的符号引用 最终分别解析成指向整型变量a,指向无返回值的cpr1()函数
四,本地方法栈(Native Method Stack)
本地方法栈 的作用与Java虚拟机栈类似,区别在于后者是为Java方法服务,而本地方法栈则为native方法服务。Java虚拟机规范没有对native方法机制及其实现语言做强制规定,如果JVM不提供native方法,则无需实现本地方法栈。
本地方法栈既可以被实现成固定大小,也可以实现成可动态地扩展和收缩,因此在特定的场景下也会抛出StackOverflowError
异常和OutOfMemoryError
异常。
五,方法区(Method Area)
方法区,与Java堆一样是各个线程共享的内存区域。用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范将方法区描述为堆的一个逻辑部分,但是它却有个别名叫做Non-Heap(非堆),目的就是和Java堆区分开来。运行时常量池也是存放于方法区中。
在Java8中,HotSpot虚拟机改变了原有方法区的物理实现,将原本由JVM管理内存的方法区的内存移到了虚拟机以外的计算机本地内存,并将其称为元空间(Metaspace)
下面用一张表来说明方法区中的存储的类的哪些信息
1、类型信息 | 类的完整名称 类的直接父类的完整名称 类的直接实现接口的有序列表 类型标志(类类型还是接口类型) 类的修饰符(public private defautlabstract final static) |
2、类型的常量池 | 存放该类型所用到的常量的有序集合,包括直接常量(字符串、整数、浮点数)和对其他类型、字段、方法的符号引用。 |
3、字段信息(该类声明的所有字段) | 字段修饰符(public、peotect、private、default) 字段的类型 字段名称 |
4、方法信息 | 方法信息中包含类的所有方法。 方法修饰符 方法返回类型 方法名 方法参数个数、类型、顺序等 方法字节码 操作数栈和该方法在栈帧中的局部变量区大小 异常表 |
5、类变量(静态变量) |
6、指向类加载器的引用 |
7、指向Class实例的引用 |
8、方法表 |
9、运行时常量池(Runtime Constant Pool) |
六,堆区
(一)堆的概念
堆(Heap)是运行时数据区中最大的一块区域,绝大部分的对象(包括类实例和数组)都在上面存储。堆是所有线程共享的,随着JVM的启动而创建。我们通过new
创建出来的对象都分配于此,而且无需主动释放对象内存,统一由垃圾收集器(Garbage Collector,GC)来进行管理和销毁 —— 这也是Java跟C++相比区别最大的特点之一。当堆中没有足够的内存来创建对象时,就会抛出OutOfMemoryError
异常
(二)堆的分代管理
从上图分析 ,堆被分为年轻代、老年代两个区域,年轻代还被进一步划分为 Eden 区、From Survivor 0、To Survivor 1 区。并且默认的虚拟机配置比例是Eden:from :to = 8:1:1 。
Java堆 = 老年代 + 新生代 新生代 = Eden + S0 + S1 默认Eden:from :to = 8:1:1
(三)有关堆的垃圾回收机制(GC)
Eden区(存放新生对象),两个幸存区(From Survivor和To Survivor)(存放每次垃圾回收后存活的对象)
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC。 对在Eden区的无效的对象(即堆中的对象没有被栈中的变量引用时)进行回收;将仍存活的对象移到From Survivor区 ,此时Eden区空间清零。
经过 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor区中每熬过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。
Ps:初学Java,有什么问题的地方,还希望大佬们给我指正下!