虚拟机运行时数据区域概述

前言

Java虚拟机在执行Java程序的过程中,会把它管理的内存区域划分为多个不同的数据区域,这些区域各自的作用不同,存储的内容不同,生命周期也有差异,了解他们有助于帮助我们加深对Java虚拟机的认识。本篇文章主要参考周志明老师的《深入理解Java虚拟机》第三版,归纳总结相关知识点用以学习和总结,希望能够对大家也有帮助。

运行时数据区

提示:hotspot虚拟机将虚拟机栈和本地方法中合二为一

一、程序计数器

1.流程控制

程序计数器是字节码的行号指示器,我们知道Java虚拟机执行的是字节码文件(.class文件),如果线程执行的是一个Java方法,行号指示器记录的是当前线程执行的字节码指令地址,通过改变程序计数器来选取下一条需要执行的字节码指令。这样就可以达到控制程序流程的作用,例如控制程序的分支、循环、跳转、异常处理等。如果执行的是本地Native方法,计数器的值则为空(Undefined)。

2.线程私有

Java虚拟机的多线程是通过对线程轮流切换、轮流分配执行时间实现的,假如A线程执行之后切到了B线程,然后又需要切回A线程,这个时候A线程应该从哪里开始执行呢?所以每个线程都有一个单独的程序计数器,记录着当前线程应该执行的字节码指令位置,各个线程相互独立不影响,称为“线程私有”。

3.无OOM

程序计数器占一块比较小的内存空间,此区域是唯一一个在《Java虚拟机规范》中规定没有OutOfMemoryError情况的区域。

二、Java虚拟机栈

1.栈帧

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候虚拟机栈会创建一个栈帧,栈帧用来存储局部变量表、操作数栈、动态连接、方法出口信息等。每个方法被调用直到其结束的过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。关于栈帧的描述内容较多,下一篇专门单独的篇幅介绍。

2.线程私有

Java虚拟机栈也是线程私有的,生命周期与线程相同。

3.异常

虚拟机栈中存在着两类异常,如果线程请求的栈的深度大于虚拟机所允许的深度,抛出StackOverflowError;如果Java虚拟机栈容量可以扩展,扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError的异常。注意,HotSpot虚拟机栈的容量是不允许动态扩展的,所以线程申请栈空间成功就不会OOM,如果申请失败就会导致OOM。

三、本地方法栈

1.Native方法

本地方法栈的作用和虚拟机栈相似,区别是虚拟机栈执行的是Java方法,而本地方法栈执行的是本地方法Native。

2.线程私有

本地方法栈也是线程私有的,生命周期与线程相同。

3.异常

本地方法栈的异常和虚拟机栈一致,会出现StackOverflowError和OutOfMemoryError。

四、Java堆

1.线程共享

Java堆是虚拟机管理的最大的一块内存空间,由所有线程共享,在虚拟机启动的时候创建。虽然是线程共享的区域,但是也可以划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),用来提升对象分配时的效率。

2.存放对象

“几乎”所有的对象实例都放在堆中,但是随着Java语言的发展与目前逃逸分析技术的日渐强大,栈上分配、标量替换优化手段也导致了一些微妙的变化,所以说Java对象实例都分配在堆上的说法逐渐变得不那么绝对。关于逃逸分析与标量替换,另外的章节篇幅描述。

3.GC堆

Java堆是垃圾收集器管理的内存区域。堆中所谓的各个分区,新生代、老年代、永久代、Eden空间、From Survivor、To Survivor等这些概念的区域划分,仅仅只是“一部分”垃圾收集器的共同特性或者说设计风格而已,并非Java虚拟机具体实现的固有内存布局,也不是《Java虚拟机规范》中对Java堆的具体划分。HotSpot虚拟机的垃圾收集器也只是基于“经典分代”来设计的,HotSpot虚拟机如今也出现了不是以分代设计的垃圾收集器,所以固有的按照上述的分区来讲,有待商榷。关于分代的具体叙述,另外的篇幅描述。

4.异常及内存分配

堆的大小可以扩展,也可以按照固定大小分配。 -Xmx和-Xms设定。Java堆可以处于物理上不连续的内存空间中,在逻辑上应该被视为连续的。对于大对象,例如数组对象,多数虚拟机出于实现简单、存储高效的考虑,可能要求连续的内存空间。本地方法栈的异常和虚拟机栈一致,会出现StackOverflowError和OutOfMemoryError。

五、方法区

1.非堆(Non-Heap)

方法区也是线程共享的区域,用来存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器JIT编译后的代码缓存。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却又一个别名“非堆”,目的就是为了与堆进行区分。

2.永久代

永久代和方法区本质上是不等价的。历史缘由:期初HotSpot设计团队是想把垃圾收集器的分代设计扩展到方法区,或者说使用永久代来实现方法区,目的是为了让方法区像堆一样可以被垃圾收集器管理,省去方法区编写内存管理代码的工作量。但是永久代有内存上限-XX:MaxPermSize,不设置也有默认值,容易导致内存溢出。JDK6的时候设计团队就有想废弃永久代改用本地内存来实现方法区的打算;JDK7的时候,将永久代的字符串常量池、静态变量等移出;JDK8,完全废弃永久代的概念,用在本地内存(Native Memory)中实现的元空间(MetaSpace)来代替将。并且将JDK7中剩余的永久代的内容(主要是类型信息)全部移到元空间。

3.运行时常量池

Class文件除了有类的版本、字段、方法、接口等描述信息之外,还有常量池表,用来存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后放到方法区的运行时常量池。class文件格式和符号引用,将在另外的篇幅中描述。运行时常量池相对于Class文件常量池的另一个特征是“动态性”,意思是不一定非得一定是编译期间的常量,在程序运行期间的常量也可以置入常量池中,例如String的intern()方法。

4.GC和异常

《Java虚拟机规范》中对于方法区的约束非常宽松,除了可以像堆一样选择固定大小或者可扩展之外,一样可以不需要连续的内存空间,甚至可以不实现垃圾收集。相对而言,垃圾收集行为在这个区域比较少,此区域的内存回收目标主要是针对常量池的回收和对类型的卸载,但是回收效果强差人意,尤其是类型卸载条件苛刻。不满足新的内存分配时,一样将抛出OOM的异常。

六、直接内存

1.非运行时数据区

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,但是这部分内存也会被频繁的使用,也可能导致OOM。

2.NIO

JDK1.4中新增了NIO的类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O方式,它可以直接使用Native函数库直接分配堆外内存,通过一个存储在Java堆里的DirectByteBuffer对象作用这块内存的引用操作,在一些场景中就能够显著提高性能,因为避免了在Java堆和Native堆来回的赋值数据。

3.内存分配

综上,直接内存的分配不受Java堆大小的限制,但既然是内存,肯定会受总内存(物理内存、SWAP分区或者分页文件)大小及处理器寻址空间的限制。如果按照设计内存取设置-Xmx等信息,会容易忽略直接内存,是的各个内存区域总和大于了物理内存的限制,导致动态扩展时出现OOM

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值