JVM 之 运行时数据区域

学习JVM,首先我们需要了解 JVM 运行时数据区域。
我们根据下面两张图,简单的了解一下。


1870221-6a909e08252673d1.png
运行时数据区域

1870221-2e71723a91f93a65.png

组成部分:

  • 方法区 (Method Area)
  • 虚拟机栈 (VM Stack)
  • 本地方法栈 (Native Method Stack)
  • 堆 (Heap)
  • 程序计数器 (Program Counter Register)
程序计数器(PC)

如果我们学过计算机原理汇编语言等等,我们一定知道PC,没学过也没事,打个比方,就好比我们小时候读书的时候,老师拿着小棍子,指着黑板上的几行字,从上到下 :

->  1+1=?
    2+2=?
    3+3=?

指到哪里我们算那一道题。其实JVM的PC程序计数器也是一般。

【指向当前线程所执行的字节码的行号】,其实就是一小块内存,记录着当前程序运行到哪了字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成。

由于Java的多线程是通过线程轮流切换完成的,一个线程没有执行完时就需要一个东西记录它执行到哪了,下次抢占到了CPU资源时再从这开始,这个东西就是程序计数器,正是因为这样,所以它也是“线程私有”的内存。(《深入理解JVM》)


虚拟机栈(VM Stack)

每一个Java方法在执行的时候,都会同时创建一个栈帧,栈帧里存储着 局部变量表,操作栈,动态链接,返回地址,常量池引用等。


1870221-1cd18a3f013312fd.png
虚拟机栈

我们在执行Java方法的时候,入栈,执行完毕方法返回的时候出栈。因此虚拟机中的入栈顺序就是方法调用的顺序。

什么是栈帧呢?可以认为是方法的运行空间,这个空间主要由两部分组成

  • 局部变量表
  • 操作数栈
  • 方法返回地址
  • 动态连接

局部变量表:方法中定义的局部变量和方法的参数都在这个表里。

操作数栈:用来存放操作数。

方法返回地址:返回到方法被调用的位置地址。

动态连接:在 Class 文件中,描述一个方法调用了其他方法,或者访问其他成员成员变量的时候是通过符号引用来表示的,动态链接的作用是讲这些符号引用转成实际的直接额引用。这些符号引用一部分会在类加载的时候或者第一次使用的时候转化为直接引用,这类转化成为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分成为动态链接。(静态分派,动态分派)

Java的字节码指令的 操作数 存放在操作数栈中,当执行某条带N个操作的指令的时候,我们就从操作数栈顶取出N个操作数。然后把指令的计算结果入栈。因此当我们说JVM执行引擎是基于栈的时候,这个栈 就是操作数栈。(汇编是把指令放到数据段和寄存器中的)。正是因为是基于栈的操作,所以不必担心不同机器寄存器和指令的不同。

iconst_1 //把整数 1 压入操作数栈
iconst_2 //把整数 2 压入操作数栈
iadd //栈顶的两个数相加后出栈,结果入栈

不知道大家注意到一个问题没有?操作数栈是如何获取数据的,直接从局部变量中获取吗?这点大家一定要注意 !!!局部变量表中的变量是不能直接使用的,必须通过相关的指令将其加载到操作出栈作为操作数来处理。举个例子,让我们更加明白吧。

举个栗子:

Java方法

public void foo(){
int a = 1 + 2;
int b= a + 3;
}

编译成字节码

iconst_1 //把整数 1 压入操作数栈
iconst_2 //把整数 2 压入操作数栈
iadd //栈顶的两个数出栈后相加,结果入栈;实际上前三步会被编译器优化为:iconst_3
istore_1 //把栈顶的内容放入局部变量表中索引为 1 的 slot 中,也就是 a 对应的空间中
iload_1 // 把局部变量表索引为 1 的 slot 中存放的变量值(3)加载至操作数栈
iconst_3 //把整数 3 压入操作数栈
iadd //栈顶的两个数出栈后相加,结果入栈
istore_2 // 把栈顶的内容放入局部变量表中索引为 2 的 slot 中,也就是 b 对应的空间中
return // 方法返回指令,回到调用点

有一点需要注意:局部变量表以及操作数栈的容量的最大值在编译器就已经确定好了,运行时不会改变。局部变量表的空间也是可以复用和覆盖的。

局部变量表最小单位是Slot,用来存放方法参数和局部变量。局部变量表从0开始到表的最大索引。第0个指向当前对象的引用,也就是Java里的this。

这里又出现了一个问题,动态链接的引用数据类型具体有两中情况。这个有可能返回的是直接地址,也可能返回一个代表对象的句柄。


