JVM(一)——运行时数据区

前言

什么是JVM,这个问题似乎到现在都没有一个确定的概念,问了很多工作多年的老司机,几乎每一个人有每一个人的理解,有的说是操作字节码的玩意,有的说是位于程序和操作系统之前的一层,有的说如果需要调优就要用到这玩意。但是从我个人来说,工作中似乎很少直接和它打交道,但是我们写的每一行代码,如果想要找到优化空间,似乎都避不开这个玩意,如果想对Java有一个更深刻的理解,JVM开始依旧是无法回避的一个坎儿。

什么是JVM

从一个大牛博客中找到的图如下,其实这个图已经从一定程度上说明了JRE,JDK和JVM三者的关系。JVM依旧是最底层,是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的,从这种层度上来讲,JVM为我们屏蔽掉了底层操作系统之间的差异,所以才会提出了一次编写,到处运行的口号。(N多种理解,或许这种理解最为合适吧)

 学习JVM最好的方式还是从运行时数据区开始,之后再去探讨类加载机制与class字节码,这一系列博客会尽力用通俗的方式解释JVM。下面附上一个很常见的图(JVM的整体逻辑构造图)

运行时数据区

其实,从我们平时写的代码来看,无非就是三个元素:数据,指令和控制。数据——就是我们定义的各种变量,指令——就是我们对数据的各种运算,控制——就是我们对程序逻辑走向的控制语句(如if...else,return等)。当类加载系统加载了Class文件之后,我们所编写的程序在计算机中的实时状态是啥样的?这个状态就在运行时数据区中都有体现,每一个区域都存放着当前程序运行的数据,所以名称就叫做运行时数据区。这个数据区描述了程序运行的实时状态。上图中已经将这一部分详细的表述出来了,其中虚拟机栈,本地方法栈,和程序计数器是每一个线程私有。堆和方法区是线程共享。下面就详细说明一下各个模块的作用(尽量会用一些通俗的语言)

程序计数器

线程私有,如果能记起来汇编语言的大学本科内容,其实可以和指令寄存器做一个类比,这个程序计数器的功能和指令寄存器类似。程序计数器可以看作是当前线程所执行的字节码的行号指示器(程序运行的下一条指令在哪儿?下一步走到哪儿,都需要程序计数器指示)由于在CPU中独立运行的最小单元是线程,不是进程,因此程序计数器自然是线程私有的。不可能多个线程共享,如果共享那不就乱套了。再者,由于CPU是时间片轮询的方式来执行线程指令,在任何一个确定的时刻,一个处理器都只会执行一个线程中的指令。因此,有时候为了线程切换之后能恢复到之前代码执行的位置,每条线程需要有一个独立的程序计数器(这一点类似于中断的概念,同时也解释了为啥程序计数器是线程私有的)。

虚拟机栈(JVM Stack)

线程私有,这个是这篇总结博客中的重点,也是JVM中最为复杂的一个内存区域。这个区域的生命周期和线程相同,这个东西也是描述Java方法执行的内存模式,每次调用一个方法,就会想这个栈中压入一个元素。

/**
 * autor:liman
 * createtime:2019/11/19
 * comment:
 * 数据,指令,控制
 */
public class RuntimeDataArea {

    private static void methodTwo(int i) {
		int j=0;
        int sum = i+j;
    }
	
	public static void main(String[] args) {
        methodTwo()
    }
}

比如这个程序,在程序运行的时候, 虚拟机栈中的状态可以用如下逻辑图表示。

每一个栈帧就是一个方法调用,每一个栈帧又会存储这个方法调用的局部变量表,操作数栈,动态链接和返回出口等等信息。这个接下来就详细探讨。每一个方法调用直至执行完成,就对应这个一个栈帧的入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverFlowError的异常,如果虚拟机栈可以动态扩展(一般都会) 如果在扩展的时候超过了内存大小的限制,就会出现OutOfMemoryError的异常(OOM)

局部变量表

局部变量表是一个定长的数据表,存放了编译期间可以知道的基本数据类型(八大基本数据类型,boolean,byte,char,short,int,float,long,double)已经引用类型(reference)。由于局部变量表是定长的,所以float和double会占用两个局部变量空间。下面还是通过实例来说明局部变量表。

