JVM之栈、堆、方法区(三)

一、CPU和内存的交互

今天除夕,祝大家新年快乐,其实,我们知道的,我们的CPU跟内存会有非常频繁的交互,因为如果这个频繁的交互是交给我们的磁盘的话,那么随着我们的CPU运转速度越来越快,那么我们的磁盘的读写性能远远跟不上我们的CPU读写的速度,哪怕是我们现在的SSD,固态硬盘,也仅仅只是减少了我们的寻道时间以及加快了我们的找数据的时间。所以,我们才会在我们磁盘的基础上设计了内存,用来解决我们的单次IO时间过长导致我们CPU的等待成本过大的问题.但是随着我们CPU的发展,我们CPU的性能越来越高,哪怕就算是我们的内存的读写速度都跟不上我们的CPU的读写速度.因此,这个时候,我们的CPU厂商就想了个办法:在我们的每颗CPU上都加入了高速缓冲区,用来加快我们的读写速度,于是乎,我们的CPU跟我们的内存的交互就演变成为了这样子的一张图片.
但是,根据摩尔定律,我们的IC芯片每隔18个月能容纳的晶体管会翻倍,但是我们的毕竟不可能不限制的增长,单核CPU的主频也有性能瓶颈,想要提升性能,必须增加多个运算核心.所以,随着时间的增长,我们的多核时代来临了.
基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存(下文成主存,main memory 主要内存),当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象。为了解决这一问题,需要各个处理器运行时都遵循一些协议,在运行时需要通过这些协议保证数据的一致性。比如MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。那么怎么做的呢?
而我们的运行时数据区其实也保有了这样子的一种设计.其实参照这种设置,我们已经能够推到出我们的JVM是如何跟我们的内存还有我们的CPU交互的了.java中使用的是多线程机制,那么必然会有多个任务同时执行,这个时候类比了我们的CPU运算核心,那么必然会有一块区域或者一种操作能够保证我们数据的一致性,那么我们的JVM内存中数据存放的部分必然会是所有线程都能够获取到的,那么就可以称之为线程共享,而每个线程又有自己单独的工作内存,当我们线程进行运作时,数据肯定会从JVM主存拷贝到线程自己的工作内存,然后再进行操作.

二、常量池

常量池分为我们前面所说过的静态常量池,运行时常量池,还有字符串常量池,那么其实我们的运行时常量池又是什么呢?

2.1、静态常量池

其实储存的就是字面量以及符号引用

2.2、运行时常量池

运行时常量池就是我们的每个类以及每个接口在我们的JVM进行run的过程中所在内存中开辟出来的一块用来储存我们静态常量池部分数据的一块特殊区域。

2.3、字符串常量池

包含在动态常量池里

2.4、JDK1.8中各常量池在内存中的划分

 三、运行时数据区(Run-Time Data Areas)

在装载阶段的第(2),(3)步可以发现有运行时数据,堆,方法区等名词(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口,说白了就是类文件被类装载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数据得要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)

3.1、官网概括

 

 3.2、jvm的运行时数据区

 

 3.2.1、Method Area(方法区)

(1)方法区只有一个,方法区是各个线程共享的内存区域,在虚拟机启动时创建
(2)虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来
(3)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
(4)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
此时回看装载阶段的第2步,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构如果这时候把从Class文件到装载的第(1)和(2)步合并起来理解的话,可以画个图
 

 

 3.2.2、Heap(堆) 

(1)Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。 
(2)Java对象实例以及数组都在堆上分配。 
(3)生命周期与虚拟机一样长
此时回看装载阶段的第3步,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口 ,此时装载(1)(2)(3)的图可以改动一下 

 3.2.3、Java Virtual Machine Stacks(虚拟机栈)

经过上面的分析,类加载机制的装载过程已经完成,后续的链接,初始化也会相应的生效。 假如目前的阶段是初始化完成了,后续就是要使用了,那怎样才能被使用到;例如通过主函数main调用其他方法,这种方式实际上是main线程执行之后调用的方法,即要想使用里面的各种内容,得要以线程为单位,执行相应的方法才行。
(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。 
(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。 
图解栈和栈帧
void a(){
b();
}
void b(){
c();
}
void c(){
}

栈帧
栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(Areference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
  • 局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
  • 操作数栈:以压栈和出栈的方式存储操作数的 
  • 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
  • 方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
结合字节码指令理解栈帧 
public static int calc(int, int);
 Code:
 0: iconst_3 //将int类型常量3压入[操作数栈]
 1: istore_0 //将int类型值存入[局部变量0]
 2: iload_0 //从[局部变量0]中装载int类型值入栈
 3: iload_1 //从[局部变量1]中装载int类型值入栈
 4: iadd //将栈顶元素弹出栈,执行int类型的加法,结果入栈
 5: istore_2 //将栈顶int类型值保存到[局部变量2]中
 6: iload_2 //从[局部变量2]中装载int类型值入栈
 7: ireturn //从方法中返回int类型的数据
 ... 

 

 

 3.2.4、The pc Register(程序计数器)

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。 

3.2.5、Native Method Stacks(本地方法栈)

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。那如果在Java方法执行的时候调用native的方法呢?
除了上面五块内存之外,其实我们的JVM还会使用到其他两块内存
  • 直接内存(Direct Memory)
并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存的大小及处理器寻址空间的限制。因此在分配JVM空间的时候应该考虑直接内存所带来的影响,特别是应用到NIO的场景。
  • 其他内存: 
Code Cache:**JVM本身是个本地程序,还需要其他的内存去完成各种基本任务,比如,JIT编译器在运行时对热点方法进行编译,就会将编译后的方法储存在Code Cache里面;GC等功能。需要运行在本地线程之中,类似部分都需要占用内存空间。这些是实现JVM JIT等功能的需要,但规范中并不涉及。 

3.3、演示说明

 3.3.1、栈指向堆

如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象。

 3.3.2、方法区指向堆

方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。
private static Object obj=new Object();

 3.3.3、堆指向方法区

 既然方法区可以指向堆,那么堆能否指向方法区?
答案是可以的,不过这个不方便代码描述。试想一下,方法区中会包含类的信息,堆中会有对象,那怎么知道对象是由哪个类创建的呢?所以,在对象的对象头中会有一个指针,用来指向方法区对应的类元数据信息
 
posted @ 2022-01-31 07:31  童话述说我的结局  阅读(296)  评论(0编辑  收藏  举报