本篇文章由概念以及一些疑问引入,写出了我对Java各大区域的理解,以及一个方法在执行的时候和他们有什么关系。

Java内存区域组成及概念


  • JVM在启动的时候向系统要了一块很大的内存区域,这个大内存分为五个区域:方法区、堆区、虚拟机栈、本地方法栈、程序计数器,下面补充一些他们的概念
  • 堆区:通俗的理解Java new出来的对象放在这个地方
  • 本地方法栈:Java利用JNI调用其他语言程序的时候,运行的区域。
  • 方法区:讲到方法区,这里补充一下方法区、元空间、永久代的关系,这里可以把方法区理解为一个接口(JVM的一个规范),而元空间和永久代是这个接口的一个实现,永久代只出现在jdk1.8以前的版本,jdk1.8以及以后永久代被移除了,元空间使用本地内存,永久代使用堆内存
  • 虚拟机栈:JVM是软件模拟出来的虚拟机,个人感觉他的操作参考了计算机的一些运行过程,比如用到了程序计数器(虽然跟真正的程序计数器完全不同),每个线程都对应了一个虚拟机栈,然后栈又由栈帧组成,一般一个栈帧对应了一个方法。
  • 栈帧由1. 局部变量表2.操作数栈 3. 动态链接 4. 返回地址5.附加信息 五个部分组成。
  • 程序计数器:这里指的是字节码的索引

补充栈帧组成的概念


动态链接

我的理解是:对于每一个栈帧,都保存了该方法对应的JVM对象在元空间的内存地址,返回地址下面会详细讲,这个大概讲一下,就是,假如有两个方法,正在运行A方法,而此时运行到某一处需要运行B方法,这时候就要新增一个栈帧,这个返回地址存放的就是A方法的地址,因为我运行完B方法之后需要返回到A方法并且恢复现场,而JVM的实现就是在B方法的栈帧中把A方法的地址存了

局部变量表

某一个方法中存放局部变量用的表,这个局部变量表存放在Java的栈中的栈帧里面。

操作数栈

  • 操作数栈是栈帧的一部分,保存的是程序在运行时生成的一些值
  • 首先说明方法是静态的,方法在运行的时候才会产生栈帧,操作数栈在运行时是动态的
  • 这里初步简略讲解一下字节码,首先用jclasslib打开下面的那个例程

java项目边界安全 java安全区_方法区

  • 比如前面的索引为0那里执行了new #2
  • 在堆区生成了一个对象(不完全对象,此时还没执行构造方法)
  • 将不完全对象的指针压入栈中,这个指针指向堆区
  • 3 dup那条指令
  • 复制栈顶元素,目前的栈顶元素是那个不完全对象
  • 压入栈
  • 这里解释一下为什么需要
  • invokespecial #3 <Simple.>
  • 这是一个非静态方法,第一个参数是this指针
  • 执行invokespecial字节码指令(一般用于执行构造方法,普通方法,父类方法),他完成的事情是完成运行方法的构建以及this指针赋值(这个this是那个对象的this指针),这个指针在哪呢,在class这个静态文件里面在每个方法的局部变量表的第0位,是一个符号引用,用来表示this指针,而实例化为对象的时候(也就是在方法new一个对象的时候),这个对象是依据方法区的Class的局部变量表生成的,这个时候要把对象的局部变量表的值从Class的符号引用换成真正的地址,那这个地址哪来的呢,由于是在这个方法里面new的对象(这也说明了程序方法的运行是在栈中),所以这个值的来源就是new这个对象的所在方法的栈帧中的操作数栈中,这个也就是第五点为什么要把新的对象的指针重新复制一遍然后压栈的原因,在给this指针赋值的时候需要把它从栈中取出来赋值
  • 执行完构造方法之后,那个不完全对象就变成了完全对象
  • 然后new完那个对象之后看到一个aload_1的指令,意思是把栈顶元素赋值给局部变量表里面index为1的变量,现在pop出来的栈顶元素,是在刚刚生成不完全对象的地方压进去的,这时候拿出来用

虚拟机栈方法的运行和现场恢复


  • 关于虚拟机栈的栈帧中的返回地址和恢复现场的步骤,结合一个例子详细说明:JVM运行add方法,内部是怎么做的
  • 创建add的方法的栈帧
  • 在add方法的栈帧中保存main方法的字节码的下一行程序计数器的内容(用jclasslib查看字节码的时候可以看到在main执行add是调用了一条指令,然后这条指令完了也就是调用add完了,需要执行main之后的指令,所以这时候先把这个值保存起来,保存的地方就是add的栈帧中,总之add执行到最后是可以通过这个值来找到main需要执行的地址,这里的地址是jvm自己定义的,字节码的地址)
  • 之前说到一个线程对应一个栈,一个方法对应一个栈帧,那除了栈帧本身需要字段来记录当前的运行状况,栈也需要有字段来记录当前线程的运行情况,比如线程的局部表开始指针和线程的操作数栈开始指针,这两个属性分别记录了当前运行方法的局部表开始指针和操作数栈开始指针。
  • 线程的局部表开始指针 和 保存到add方法的栈帧中(在main跳转到add之前是main的),这两个值之后会被用于恢复现场
  • 将add方法的局部表指针和操作数栈指针赋值给线程的操作数栈指针(真正执行操作的是线程,执行方法需要这两个表,所以把add这个当前执行的方法保存在线程里)
  • JVM运行main方法,内部是如何运行的(以上面那个图为例)
  • 创建运行main方法需要的栈帧
  • 将main方法的操作数栈指针赋值给线程的属性:操作数栈
  • 将main方法的局部变量表指针赋值给线程的属性:局部变量表

各区域的联系


  • 虚拟机栈指向方法区:这里就是上面说的动态链接
  • 虚拟机栈指向堆区:栈帧中局部变量表存放对堆区对象的引用
  • 方法区指向堆区:引用了堆区对象的静态属性
  • 堆区指向方法区:对象的内存布局中保存了对对象元信息也就是Class在方法区中的地址