本文描述的有关于 JVM 的运行时数据区是基于 HotSpot 虚拟机。html
JVM 在执行 Java 程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。这些区域都有各自的用途,以及建立和销毁的时间,有的区域随着虚拟机的进程启动而存在,有的区域则依赖于用户线程的启动和结束而创建和销毁。java
运行时数据区在 HotSpot 1.8 以前的版本和 1.8 版本有所不一样,主要是 方法区移到元空间 了。git
程序计数器是一块很小的区域,它存储的是当前线程正在执行的字节码的地址(在这里,其实有两个“当前”,一个是:当前正在被 CPU 执行的线程,另外一个是:当前这个被执行的线程中正在被执行的字节码指令)。字节码解释器工做时就是改变程序计数器的值来选取下一条须要执行的字节码。对于单核心而言,多线程是经过线程轮流切换的方式实现的,在任一时刻只有一个线程可以获得 CPU 的执行权从而执行线程中的字节码指令,所以,为了使线程切换后可以恢复到正在执行的字节码的位置,每一个线程都须要拥有本身的程序计数器。面试
注意:程序计数器是惟一的一块在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 的区域。因为它是线程私有的,因此它的生命周期随着线程的建立而建立,随着线程的结束而死亡 。算法
虚拟机栈也是线程私有的,因此它的生命周期与程序计数器相同。虚拟机栈描述的是 Java 方法执行的内存模型。数组
每一个方法在执行的时候都会建立一个栈帧(一个方法对应一个栈帧,栈帧即栈的基本单位)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每一个方法被线程执行从开始到结束,就对应着一个栈帧在虚拟机栈中入栈(压栈)和出栈(弹栈)的过程。局部变量表中存放了编译可知的各类基本数据类型(byte,short,int,long,float,double,char,boolean)、对象引用(reference 类型,它存储的是:对象的地址或者是指向表明对象的句柄)。缓存
Java 虚拟机规范中规定了虚拟机栈可能出现的两种异常情况:StackOverflowError 和 OutOfMemoryError。多线程
StackOverflowError: 若当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候会抛出 StackOverflowError。框架
OutOfMemoryError: 若虚拟机栈动态扩展过程当中,若是线程请求申请栈空间没法申请到足够的内存,就会抛出 OutOfMemoryError。jvm
本地方法栈与虚拟机栈相似,虚拟机栈是执行 Java 方法开辟的内存空间,而本地方法栈是执行 Native 方法开辟的内存空间。
与虚拟机栈同样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常,抛出条件也是相似的。
堆是全部线程共享的一块区域,主要用来存放对象和数组。
在 Java 虚拟机规范中有描述:全部的对象实例和数组都要在堆上分配,可是 随着 JIT(JUST-IN-TIME)编译器的发展与逃逸分析技术的逐渐成熟,并非全部对象都只在堆上分配了,好比:随着逃逸分析技术的逐渐成熟,在即时能被回收的对象也有可能会在虚拟机栈上分配。
因为如今都采用分代回收算法,因此从内存回收的角度来看,堆还能够细分为:新生代、老年代。新生代又能够分为:Eden 空间、From Survivor 空间、To Survivor 空间。
注意:1.8 中已经完全将方法区的实现由以前的永久代改成元空间。
堆里面可能抛出的异常就是 OutOfMemoryError, 出现这种错误的表现形式主要有两种:
OutOfMemoryError: GC Overhead Limit Exceeded
:当 JVM 花太多时间执行垃圾回收而且只能回收不多的堆空间时,就会发生此错误。
java.lang.OutOfMemoryError: Java heap space
:假如在建立新的对象时, 堆内存中的空间不足以存放新建立的对象, 就会引起java.lang.OutOfMemoryError: Java heap space
错误。
方法区和堆同样也是全部线程共享的一块区域,主要用来存储已经被虚拟机加载的类信息、常量(final 修饰的)、静态变量、即时编译器(JIT)编译后产生的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
永久代就是方法区域?
早些时候,不少开发者更愿意称方法区为“永久代”。其实“永久代”这个称呼的由来是由于 HotSpot 团队并不打算为方法区从新设计垃圾回收算法,为了在方法区中可以沿用堆中的分代回收算法,因此按照堆中的命名方式,将方法去称为“永久代”。对于 JRocket、J9 而言是不存在“永久代”的概念的,因此当 HotSpot 1.8 和 JRocket 合并时,就完全放弃了“永久代”的概念(其实从 1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
方法区的垃圾回收很困难!!!
因为 Java 虚拟机规范对方法区的限制很是松,甚至能够不实现垃圾回收,通常而言,这个区域的内存回收很不使人满意,尤为是类型的卸载,条件很是苛刻,可是因为现代框架大量的依赖于 JIT 技术,致使方法区的占用比逐渐提升,因此对于方法区的回收相当重要。根据 Java 虚拟机规范规定,当方法区没法知足内存分配需求时,将抛出 OutOfMemoryError 异常。
JDK1.7 及以后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
这块区域在 1.7 以前原来是方法区的一部分,Class 文件中有一项信息是常量池(或者说是一张常量表,Class 文件以表存储数据)。
运行时常量池存储的东西较为复杂,主要分为字面量和符号引用。
字面量
存放的字面量主要包括 常量(final 修饰的),好比:final int x = 1
、静态变量(static 修饰的),还有一些其余的字面量。
符号引用
符号引用主要包括:类的彻底限定名、字段名称和描述符、方法名称和描述符,包括不少符号,好比:()
也能够看作符号引用。
字面量和符号引用将在类加载(ClassLoader 加载 Class 字节码文件)后进入方法区的运行时常量池中存放。不过,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于 Class 文件常量池一个重要的特征就是具有动态性,Java 语言并不要求常量必定产生于编译期的 Class 文件的常量池中,也并非只有 Class 文件常量池中的常量才可以进入运行时常量池中,在线程执行方法的过程中可能产生新的常量存放到运行时常量池中,例如:String 类的 intern() 方法。当运行时常量池没法申请到内存的时候就会抛出 OutOfMemoryError 异常。
直接内存并非 JVM 运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用。并且也可能致使 OutOfMemoryError 错误出现。
在 JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它能够直接使用 Native 函数库直接分配堆外内存,而后经过一个存储在 Java 堆中的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样就能在一些场景中显著提升性能,由于避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,可是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
Java 虚拟机包含的内容不少,本篇文章也只是对 Java 内存管理模块的 Java 虚拟机运行时数据区作了简要的分析,关于内存管理模块的其余部分后续会继续更新,敬请期待!
欢迎关注个人公众号,一块儿交流技术。