1870221-4f77cdaea0e55a60.png

Java虚拟机栈可能抛出以下异常:

当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。


本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。(《深入理解JVM》)


Java堆(Java Heap)是Java虚拟机管理的内存最大的一块。是被所有线程共享的一块。此内存区域的唯一目的是存放对象实例。

我们经常遇到的内存泄露,GC,都是发生在这里。我们将在另一篇中详细介绍这一部分内容。

稍有不注意可能就有OutOfMemoryError异常。


方法区

方法区也是各个线程共享的内存区域。它用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

这个区域的内存回收目标主要是常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻。

1870221-1ec5c9347b3ff9b0.png
image.png
一、类信息

1、类型信息

  • 类型的全限定名
  • 超类的全限定名
  • 直接实现的接口的全限定名数组
  • 类型标志(类还是接口)
  • 类的访问描述符 (public、private、default、abstract、final、static)

其实就是个身份证,记录这个类的所有信息。类叫啥,他爸叫啥,实现的接口有哪些?是类还是接口?访问描述符是啥子。

2、字段信息

  • 字段的修饰符
  • 字段的类型
  • 字段的名称

3、方法信息

  • 方法修饰符
  • 方法返回类型
  • 方法名
  • 方法参数个数 、类型 、顺序
  • 方法字节码
  • 操作数栈和该方法在栈帧中的局部变量区的大小
  • 异常表

4、类变量(静态变量)
该类所有对象共享的变量,即使没有任何实例对象时,也可以访问类的变量。他们和类进行绑定着。

5、指向类加载的引用
每一个被JVM加载的类型,都保存着这个类加载器的引用,类加载器动态链接会用到。

6、指向 Class 实例的引用
类加载的过程中,虚拟机会创建该类型的 class 实例,方法区中必须保存对该对象的引用,通过Class.froName(String className)来查找获得该实例的引用,然后创建该类的引用。

7、方法表
为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承方法。这个表在抽象类或者接口里没有。

总结一下 方法区已加载的类信息。这里存放着类的各种身份信息,还有他的父类名字,接口名字,类访问描述符,然后还要持有类实例的引用,供Class.froName(String className)调用。对于类里的类变量,字段,方法,都有详细的记录,

二、运行时常量池

用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中,具备动态性。(比如String的intern()方法)。

需要注意的几种常量池 ,希望大家不要混淆

1、全局字符串池

String pool 我们在使用 String 的时候经常会涉及这个字符串池。全局字符串池里的内容是在类加载完成,经过验证和准备阶段后在堆中生成的字符串对象实例,然后将该字符串实例的引用值放到 String Pool 中。在 HotSpot 中它的实例就一份,被所有类所共享。

在 JDK 6.0 之前String Pool里放的都是字符串常量。
在JDK7.0中,intern()发生了改变,String Pool中可以放置Java对象的引用。

2、class 常量池

  • Java类在编译后,会生成 class 文件,class 文件中除了类的信息外,还有一项就是常量池(constant pool table)用来存放编译器生成的各种字面量(Literal) 和符号引用(Symbolic References)。
  • 每个 class 文件都有一个 class 常量池。
1870221-e9d05e62e8e99486.png

3、运行时常量池

  • 运行时常量存在于内存中,也就是 class 常量池加载到内存之后的。不同的是 他的字面量可以动态添加(String.intern()),符号引用可以被解析为直接引用。
  • JVM 在执行某个类的时候,必须经过 加载,链接,初始化,链接包含验证、准备、解析三个阶段。当类加载到内存中后,JVM 就会将 class 常量池中的内容存放到运行时常量池中,所以运行时常量池也是每个类一个。

总结一下 :

  • 全局字符串在每个JVM中只有一个,存放的是字符串常量的引用(1.7之后)
  • class 常量池是在编译后的 class 文件里的。在编译阶段是符号引用
  • class 常量池中存放的是字面量和符号引用,而不是实例对象,在解析的过程中,需要查询全局字符串池,保证运行时常量池所引用的字符串域全局字符串池中的引用是一致的。

最后留一张宝图,回顾这篇文章,讲讲各个之间关系。看看是否掌握了。

1870221-a804223f9930bcff.png
价值千万的宝图

参考:
图解JAVA对象的创建过程
方法区(关于java虚拟机内存的那些事)
java虚拟机
http://www.importnew.com/26842.html
https://blog.csdn.net/ychenfeng/article/details/77247807
https://blog.csdn.net/u013412772/article/details/81051465

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值