通过javap命令(这个命令后面会总结)反编译上述Java类生成的class文件,得到的methodTwo的字节码如下所示:

  public void methodTwo(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: iconst_0
         1: istore_2
         2: iload_1
         3: iload_2
         4: iadd
         5: istore_3
         6: return
      LineNumberTable:
        line 32: 0
        line 33: 2
        line 34: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/learn/jvm/RuntimeDataArea;
            0       7     1     i   I
            2       5     2     j   I
            6       1     3   sum   I

ps:在生成这个反编译的文件的时候,也有些坑。在自己的dos窗口下生成的时候,不知道怎么的,就是没有局部变量表,查了很多资料都没解决,后来发现是用的javac好像不同,导致生成的class文件中缺失了这一段,无奈就在idea中调用terminal窗口生成了上述文件,这里需要注意的是,在生成这个文件的时候需要注意package的名称。

指定类的全限定名称之后才会正常生成指定的反编译文件。这里弄个动图简单说明这一点。 下面我们开始分析生成的反编译文件。

先分析第一行:iconst_0——这个是将0入栈,放到栈顶(这里的栈就是指的操作数栈,接下来会总结),这个时候,操作数栈和局部变量表如下所示:

局部变量表,第一个变量存放的是this对象的引用,第二个对象存放的是入参——i 

 这个时候,只是简单的将0进行了入栈,下一步就是将0赋值给j

 istore_2——将操作数栈中的栈顶元素存放到第三个本次操作数变量中,这个时候,操作数栈就没有元素了,局部变量表就变成如下所示:

iload_1,iload_2,iadd,istore_3——这几句操作就是完成简单的i+j运算,将局部变量表中的变量加载到操作数栈中,然后iadd命令会调用简单的加法操作,完成数据运行,最后将运算结果出栈,然后存储在局部变量表中。完成这些操作之后,局部变量表会多一个sum的局部变量。

 完成运算之后的局部变量表。

至于局部变量表中的引用对象,其实就可以简单理解成一个指针,指向堆上分配的对象。 

操作数栈

通过上面的实例,我想操作数栈是干什么的,应该有了一个大致的认识,就是用于存放临时操作的变量。这里就不再赘述,结合上面的实例理解一下就可以。

动态链接

这个区域有些资料上介绍的比较少,其实动态链接常用来解析常量池以及正常方法返回和异常处理等。这么说可能有点抽象,还是根据实例来说明吧,如果上述的methodTwo方法中我们调用了某一个接口的方法,如下所示:

public class RuntimeDataArea {
	
	private ITestInterface iTestInterface

    public void methodTwo(int i) {
		int j=0;
        int sum = i+j;
		iTestInterface.test();//这里调用ITestInterface接口中的方法
		return;
    }
	
	public static void main(String[] args) {
        
    }
}

那这个时候,我们需要解析到系统为我们注入的ITestInterface的具体的实现类,这个类就需要靠动态链接的区域去访问常量池,从常量池中获取具体的接口实现类,完成方法的调用(这就是多态,在运行时确定具体的实现类,这也只是简单的一个理解动态链接的例子) 

出口

这个就是方法调用完成之后的返回出口。没什么可以具体探讨的。

本地方法栈

与虚拟机栈作用差不多,只不过调用的是native方法而已。

所有线程共享,这个区域可能每一个Java程序员都听过,堆是Java虚拟机内存中最大的一块。堆是Java垃圾收集器管理的主要区域。在虚拟机启动的时候会创建,在JDK1.7时,通常将堆分为新生代和老年代,在JDK1.8之后,老年代被取消,由所谓的元数据区来代替。对象实例以及数组都会在堆上分配,一张图可以表述堆的划分。

方法区

所有线程共享 ,用于存放已经被虚拟机加载的类信息,常量,静态变量。

运行时常量池

我们常说的常量池其实就是位于方法区,class文件中除了类的元信息(类的限定名,类实现的接口等)之外,存放编译器期间生成的各种字面量和符号引用,这些内容都是在类加载之后存放到方法区的运行时常量池

总结

本篇博客算是JVM的开篇,只是针对各个存储区域做了一个简单的介绍,总体来说依旧有点泛化,后续对JVM有些更深的理解了之后,会重新补充本篇博客。

发布了129 篇原创文章 · 获赞 37 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/103149488
今日推荐