SexyCode

用简单的语言,分享JavaSE、JVM、JavaEE的知识和心得。

Java内存区域剖析 —— 定位OutOfMemory异常之前的必修课

带着问题阅读

  • Java中都有哪些数据需要放进内存?
  • 如果你是JVM的设计者,你会怎样将内存按功能进行划分?
  • HotSpot是怎么划分内存的?


导语

上一讲带着大家踏入了Java虚拟机的大门,从这一讲开始,进入专题的第一个版块——Java虚拟机的自动内存管理机制。

说起内存,大家很容易就想到了内存溢出,的确,对于Java工程师来说,谁的一生不会经历OutOfMemory呢,要么是Heap Space家起火,要么是Stack家淹水了,要么就是PermGen被打劫了。在学习如何定位这些异常发生的原因并提出解决方案之前,我们必须了解一下,Java虚拟机是如何划分自己的内存区域的。

本文是Effective Java专栏Java虚拟机专题的第二讲,如果你觉得看完之后对你有所帮助,欢迎订阅本专栏,也欢迎您将本专栏分享给你身边的工程师同学。

在学习本节课程之前,建议您先了解一下以下知识点:


Java内存区域

一个Java进程启动后,会被划分一块类似于疆土的内存区域,虚拟机将这块大的内存,按照所存储的数据类型,划分为不同的区域进行管理,Java虚拟机的运行时数据区,可以用下面这张图来表示:



下面就来对这张内存区域模型图作详细的讲解。


程序计数器

程序计数器在虚拟机内存中是一块很小的区域,这个计数器记录了当前线程执行到了哪一行的字节码指令。每条线程都拥有一个独立的程序计数器,有了这个计数器,才能在不停的线程切换中,让线程记得下一条要执行的指令

程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemory异常的区域。


Java虚拟机栈

和程序计数器一样,Java虚拟机栈也是线程私有的。虚拟机栈主要存储的内容是方法执行时的局部变量表,包括各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(指向一个对象地址的指针)和returnAddress类型(指向一条字节码指令的地址)。

其中,64位长度的long和double会占用2个局部变量空间,其余数据类型只占用1个。局部变量表所需的内存空间在编译器就分配完成,也就是说,进入一个方法时,这个方法需要分配多大的局部变量空间是完全确定的

可能这样讲大家还有点模糊,这里上个图:


每个线程都拥有一个虚拟机栈,线程中每个方法执行时都会在栈中创建一个栈帧,栈帧中就包含了上面所说的数据,方法每递归一次,就新建一个栈帧,依此类推。后面讲到虚拟机字节码执行引擎时,会对这张图作更详细的介绍。

在Java虚拟机规范中,这个区域有两种异常情况:

  • 如果线程运行时的栈帧总得大小超过虚拟机限制的大小,会抛出StackOverflow异常,这一点通常发生在递归运行时;
  • 如果虚拟机栈设置为可以动态扩展,并且在扩展时无法申请到足够内存,则会抛出OutOfMemory异常。


本地方法栈

本地方法栈发挥的作用和虚拟机栈是十分相似的,主要都是存储着方法执行的局部变量的信息,不同的是虚拟机栈是为执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地Native方法服务。

基于两者的相似性,HotSpot虚拟机直接将两者合二为一。

和虚拟机栈一样,本地方法栈也有StackOverflow和OutOfMemory异常。


Java堆

Java堆是Java虚拟机内存中最大的一块,是被所有线程共享的一块内存区域。Java堆负责存放Java对象实例,因此Java堆也是垃圾收集的主要区域。

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

可以通过-Xmx和-Xms参数来控制堆的大小,当堆没有内存完成实例分配,且无法继续扩展时,就会抛出OutOfMemory异常。


方法区

前面提到了虚拟机栈是用来管理Java方法执行的内存信息的,因此,很明显,这里的方法区存储的并不是方法执行的信息。

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据,也是各个线程共享的内存区域。


直接内存

直接内存并不是虚拟机运行时数据区的一部分,但是Java可以对这部分的内存进行使用,比如JDK 1.4新加入的NIO,它可以通过Native函数库直接在机器内存中获取内存,然后通过存储在Java堆中的对象,作为这块内存的引用进行操作,避免了在Java堆和Native之间来回复制数据,提高了操作的性能。

直接内存的大小只受到本机总内存的限制,因此,如果直接内存使用过多,超过了机器的物理内存限制,就会导致OutOfMemory异常。


总结

这一讲,主要给大家介绍了Java虚拟机是如何划分内存以及各个内存区域的功能和职责。了解了这些之后,我们就能够理解为什么会发生各种内存异常的异常了,在下一讲,我将在自己的机器上,演示各种内存异常是如何发生的。


课后思考

如果要你在自己的机器上模拟Java堆的内存溢出,你会怎么做?欢迎在评论区写下你的想法,O(∩_∩)O谢谢。


上一讲课后思考题的答案

上一讲的问题是——“我们经常看到有人说他掌握J2SE、J2ME、J2EE,也看过有人说他很懂Java SE、Java ME、Java EE,那么到底应该叫是J2XX还是Java XX呢?”

这个问题涉及到Java发展的历史,1998年12月4日,JDK1.2发布,Sun将Java技术体系拆分为3个方向——J2SE、J2ME、J2EE;而到了2006年12月11日,JDK1.6发布,Sun终结了J2XX的命名方式,启用Java SE、Java ME、Java EE的命名方式。因此,在面试或者平时写作的时候,建议大家采用Java XX的方式吧,就像Java的首字母要大写一样。


参考资料


阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hzy38324/article/details/76706044
个人分类: JVM
所属专栏: Effective Java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

Java内存区域剖析 —— 定位OutOfMemory异常之前的必修课

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