一、概述
根据《Java虚拟机规范》的描述,JVM所管理的内存区域如下图所示:
JVM内存模型.png
JVM内存区域主要分为线程私有区域【虚拟机栈、本地方法栈、程序计数器】、线程共享区域【堆、方法区】、直接内存【元空间】。
- 线程私有区域生命周期与线程一致,随着线程的启动而创建,结束而销毁,每个线程与操作系统本地线程对应。
- 线程共享区域随着虚拟机的启动而创建,关闭而销毁。
- 直接内存并非运行时数据区的一部分,但也会被频繁使用。
二、各个内存区域描述
2.1 程序计数器(线程私有)
- 是一个较小的内存空间,可看成是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变计数器的值选取吓一跳需执行的字节码指令。
- 线程私有,各计数器互不影响,独立存储。
- 如在执行Java方法,计数器记录当前指令地址;若执行Native方法,则计数器为空。
- 唯一没有规定OutOfMemoryError情况的区域。
2.2 Java虚拟机栈(线程私有)
虚拟机栈.png- 线程私有,生命周期与线程相同。
- 描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个帧栈(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着栈帧的入栈到出栈的过程。栈遵循后进先出的规则。
- 线程请求栈深度大于虚拟机所允许的深度(StackOverflowError)
- 虚拟机栈动态扩展无法申请足够内存(OutOfMemoryError)
2.2.1 局部变量表
a). 存储方法中的局部变量,包括方法的参数。
b). 存放编译器可知的各种基本类型(其中64位长度的long和double类型数据占2个局部变量空间(Slot)),引用对象(reference类型,不同于对象本身,可能是指向起始地址的引用指针,也可能是一个指向代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
c). 局部变量表的大小在编译期间确定,程序执行期间,大小不会改变。
2.2.2 操作数栈
a). 表达式地计算在操作数栈中完成。
b). 方法开始执行时为空,执行过程中有各种字节码指令往操作数栈中写入和提取内容,即入栈/出栈操作。
c). 例如:算术运算和参数传递。
2.2.3 指向运行时常量池的引用
当前方法所属类的运行时常量池的引用,引用其他的常量类或使用字符串常量池中的字符串。
2.2.4 方法返回地址
方法执行完(正常/异常)需返回被调用位置,方法返回地址保存一些恢复上层方法执行状态的信息。
2.2.5 动态链接
每个栈帧都包含执行运行时常量池中该栈帧所属方法的引用,持有此引用为调用过程中的动态链接。
2.3 本地方法栈(线程私有)
- 与虚拟机栈类似。区别在于服务对象不同,虚拟机栈为执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。HotSpot不做区分。
- 也会产生StackOverflowError和OutOfMemoryError。
2.4 堆(GC堆)
- 线程共享。
- 内存最大的一块。
- 用来存储对象实例(数组也是对象)。
- 以前几乎所有对象都在堆中分配,但随着JIT编译器及逃逸技术成熟,栈上分配、标量替换优化技术,也没那么绝对。JDK1.7开始,Java虚拟机默认开启逃逸分析,意味着某些方法的对象引用若没被返回或未被外面使用,可在栈中分配内存。
JIT (just-in-time)
- 前提:常见编译性语言为C++,通常会把代码翻译成CPU能识别的机器码,Java为实现“一次编译,处处运行”,分为两部分,首先把代码通过javac翻译成字节码(通用的中间码),再由解释器逐条将字节码翻译成机器码,所以性能上弱于C++。
- 为优化 性能,Java虚拟机在解释器之外引入JIT编译器:当程序运行时,解释器首先发挥作用,代码可直接执行。随着时间推移,即时编译器逐渐发挥作用,它把越来越多的代码编译优化成本地方法,来获取较高的执行效率。解释器可作为编译运行的降级手段,当遇到不可靠的编译优化出现问题时,再切回来解释运行,保证程序正常使用。
逃逸分析(Escape Analysis)
- 简单说是HotSpot虚拟机分析新创建对象的使用范围,从而决定是否在堆中分配内存的一项技术。
-
堆也称GC堆,是垃圾回收的主要区域,因基本采用分代垃圾回收算法,还可划分为:新生代(Eden空间、From Survivor空间、To Survivor空间),老年代(GC回收时会详细讲解)。从内存分配的角度看,线程共享Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),但存放的仍然是对象实例,进一步划分的目的是为了更好的回收内存或者更快的分配内存。
堆内存.png -
根据规范规定,Java堆可处于物理上不连续但逻辑上连续的内存空间中,如磁盘空间。实现中既可固定大小,也可扩展。主流按照可扩展实现的(通过-Xmx 和 -Xms 控制)。
7.如果堆中没有内存完成实例分配,且堆无法扩展则会抛出OutOfMemoryError异常。可有以下形式:- OutOfMemoryError:GC Overhead Limit Exceeded(JVM花太多时间垃圾回收,却回收很小部分空间时)
- java.lang.OutOfMemoryError:java heap space(当现有的堆空间不足以创建新的对象时出现,与物理内存无关,和配置 的虚拟机内存相关)
2.5 元空间(JDK8以前的方法区,准确的说是永久代)
- JDK8时,原有的方法区(准确的说是永久代)被元空间取代。
- 方法区
- 线程共享。
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 《Java虚拟机规范》规定了方法区这种概念及作用,没有实现。永久代是HotSpot的一种方法区的实现。两者关系类似接口与类的关系
- 运行时常量池:Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。运行时常量池,JVM没做任何细节要求,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用存储。运行时常量池相对于Class文件常量池另一个重要特征为动态性,除了编译器产生的常量,运行期间也可将常量放入池中,用的较多的便是String类的intern()方法。运行时常量池是方法区的一部分,也受到方法区内存的限制,无法申请到内存则抛出OutOfMemoryError。
- 版本差异
- JDK7以前:运行时常量池包含字符串常量池都在方法区。
- JDK7时:字符串常量池放在堆中,其它仍在方法区中。
- JDK8及以后:HotSpot取消永久代,即方法区不存在,字符串常量池在堆中,运行时常量池去了云空间中。
- 取代永久代(方法区的原因)
- 第一,永久代放在JVM中,受JVM内存大小限制,元空间在本地内存中,不收限制。
- 第二,JDK8时HotSpot融合了JRockit虚拟机,而JRockit虚拟机没有永久代,所以不再开辟永久代空间。
- 直接内存(堆外内存)
- JDK1.4中新加入NIO(New Input/Output)类,引入了一个基于通道(Channel)与缓冲区(Buffer)的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了Java堆和Native堆中来回复制数据。
- 直接内存不受Java堆大小限制,但受到本机总内存(RAM以及SWAP区或分页文件)大小以及处理器寻址空间的限制。配置虚拟机参数时,经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。
网友评论