JVM之Java内存区域与内存溢出异常

                                  第二章    Java内存区域与内存溢出异常
2.1   运行时数据区域
       Java虚拟机在执行Java程序的过程中把它所管理的内存划分为若干个不同的数据区域。有的区域随虚拟机进程的启动而存在,有的依赖用户线程的启动和结束而建立和销毁,运行时数据区域如图2-1所示。

   上述各个区域的说明如下:
  • 程序计数器 一块较小的内存空间,可看作是当前线程所执行的字节码的行号的指示器。JVM的字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字      节码指令,分支、循环、跳转、异常、线程恢复等基础功能都需要依赖这个计数器来完成。其中,每个线程都有一个独立的计数器,各线程之间的计数器互不影响,独立存储,成为"线程私有"的内存;若线程正在执行的是Java方法,则计数器记录正在执行的虚拟机字节码指令的地址;若执行Native方法,则计数器为空。另外,它是JVM规范中唯一一个没有规定任何OutOfMemoryError情况的区域。
  • Java虚拟机栈:线程私有,生命周期与线程相同,它描述的是Java方法执行的内存模型,每个方法被执行时都会同时创建一个栈帧用于保存局部变量表、操作栈、动态链接、方法出口等信息,每个方法被调用直到执行完成的过程,都对应这一个栈帧从虚拟机栈中入栈到出栈的过程。在JVM规范中,若线程请求的栈深度大于JVM所允许的深度,将会抛出StackOverflowError异常;若虚拟机栈可动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
  • 本地方法栈:与虚拟机栈相似,区别是虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则是为虚拟机使用到的native方法服务。它也会抛出StackOverflowError和OutOfMemoryError异常。
  • Java堆:对于大多数应用来说,Java堆是JVM所管理的内存中最大的一块,它被所有的线程共享,在虚拟机启动时被创建,此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。JVM规范中的描述:所有的对象实例以及数组都要在堆上分配。另外,Java堆是垃圾收集器管理的主要区域,故也被成为"GC堆"。Java堆可细分为新生代和老年代,但无论如何分配,都与存放的内容无关,无论哪个区域,存放的都是对象实例,进一步划分是为了更好的回收内存或更快地分配内存。Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。在实现时,既可以实现固定大小的,也可以是可扩展的。当前主流都是可扩展的,若在堆中没有内存完成实例分配,并且无法再扩展时,将会抛出OutOfMemoryError异常。
  • 方法区:各线程共享的内存区域,用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它不需要连续的内存空间,可以选择固定大小或可扩展实现,并且可选择不实现垃圾收集。相对而言,垃圾收集行为在此区域比较少见,此区域内存回收的目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
  • 运行时常量池:它是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法去的运行时常量池中。另外,它具备动态性,即可在运行期间将新的常量放入方法区运行常量池中,如String类的intern()方法。当常量无法申请到内存时将会抛出OutOfMemoryError异常。
    另外,还有一种内存类型叫 直接内存,它不属于虚拟机运行时数据区,也不是JVM规范中定义的内存区域,但它也被频繁地调用。在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通到(Channel)和缓冲区(Buffer)的I/O方式,它可使用Native函数库直接分配堆外内存,然后通过存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作,这在某些场景中可显著提高性能,因为它避免了在Java堆和Native堆中来回复制数据。

2.2    对象访问

      不同的虚拟机实现对象访问的方式是不同的,主流的访问方式有两种:使用句柄和直接指针。
  • 句柄访问方式:Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如图2-2所示。
  • 直接指针访问方式:Java堆对象的布局中需考虑如何放置访问类型数据的具体信息,reference中直接存储的就是对象的地址,如图2-3所示。

     两种访问方式优缺点比较:
  • 句柄访问方式:reference存储的是稳定的句柄地址,在对象移动时只需修改句柄中实例数据指针,而reference本身无需修改;
  • 直接指针访问方式:速度快、节省了一次指针定位开销的时间开销。
2.3   实战:OutOfMemoryError异常
  • Java堆溢出:Java堆用于存储对象实例,只要不断创建对象,并且保证这些对象有有效的引用而避免被GC回收,就会出现对象数量到达最大堆容量限制后产生内存溢出异常。通过设置JVM的参数-XX:HeapDumpOnOutOfMemoryError可让虚拟机在内存出现溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。分析内存溢出的原因可通过内存映像分析工具(Eclipse Memory Analyzer)对Dump堆转储快照进行分析。
  • 虚拟机栈和本地方法栈溢出:若线程请求的栈深度大于虚拟机所允许的最大深度时,将抛出StackOverFlowError异常;若虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
  • 运行时常量池溢出:向运行时常量池中添加内容最简单的办法是使用String.intern()这个Native方法(若池中含有String对象,则直接返回其引用,否则,向池中添加并返回其引用)。由于常量池分配在方法区中,可通过-Xx:PermSize和-Xx:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。
  • 方法区溢出:方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对该区域测试的基本思路是运行时产生大量的类去填满方法区,直到溢出。Spring和Hibernate对类进行增强时,都会用到CGlib字节码技术,增强的类越多,需要的方法区内存来保证生成的Class可以载入。
  • 直接内存溢出:直接内存容量可通过-Xx:MaxDirectMemorySize指定,若不指定,则默认与Java堆的最大值(-Xms指定)一样。在测试溢出时,虽然可以通过DirectByteBuffer分配内存也会抛出内存溢出异常,但它并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()方法。
                                                                核心内容出处:《深入理解Java虚拟机:JVM高级特性与最佳实践》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值