您的位置:首页 > 编程语言 > Java开发

【深入理解JVM虚拟机】第2章 java内存区域与内存溢出异常

2018-10-20 21:08 597 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/CharJay_Lin/article/details/83216935

文章目录

  • 2.3 对象访问
  • 2.4 实战:OutOfMemoryError异常
  • 2.1 概述

    Java与C++之间有一堵由__内存分配__和__垃圾收集__技术所围成的高墙,墙外的人想进来,墙里面的人想出来。

    2.2 Java运行时数据区

    2.2.1 程序计数器

    程序计数器(Program Counter Register)

    • 作用
      记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
    • 意义
      JVM的多线程是通过线程轮流切换并分配处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。所以,为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器。
    • 存储内容
      当线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址。
      当线程中执行的是一个本地方法时,程序计数器中的值为空。
    • 可能出现异常
      此内存区域是唯一一个在JVM上不会发生内存溢出异常(OutOfMemoryError)的区域。

    2.2.2 虚拟机栈

    Java虚拟机栈(Java Virtual Machine Stack)

    • 作用
      描述Java方法执行的内存模型。每个方法在执行的同时都会开辟一段内存区域用于存放方法运行时所需的数据,成为栈帧,一个栈帧包含如:局部变量表、操作数栈、动态链接、方法出口等信息。
    • 意义
      JVM是基于栈的,所以每个方法从调用到执行结束,就对应着一个栈帧在虚拟机栈中入栈和出栈的整个过程。
    • 存储内容
      局部变量表(编译期可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型)、操作数栈、动态链接、方法出口等信息。
      值得注意的是:局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。
    • 可能出现的异常
      如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
      如果在动态扩展内存的时候无法申请到足够的内存,就会抛出OutOfMemoryError异常。

    2.2.3 本地方法栈

    本地方法栈(Native Method Stack)

    • 作用
      为JVM所调用到的Nativa即本地方法服务。
    • 可能出现的异常
      和虚拟机栈出现的异常很相像。

    2.2.4 Java堆

    Java堆(Java Heap)

    • 作用
      所有线程共享一块内存区域,在虚拟机开启的时候创建。
    • 意义
      1、存储对象实例,更好地分配内存。
      2、垃圾回收(GC)。堆是垃圾收集器管理的主要区域。更好地回收内存。
      -存储内容
      存放对象实例,几乎所有的对象实例都在这里进行分配。堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以。
      值得注意的是:在JIT编译器等技术的发展下,所有对象都在堆上进行分配已变得不那么绝对。有些对象实例也可以分配在栈中。
    • 可能出现的异常
      实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。
      如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常。

    2.2.5 方法区

    方法区(Method Area)

    • 作用
      用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 意义
      对运行时常量池、常量、静态变量等数据做出了规定。
    • 存储内容
      运行时常量池(具有动态性)、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 可能出现的异常
      当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

    2.2.6 运行时常量池

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

    运行时常量池相对于Class文件常量池的另外一个重要特征是__具备动态性__,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将将新的常量放入池中,这种特性被开发人员利用的比较多的便是String类的intern()方法。

    既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时将会抛出OutOfMemoryError异常。

    2.2.7 直接内存

    __直接内存(Direct Memory)__并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且有可能导致OutOfMemoryError异常。

    在JDK1.4种新加入了NIO(New Iuput/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这快内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

    显然,本机直接内存非分配不会受到Java堆大小的限制,但是既然是内存,肯定会受到本机总内存的大小和处理器寻址空间的限制。所以也可能会抛出OutOfMemoryError异常。

    2.3 对象访问

    对象访问在Java语言中无处不在,是最普通的程序行为,即使是最简单的访问,也涉及到Java栈、Java堆、方法区这三个重要的内存区域之间的关联关系,如下一句代码:

    Object obj = new Object();

    假设这句代码出现在方法体中,那"Object obj"这部分的语义将会反映到Java栈(虚拟机栈)的本地变量中,作为一个Reference类型数据出现。

    而"new Object()"这部分的语义将会反映到Java堆中,形成一块儿存储了Object类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块儿内存的大小是不固定的。

    另外,在Java堆中,还必须包含能包括能查到此对象类型数据(如,对象类型、父类、实现的接口、方法等)的地址信息,这些数据存储在方法区中。

    由于reference类型在Java虚拟机规范里只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同的虚拟机实现的对象访问方式会有所不同。

    主流的访问方式有两种:使用句柄和直接指针。

    • 句柄方式,Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图:

    • 指针方式,reference变量中直接存储的就是对象的地址,而Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,如下图:

    这两种对象的访问方式各有优势:

    • 使用句柄访问方式的最大好处是reference中存贮的是稳定的句柄地址,在对象被移动(比如垃圾收集时,对象移动是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
    • 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多也是一项非常可观的执行成本。
    • 本书讨论的主要虚拟机Sun HotSpot,它是使用第二种方式进行对象访问的,但是从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

    2.4 实战:OutOfMemoryError异常

    2.4.1 Java堆溢出

    可以通过限制Java堆的大小,设置为不可扩展(将堆的最小值参数-Xms和最大值参数-Xmx设置成一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便以后进行分析。
    Java堆内存的OOM异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。如下图所示:

    要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。如下图展示了使用Eclipse Memory Analyzer Tools打开的堆转储快照文件:

    如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
    如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(Xmx和Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期间的内存消耗。

    2.4.2 虚拟机栈和本地方法栈溢出

    由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。
    关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

    • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
    • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

    这里把异常分为两种情况,实际上却存在着一些互相重叠的地方:当栈空间无法分配时,到底是内存太小,还是已使用的栈空间太大,其本质只是对同一事件的两种表述而已。

    2.4.3 运行时常量池溢出

    如果要向运行时常量池中添加内容,最简单的方法是使用String.intern()这个Native方法。该方法的作用时:如果池中包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

    2.4.4 方法区溢出

    方法区用于存放Class相关的信息,如类名、访问修饰符、常量池、字段描述、方法描述等。生成大量的动态类区填充方法区,可以触发此区域的溢出异常。
    方法区溢出是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定的条件比较苛刻。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景出了CGLib字节码增强外,还有大量JSP或者基于OSGi的应用等。

    2.4.5 本机直接内存溢出

    DirectMemory可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样。

    阅读更多
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: 
    相关文章推荐