【Java虚拟机学习笔记】《深入理解Java虚拟机》之第二章 - Java内存模型以及内存溢出异常
Java内存模型以及内存溢出异常
这是我在学习周志明的《深入理解Java虚拟机》第二版的第二章的知识点总结
-
前提概念
- 运行时数据区域
- 区分《Java虚拟机规范》的内存规定和实际虚拟机实现的部分
- 解释关于通常所说的栈和堆对应位置
-
Java虚拟机规范中的运行时数据区域的五大块
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区
- 补充区域
- 运行时常量池
- 直接内存
- HotSpot虚拟机对象探秘
前提概念
运行时数据区域
要区别虚拟机运行时数据区域和虚拟机非运行时数据区域
区分《Java虚拟机规范》的内存规定和实际虚拟机实现的部分
这里说的五大块内存区域都是根据Java虚拟机规范规定的一个标准,而各大提供商实现的虚拟机不一定完全与Java虚拟机规范规定的一样。
解释关于通常所说的栈和堆对应位置
这里引用《深入理解Java虚拟机》的话:
经常有人把Java内存区分成堆内存和栈内存,这种分法比较粗糙,Java内存区域的划分实际比这个复杂的多。这种划分方法的流行只能说明大多说程序员最关注的、与对象内存分配关系最密切的内存区域就是这两块。
其中所说的堆内存对应的就是Java堆,而所说的栈内存说的就是虚拟机栈或是虚拟机栈的局部变量表
Java虚拟机规范中的运行时数据区域的五大块
《Java虚拟机规范(Java SE 7 版)》中规定Java虚拟机所管理的内存将会包括以下5大块运行时数据区域:
如上图我们可以看出(图来源于网上):
线程共享的区域:
- 堆(heap)
- 方法区(Method Area)
线程私有的区域:
- 虚拟机栈 (VM Stack)
- 本地方法栈 (Native Method Stack)
- 程序计数器 (Program Counter Register)
方法区
书中原话
- 方法区和堆一样,是各个线程共享的内存区域
- 方法区被用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名,叫做Non-Heap(非堆),目的应该就是与Java堆区分开来
- Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的的内存和可选择固定大小或可拓展外,还可以选择不实现垃圾收集
方法区是什么?
方法区是一个线程共享的,用于存储类信息、常量、静态变量等等的一个内存区域,在Java8以前的HotSpot虚拟机实现中,称为永久代(PermGen)
方法区的作用是什么?
方法区被用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 类加载子系统从文件系统和网络中加载的class信息,都存在到方法区的内存空间中。通俗的讲,比如你定义的一个类,这个类的所有信息都是存储在方法区的。比如说当你要实例化一个类,那么虚拟机就会去方法区找到这个类的信息,根据这个信息进行类的实例化。
- 当然还有一些常量信息存在方法区,不过这个需要看实际虚拟机的实现,和虚拟机的版本。比如说以前字符串常量池和运行时常量池都在方法区中,但是java7后,字符串常量池迁出方法区,Java8后,字符串常量池移入堆中。
方法区的内存溢出异常
- 根据Java虚拟机规范,当方法区无法满足内存分配需求,也无法扩展时,将抛出OutOfMemoryError异常
- 方法区和堆一样,是所有线程共享的内存区域,它保存系统的类信息,比如类的字段,方法,常量池等。方法区的大小决定了可以保存多少个类,如果系统定义了太多的类,会导致方法区溢出。
方法区的一些问题:
方法区与永久代的关系?
方法区是Java虚拟机规范的内存区域,是一种标准。而永久代是HotSpot虚拟机根据虚拟机规范的方法区的一个实现
既一个标准,另一个是某种虚拟机参照这个标准的实现,每种虚拟机都可能有自己的实现。
为什么Java8不说永久代?
既以前的HotSpot虚拟机版本将方法区实现成永久代,但是从Java8之后,就没有永久代(PermGen)了,替换而来的是一个叫元空间(Metaspace)的东西。
堆
书中原话
- 对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
- 此内存区域的目的就是存放对象实例
- 随着JIT编译器的发展和逃逸分析技术的成熟,栈上分配、标量优化技术将会导致一些微妙的变化产生,所有的对象都分配在堆中也渐渐变的不是那么绝对
- Java堆是垃圾收集器管理的主要区域
- 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
- 如果堆中没有内存完成实例分配,并且堆也无法再拓展时,就会抛出OutOfMemoryError
什么是Java堆?
Java堆是虚拟机管理的最大的一块内存趋区域,是线程共享的,在虚拟机启动时创建。主要是用于存放对象实例,所以GC一般都发生在Java堆中。
Java堆的作用?
Java堆唯一的目的就是存放对象实例,也就是说平时我们new的对象都是存储在Java堆中的。
Java堆从年代中划分的构成
- 新生代:新生代分为Eden区、S0区(From区),S1区(To区)。
- 老年代:老年代主要为Tenured区
现在的垃圾收集器也基本是根据不同年代使用不同的算法收集
(1)新生代存放新生的对象或则年龄不大的对象,老年代则存放老年对象。
(2)S1和S2区是两块大小相等,并且可以互换角色的空间
(2)绝大多数情况下,对象首先分配在Eden区,在一次新生代回收后,如果对象还存活,则会进入S0或S1区,之后没经过一次新生代回收,如果对象存活,则它的年龄则加1,当对象达到一定的年龄后,就进入老年代
Java堆的内存溢出异常
如果在堆中没有内存完成实例分配,并且堆也无法进行扩展时就会抛出OutOfMemoryError异常
相关问题说明
Java虚拟机栈
书中的原话
- 与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同。
- 虚拟机栈描述的是Java方法执行的内存模型
- 每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
什么是Java虚拟机栈呢?
虚拟机栈是Java虚拟机规范中作为描述Java方法执行的内存模型,是线程私有的,既每一个线程都有自己的Java虚拟机栈,虚拟机栈的生命周期是跟随线程的
虚拟机栈有什么构成?
每个线程都有自己的虚拟机栈,线程执行的每个方法都有自己的栈帧,这个栈帧存储了调用方法的各种信息。所以可以说,虚拟机栈存储了当前线程所调用的所有方法的栈帧(当然可能还有其他的数据,目前书上没有看出来)
方法栈帧大致由局部变量表、操作数栈、动态链接、方法出口等信息。深入了解可以看《深入理解Java虚拟机》的第八章。
虚拟机栈的作用?
- 描述方法执行的内存模型。你可以想象以下每个线程都有一个虚拟机栈,而线程是用来干嘛的,就是用来执行任务,任务是什么,我们的方法可以说是任务也可以说说是任务的一部分。
- 比如在Java开发中,如果我们没有new一个线程,那么就只有一个main主线程在执行(排除其他的GC线程之类),那么我们的主线程每次调用一个方法时,就会把方法的一些信息加入主线程的虚拟机栈中。而存有的每个调用方法对应信息的结构称为一个栈帧(Stack Frame),当我们需要用到方法的信息的时候,就会从虚拟机栈的该方法的栈帧中获取。
- 再通俗的说一个作用,就是平时我们在方法中定义的局部变量的信息就是存储在虚拟机栈的该方法栈帧的局部变量表中
虚拟机栈的内存溢出异常
在Java虚拟机规范中,对这个区域规定了两种异常:
- StackOverFlowError
如果线程请求栈深度大于虚拟机所允许的深度,则会抛出栈溢出异常 - OutOfMemoryError
因为虚拟机栈在允许动态扩展的情况下,扩展时无法申请到足够的内存,则抛出内存溢出异常
本地方法栈
书中原话
- 本地方法栈与虚拟机栈锁发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
- 在虚拟机规范中对本地方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由的实现它(本地方法栈)。甚至有的虚拟机会直接把本地方法栈和虚拟机栈合二为一(如HotSpot)
什么是Native方法?
Native方法就是本地方法,既不是用Java语言所写的方法,而是使用其他语言实现的方法,比如String.intern()
什么是本地方法栈?
本地方法栈就是跟虚拟机栈几乎一样的东西,只是服务的对象不同,虚拟机栈服务Java方法,本地方法栈服务本地方法,也是线程私有,跟谁线程生命周期
本地方法栈的作用?
描述本地方法的内存模型
本地方法栈的内存溢出异常
与虚拟机栈一样,存在两个异常
- StackOverFlowError
如果线程请求栈深度大于虚拟机所允许的深度,则会抛出栈溢出异常 - OutOfMemoryError
因为本地方法栈在允许动态扩展的情况下,扩展时无法申请到足够的内存,则抛出内存溢出异常
程序计数器
书上的原话
1. 程序计数器(Program Counter Register)是一个块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
2. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基层功能都需要依赖这个计数器来完成。
3. 如果线程正在执行的是一个Java方法,这个计数器记录的都是正在执行的虚拟机字节码指令的地址,如果是Native,则这个计数器为空(Undefined)
以上是书中的原话,可以简单的对程序计数器的下定义和说明功能,以下是我的一些理解:
什么是程序计数器?
程序计数器是运行时数据区域中一个块很小的内存区域,是线程私有的,是用来指向当前线程下一步要执行的指令的地址。既让虚拟机知道某个线程目前执行到哪里,下一步要执行哪里,它不会有内存溢出异常,生命周期跟随线程。
程序计算器记录的是什么?
正如书中的话,计数器记录的是虚拟机字节码指令的地址,可以说是当前线程下一步要执行的指令
为什么要程序计数器?
Class文件是由二进制字节码组件的代码文件,当JVM去加载识别这些字节码文件时,也就是Java解释器去解释这个代码的时候,需要一个东西来辨别当前的字节码解析到了哪个地方,下一步又要解析那个位置。这就需要引进一个用来引导字节码解析顺序的东西,就叫做程序计数器。
为了让每个线程正常工作,每个线程都有自己的程序计数器,这样当线程执行切换的时候就可以在上次执行的基础上继续执行,因为切换的线程的计数器上记录了下一步要执行的指令,(既知道了当前运行到哪),JVM就是通过读取程序计数器的值来决定下一条需要执行的字节码指令,进而进行选择语句、循环、异常处理等
程序计数器的内存异常情况
- 程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域,既不会有内存溢出之类的异常
程序计数器的相关问题
程序计数器是线程私有的,也就意味着是线程隔离,并非线程共享
(一个单核处理器在同一时间只会执行一条线程中的指令,因此,为了线程切换之后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,单独存储。)当线程执行的是Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址。如果是Native方法,则这个计数器为空
(因为Java方法是由Java代码所实现的,编译之后是字节码文件,计数器也是为了标记字节码代码的执行顺序。但是Native方法并不是由Java代码所实现,是Java调用非Java代码实现的接口,所以不需记录,自然为空。)该内存区域是Java虚拟机规范中唯一没有规定OutofMemoryError情况的内存区域。
(程序计数器是用来指向下一条要执行的指令的位置,是个固定宽度的整数的存储空间,也不由程序员操控,所以不存在内存溢出的情况,又因为内存区域非常小,跟随线程生命周期,所以也不需要考虑垃圾回收的问题)
小结:
程序计数器是一块很小,线程私有,没有内存溢出异常的运行时数据区域。
参考网站:
JVM运行的数据分区—-程序计数器实现的功能
JVM学习笔记——程序计数器
JVM程序计数器 - Rainy
补充区域
运行时常量池
书中原话:
- 运行时常量池是方法区的一部分。
- Class文件中除了有类的版本,字段,方法,接口等信息外,还有一项信息叫做常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
- Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需求来实现这个内存区域。
什么是运行时常量池
运行时常量池是方法区的一部分,用于存放一下常量信息
运行时常量池的作用是什么?
- 静态常量池(Class文件常量池)的信息在类加载后被放入运行时常量池:如字面量,符号引用,直接引用等
- 还有运行期间的一些新常量也放入池中,如String.intern
运行时常量池的内存溢出
因为运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时就会抛出OutOfMemoryError
相关问题
1.7的时候符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
直接内存
书中原话
- 直接内存并不是虚拟机运行时数据区域的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也别频繁的使用,而且也可能导致OutOfMemoryError异常的出现
直接内存是什么?
- 直接内存不是运行时区域的一部分,本机的直接内存不受Java堆大小的影响。
- Java的NIO库允许Java程序使用直接内存,从而提高性能,通常直接内存速度回优于Java堆,读写频繁的场合可能会考虑使用
HotSpot虚拟机对象探秘
有机会再总结~
参考资料
《深入理解Java虚拟机》