java运行时数据区域的划分与知识点(java面试必备)

运行时数据区域

java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
下图是jdk8后的JVM内存布局,引用于https://www.cnblogs.com/czwbig/p/11127124.html
在这里插入图片描述
jdk8之前的JVM内存布局,来源如上
在这里插入图片描述

1.1 程序计数器

Program Counter Register

程序计数器存储的值分为两种情况:

  1. 执行java的方法时,程序计数器存储下一条需要执行的字节码指令的地址
  2. 执行native方法时,程序计数器为空(undefined),native方法是java调用非java实现的代码,这部分的代码java只负责调用,无法生成字节码交给jvm,native方法的执行过程和内存管理由jvm所在的原生平台负责,jvm不负责

下图对java方法和native方法的内存管理做了区分
在这里插入图片描述

每一个线程都有一个程序计数器,方便线程切换后能恢复到上一次运行的位置.
jvm加载class文件时已经知道了程序计数器的最大偏移量,所以程序计数器不会抛出
OutOfMemoryError异常(可以理解为程序计数器在class中的字节码中左右反复横跳,但不会跳出已知的区域)

1.2 java虚拟机栈

Java Virtual Machine Stacks
java虚拟机栈是线程私有的,生命周期与线程相同。
虚拟机栈描述的是java方法执行的内存模型,每个方法在开始执行前会创建一个栈帧(Stack Frame),并推入虚拟机栈,方法执行结束后则出栈,可以把栈帧理解为一个java方法运行时在内存中的一个实体映射。
java虚拟机规范中规定了java虚拟机栈的两种异常:线程请求的栈深度大于虚拟机允许的深度(栈帧太多,即方法层级调用过深),会抛出StackoverflowError;若虚拟机栈允许动态扩展,但无法申请到足够的内存,抛出OutOfMemoryError
栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址(returnAddress类型,指向了一条字节码指令的地址)、附加信息等信息。这里可以跳到书的第八章了解栈帧的结构
在这里插入图片描述

栈帧的各个信息描述如下:

  1. 局部变量表
    局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。
    局部变量表的容量以变量槽(Variable Slot)为最小单位,slot的大小根据操作系统、处理器、JVM的不同有所差别,虚拟机规范中说每个slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据
    虚拟机会以高位对其的方式为64位的数据类型分配两个连续的slot,不允许单独访问两个slot的任意一个,否则会抛出异常
    按照索引访问局部变量表,对于实例方法,index=0的slot默认是方法所属对象实例的引用(this)
    slot可以复用,方法中的变量的作用域不一定是整个方法,在离开变量作用域后,对应的slot可以给其他变量使用。
    slot复用可能带来的内存回收问题见另外的文章https://blog.csdn.net/qq_36267931/article/details/106822588

    局部变量如果不初始化则无法使用。

  2. 操作数栈
    Operand Stack
    方法刚开始执行时,操作数栈是空的,方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,执行入栈/出栈操作(学过计组的应该对这个很好理解)
    操作数栈的最大深度在编译时写入到Code属性的max_stack数据项中,操作数栈的每个元素可以是任意的java数据类型,包括long double

  3. 动态链接
    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接
    关于静态分派与动态分派可以参考https://blog.csdn.net/sunxianghuang/article/details/52280002

  4. 方法返回地址
    方法执行结束有两种方法,一种是正常返回,如果有返回值则把返回值传给上层方法,另一种是发生异常,而且方法中没有对应的异常处理器,此时不会有返回值。
    正常返回的情况下,方法返回地址存储的是方法执行结束后程序要执行的下一条字节码指令的地址,一般是方法调用者的PC计数器的值
    异常返回的情况下,返回地址通过异常处理器来确定,栈帧中一般不会保存这部分信息

  5. 附加信息

    附加信息里是一些规范里没有描述的信息,取决于具体的虚拟机实现

1.3 本地方法栈

Native Method Stack
本地方法栈与虚拟机栈的作用非常类似,虚拟机栈为虚拟机执行java方法,本地方法栈为虚拟机使用到的本地方法服务,本地方法与java 方法的差别可以见 1.1小节的图
本地方法栈也会抛出StackoverflowError、OutOfMemoryError

1.4 java堆

Java Heap
java堆被所有线程共享,用于存放大部分对象实例。
Java堆是垃圾收集器管理的主要区域,所以也叫GC堆。
从内存回收算法来看,堆可以分为新生代和老年代,从内存分配角度看,线程共享的java堆可能划出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
java堆相关的知识后续再写文章补充

1.5 方法区

Method Area
jdk8之前才有的内存区域,用于存储已被虚拟机啊加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,是线程共享的区域
使用HotSpot虚拟机的开发者,更愿意把方法区成为永久代(Permanent Generation)。因为HotSpot的设计团队用永久代来实现方法区,使得垃圾收集器可以像管理java堆一样管理方法区,但这样的实现由性能和内存溢出的问题,且其他虚拟机不存在永久代的说法,所以在jdk8后不再有方法区

JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。为什么要使用元空间取代永久代的实现?

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  4. 将 HotSpot 与 JRockit 合二为一。

1.6 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

1.7 直接内存

Direct Memory
直接内存不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。
JDk1.4后加入了NIO类,它可以直接使用Native方法分配堆外内存(直接内存),然后通过存储在java堆中的DirectByteBuffer对象对直接内存进行操作,使用直接内存可以避免数据在java堆和堆外内存中来回切换,进而提高性能。
关于直接内存可以参考https://www.jianshu.com/p/007052ee3773

参考

《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值