引:总是提到堆呀,栈呀,常量池呀,但是Java虚拟机内的Java模型却总是理解不清楚,这次就总结一下。
Java内存区域组成
这里讲的是Java虚拟机在执行Java程序的过程中会把它所管理的内存划分五大区域。
- 程序计算器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区
下面也把来自深入理解Java虚拟机的图片贴一下:
接着我们重点理解一下这五大区域。
程序计数器
什么是程序计数器
- 程序计数器是一块较小的内存空间,他可以看做当前线程所执行的字节码的行号指示器。
- 如果线程在执行一个Java方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令地址,如果正在执行的是Native方法,这个计数器值则为空。
程序计数器的作用
- 单线程的时候:通过改变这个计数器的值来选取下一条需要执行的字节码指令,从而实现分支、循环、跳转、异常处理、线程回复等基础功能。
- 多线程的时候:当每个线程都有独立的程序计数器,则线程切换后就能回复到正确的执行位置。
PS: Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。
程序计数器的特点
- 存储空间较小
- 线程私有,每个线程都有一个程序计数器
- 唯一一个没有规定任何OutOfMemoryError情况的区域
- 生命周期与线程相同
Java虚拟机栈
什么是Java虚拟机栈
Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表(包括基本数据类型,对象引用和returnAddress,其中long和double会占据2个局部变量空间,其他只占用一个)、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
Ps: 人们常说的Java内存区分为“堆”和“栈”,“堆”存放对象(可以),“栈”只是值其中的局部变量表(不可以),这是不正确的。
Java虚拟机栈的特点
- 线程私有,生命周期与线程相同
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 虚拟机栈可以动态扩展,也可以固定长度,在动态扩展的时候如果无法申请到足够的内存,就会抛出OutOfMemoryError异常
本地方法栈
什么是本地方法栈
本地方法栈描述的是本地方法执行的内存模型,它发挥的作用与虚拟机栈发挥的作用是非常相似的,如Sun HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一,它与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆
什么是Java堆
- Java堆是存放对象实例的内存区域
- 几乎所有对象实例都在这里分配内存
Java堆的特点
- Java虚拟机管理的内存中最大的一块
- 线程共享,在虚拟机启动时创建
- 垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器都采用分代收集算法,所以可分为新生代和老年代,再细致一点可以分为Eden空间、From Survivor空间和To Survivor空间等。从内存分配来看,线程共享的Java堆可能划分出多个线程私有的分配缓存区TLAB,进一步划分的目的是为了更好地回收内存,或者更快的分配内存
- 实现中可以固定大小,也可以是可扩展的,如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,就会抛出OutOfMemoryError异常
方法区
什么是方法区
- Java虚拟机规范把方法区描述为堆的一个逻辑部分。
- 他用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
方法区的特点
- 线程共享,在虚拟机启动时创建
- 永久代是因为HotSpot选择将GC分代收集器扩展到方法区,或者使用永久代来实现方法区,对于HotSpot官方发布的路线图信息,现在也有放弃永久代逐步采用Native Memory来实现方法区的规划了,并且已经把原本放在永久代的字符串常量池移除
- 不需要连续的内存,可以选择固定大小或者可扩展,还可以选择不实现垃圾回收
- 内存回收的主要目标主要是针对常量池的回收和对类型的卸载,但是回收效率低
- 当方法区无法满足内存分配的需求的时候,将抛出OutOfMemoryError的异常
运行时常量池
- Class文件除了包含类的版本
字段、方法、接口等描述信息外,还有一项信息是常量池 - 常量池用于存放编译期间生成的各种字面量和符号引用,这部分内容在类加载后进入运行时常量池中存放
- 运行时常量池相对于Class文件常量池具有动态性,Java语言不要求只有Class文件中的常量池的内容才能进入运行时常量池,运行时也可能将新的常量放入池中,如String类的intern方法
- 当常量池无法再申请内存时将会抛出OutOfMemoryError异常
PS: jdk1.7的常量池移到了堆中,同时在jdk1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域
直接内存
- 通过一个存储在Java堆中的DirectByteBuffer对象最为这块内存的引用进行操作,这个可以显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
- 案例是NIO类引入一种基于通道与缓存区的I/O方式,它可以使用Native函数库直接分配对外内存,然后通过DirectByteBuffer进行操作
- 本机直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存大小和处理器寻址空间的限制,也可能会抛出OutOfMemoryError异常
总结
本篇文章是参考《深入理解Java虚拟机》第二版所写,当时作者用的是jdk1.7,结果现在jdk1.9都出了,最大的改变就是方法区了,目前还没有能力改正,也就将就了,望作者早日更新,或者自己成为大牛,将这部分重新整理。