《深入理解Java虚拟机》读书笔记——Java内存区域与内存溢出异常

一、Java内存区域


上图为Java虚拟机内存模型(转自https://blog.csdn.net/ZhongGuoZhiChuang/article/details/53940562)

1、程序计数器(Program Counter Register)

        程序计数器是一个比较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

       如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。

        注:每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

2、Java虚拟机栈(Java Virtual Machine Stacks)

        描述的是Java方法执行的内存模型,每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等信息。

        局部变量表中存放了编译期可知的各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

        虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。      

 注:每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的,它的生命周期和线程相同。

3、本地方法栈(Native Method Stack) 

        同虚拟机栈,只不过本地方法栈为虚拟机使用到的native方法服务。

        Sun HotSpot虚拟机把本地方法栈和虚拟机栈合二为一 

        注:线程私有

4、Java堆(Java Heap)

        主要用于分配对象实例以及数组。

        是JVM所管理的内存中最大的一块,也是垃圾收集器管理的主要区域,也称作GC堆。    

        -Xms参数设置最小值

        -Xmx参数设置最大值 例:VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

        若-Xms=-Xmx,则可避免堆自动扩展。

        -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出是dump出当前的内存堆转储快照。

        一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

        注:所有线程共享  

5、方法区(Method Area)

        用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。

       运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载器后进入方法区的运行时异常常量池存放。

          注:所有线程共享

6、直接内存(Direct Memory)

        直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。


二、内存溢出异常     

1、Java堆溢出      

        -Xms参数设置最小值

        -Xmx参数设置最大值 例:VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

        若-Xms=-Xmx,则可避免堆自动扩展。

        -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出是dump出当前的内存堆转储快照。

2、虚拟机栈和本地方法栈溢出    

根据JAVA虚拟机规范描述:

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError

如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError。

实验表明:

在单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError。

通过不断的建立新线程的方式可以产生内存溢出溢出。为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

假设32位windows系统虚拟机最大设为2G,虚拟机提供了参数来控制java堆和方法区这两部分最大值,剩余的内存为2G - Xmx- MaxPermSize,如果虚拟机本身进程内存大小不算在内,省下的内存就有虚拟机和本地方法栈瓜分了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少。

3、方法区和运行时常量池溢出

           当运行时常量池过大或者类过多时就会导致方法区溢出。

         通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中的常量池的容量。

4、直接内存溢出   

        通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值一样。

        虽然使用DerictByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。






猜你喜欢

转载自blog.csdn.net/wq_1995/article/details/80965863