java虚拟机的二三事之运行时数据区

运行时数据区是jvm比较重要的一部分,它主要包括方法区、堆、程序计数器、虚拟机栈、本地方法栈,可以根据是否是线程私有将它们分成两部分,其中方法区跟堆是线程共享的,其他三个是线程私有的。
画个图更有助于记忆运行时数据区的结构。
在这里插入图片描述
接下来我们挨个介绍一下它们,让大家能够了解每块区域的功能。我们先从线程私有的这几块区域开始说起。

  • 程序计数器
    在《深入理解java虚拟机》这本书中是这样对程序计数器定义的:

一块较小的内存空间,他的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅仅是概念模型,各种虚拟机可能会有更高效的实现方式),字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个计数器来完成。

总结来说它是线程执行字节码的行号指示器。字节码又是什么呢?我们可以通过javap -c命令来对class文件进行反编译,得到的就是字节码,如下所示:

C:\Users\98465>javap -c G:\workspace\xgq\bin\com\langsin\JVM\testJVM.class
Compiled from "testJVM.java"
public class com.langsin.JVM.testJVM {
  public com.langsin.JVM.testJVM();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static int getSum(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: iadd
       3: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: bipush        12
       2: istore_1
       3: bipush        21
       5: istore_2
       6: getstatic     #21                 // Field java/lang/System.out:Ljava/io/PrintStream;
       9: iload_1
      10: iload_2
      11: invokestatic  #27                 // Method getSum:(II)I
      14: invokevirtual #29                 // Method java/io/PrintStream.println:(I)V
      17: return
}

行左边的数字就是他们的行号,程序计数器中记录的值就是这些行号。不过如果是执行的是native方法,这个计数器则为空。分支、循环、跳转、异常处理、线程恢复等基础功能都是通过这个计数器来完成。

  • 虚拟机栈
    虚拟机栈的生命周期与线程相同的,它描述的是java方法执行的内存模型:每个方法被执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从被调用到执行完成的过程对应着一个栈帧从入栈到出栈的过程。
    JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,典型如一个无结束条件的递归函数调用。若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。
    下面举个例子来详细解释一下虚拟机栈。
public class Calculator {

    public  int getSum(int m,int n){
        return m+n;
    }
    public static void main(String[] args){
    		int m = 1;
    		int n = 2;
    		Calculator  c = new Calculator();
    		c.getSum(m,n); 
    }
}

这是一个单线程的程序,我们一运行main线程,java就为它分配一块栈内存区域,我们运行了main方法,所以在这块栈内存区域中添加一个main方法对应的栈帧,然后执行到getSum方法后,又在栈内存区域添加一个getSum方法对应的栈帧。当getSum方法执行完成后,就销毁其对应的栈帧,然后回到main方法,等main方法执行完后就销毁main方法的栈帧。其结构如下图所示:
在这里插入图片描述
栈帧也有其结构,其中存放着局部变量表、操作数栈、动态链接、方法出口等。
我们以main方法为例,在main方法中有m、n以及c三个局部变量,由于变量c是一个引用数据类型,所以它在局部变量表中的值是此对象在堆中的地址,而变量m以及n都是直接将值也存放在局部变量表中。即下图所示:在这里插入图片描述
至于动态链接,就是将符号引用在程序运行过程中转变为直接引用,这个今天我们先不细说,以后有机会再拿出来慢慢分析。
而方法出口就是当getSum方法执行完后,jvm通过获得方法出口中存放的字节码的行号来确定接下来该执行哪一行字节码。

  • 本地方法栈
    本地方法栈与虚拟机栈类似,虚拟机栈描述的是java方法执行的内存模型,而本地方法栈则是为虚拟机用到的native方法服务。

讲完了线程私有的这几部分之后,我们再来说一下共享的这两部分区域,这里主要要介绍的就是堆,这里涉及到的知识就比较多了,可能提到堆,大家首先想到的是它是存放对象实例的地方。另外,它也是垃圾收集器管理的主要区域。想知道对象在这块区域是如何存放以及管理的吗?继续往下看吧。


  • 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。从内存回收的角度来看,它还可以细分为新生代与老年代,默认情况下新生代与老年代的比例为1:2,我们可以通过参数-XX:NewRatio来调整它俩的比例。新生代再细分为Eden区域、From Survivor区、To Survivor区。
    一般情况下我们新生成的对象会先放在新生代,如果是比较大的对象会直接放进老年代。经历过多次垃圾回收并存活下来的对象也会放进老年代。新生代与老年代用的垃圾收集算法是不同的,这是由垃圾回收算法以及不同区域的特点决定的。
    由于新生代的对象存活率较低,所以比较适合复制算法,而老年代由于对象存活率比较高,采用的是标记清除或者是标记整理算法。至于如何判定对象是否可回收以及不同垃圾回收算法、垃圾收集器的特点等下次细说。另外jvm参数调优主要也是围绕着堆内存进行的,其最基本的原则就是减少垃圾回收的次数。

  • 方法区
    它是用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值