美文网首页架构社区我爱编程
深入理解java虚拟机(一):Java 内存区域与内存溢出异常

深入理解java虚拟机(一):Java 内存区域与内存溢出异常

作者: susu2016 | 来源:发表于2018-04-04 17:26 被阅读178次

    参考博客
    http://blog.csdn.net/dongyuxu342719/article/details/78809049

    一、运行时数据区域

    1、线程隔离的数据区

    • 程序计数器(Program Counter Register)
      • 当前线程所执行代码的行号指示器
      • 每个线程有一个独立的程序计数器
    • 虚拟机栈(VM Stack)
      • 线程私有,生命周期等同于线程
      • 每个方法在执行的同时会创建一个帧栈
      • 储存局部变量表、操作数栈、动态链接、方法出口等信息。最重要的是局部变量表,也是大家常常讨论的“栈”空间。
    • 本地方法栈(Native Method Stack)
      • 本地方法栈类似于虚拟机栈,只不过虚拟机栈为虚拟机执行的java方法服务,本地方法栈为虚拟机使用到的Native方法服务

    Notice:

    1. HotPot 的实现合并了本地方法栈和虚拟机栈

    2、由线程共享的数据区

    • 堆(Heap)
      • 线程共享,虚拟机不关闭生命就不会结束。因此需要对此空间进行管理,是垃圾收集机制(GC)的主要区域。
      • 绝大部分对象实例及数组都要在堆上分配内存
    • 方法区(Method Area)
      • 线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 运行时常量池(Runtime Constant Pool)
      • 方法区的一部分,用于存放编译器生成的各种字面量和符号引用
    • 直接内存(Direct Memory)
      • 直接分配堆外内存,不属于虚拟机运行时数据区。
      • 避免在Java堆和Native堆中来回复制数据,在某些场景能显著提高性能。如NIO中的通道(Channel)与缓冲区(Buffer)。

    Notice:

    1. 为什么GC的主要区域是Heap:动态创建的对象都在Heap上为其实例分配内存。Stack在方法和局部代码调用完成后释放帧栈空间,但Heap生命周期与虚拟机相同,内存不能自动释放。C/C++中程序员往往在代码中显示调用 free/delete 释放堆中对象的空间,操作繁琐,且容易发生内存泄漏。java 虚拟机的自动垃圾收集机制则能自动释放不需要用到的对象实例,实现堆内存的自动管理。
    1. 已加载类的基本信息和方法储存在方法区
    2. 常量(final)和静态变量(static)储存在方法区。问题:局部变量声明final,储存在哪个区域(堆、方法区、虚拟机栈)。
    3. Object 和 Array 的实例在堆中分配内存,并在相应的位置(如栈)创建引用。没有有效引用将被GC。
    1. HotPot 的实现把方法区合并到了堆的永久代区(Permanent Generation)。
    2. 如何实现方法区是虚拟机的技术实现细节,但是使用永久代实现方法区现在看来并不是一个好主意,因为这样更容易遇到内存泄漏问题。JDK 1.7 的 HotPot 中,已经把原本放在永久代的字符串常量池移出。

    二、HotPot 虚拟机对象探秘

    1、对象的内存布局

    1. 对象头(Header)
      1. (Mark Word)对象自身的运行时数据:如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
      2. 类型指针:对象指向元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。数组还会记录数组的长度。
    2. 实例数据(Instance Data)
      1.各种成员变量,包括父类继承的和子类定义的。
    3. 对齐填充(Padding)
      1. HotPot VM 要求对象起始地址必须是8字节的整数倍
      2. 对象实例数据没有对齐时,用对齐填充补全。

    2、对象的访问定位

    • 句柄
      • 如果使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
      • 句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是很普遍的行为)时,只会修改句柄中的实例数据的指针,而reference本身不需要修改。
    通过句柄访问对象
    • 直接指针
      • 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何存放访问类型数据的相关信息,而reference中存储的直接就是对象地址。
      • 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多也是一项非常可观的执行成本。
      • 就HotSpot虚拟机而言,它是使用直接指针进行对象访问的。
    通过直接指针访问对象

    三、堆栈溢出异常

    1、对应参数:

    • -Xms 堆的最小值
    • -Xmx 堆的最大值
    • -Xss 栈容量
    • -Xoss 本地方法栈(HotPot 无效,因为没有设置单独的本地方法栈)
    • -XX:PermSize=10M -XX:MaxPermSize=10M 永久代
    • -XX:MaxDirectMemorySize=10M 直接内存
    • -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。

    2、Java 堆溢出

    • java.lang.OutOfMemoryError:Javaheap space

    • 要解决这个区域的异常,一般的手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)堆Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要分清楚到底是内存泄露还是内存溢出。

    • 如果是内存泄露,可进一步通过工具查看泄露对象到GCRoots的引用链。于是就能找到泄露对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法对他们进行回收的。掌握了泄露对象的类型信息及GC Roots引用链信息,就可以比较准确的定位到泄露代码的位置。

    • 如果不存在泄露,换句话说,就是内存中的对象确实都是必须要存活的,那就应当检查虚拟机的堆参数(-Xms和-Xmx),与机器的物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态过长的情况,尝试减少程序运行期的内存消耗。

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

    • java.lang.StackOverflowError :某线程中线程请求的栈深度大于虚拟机所允许的最大深度

    • java.lang.OutOfMemoryError:unable to create new native thread:申请多线程时栈容量不够

    • 因为操作系统分配给每个进程的内存是有限制的,比如32位Windows系统限制为2GB。虚拟机提供了参数来限制Java堆和方法区这两部分的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机本身消耗的内存不计算在内,剩余的内存就是由虚拟机栈和本地方法栈瓜分掉了,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把内存耗尽。

    • 所以,如果是建立过多线程导致的内存溢出,在不能减少线程数量或更换64位操作系统的情况下,可以通过减小最大堆和减小栈容量来换取更多的线程。如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会很难想到。

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

    • java.lang.OutOfMemoryError:PermGen space

    • HotPot中方法区和常量池都存放在永久代。

    • 运行时常量池:字符串和整型等常量池数据的存放方式在 JDK 1.7 中有一定的调整,因此表现与 JDK 1.6 会有所不同。如 intern()方法。

    • 方法区溢出:方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾回收器回收的条件是非常苛刻的。在经常生成大量Class的应用中,需要特别主要类的回收情况。比如:程序中使用了CGLib字节码增强和动态语言(Spring、Hibernate等主流框架)、大量JSP或动态生成JSP文件的应用(JSP第一次运行时需要编译为Java类),基于OSGi的应用(即使是同一个类文件被不同的类加载器加载也会被视为不同的类)等。

    5、本机直接内存溢出

    • java.lang.OutOfMemoryError

    • DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,可以直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsa()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。

    • 由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接的使用了NIO,那就可以考虑检查一下是不是这方面的原因。

    四、Intern() 方法

    不必太纠结于 intern() 方法,这个例子只是告诉我们,虚拟机底层的不同实现,会影响某些代码的结果。高版本的虚拟机对低版本做了一些优化,效率更高。

    public class RunTimeConstantPool {
        public static void main(String [] args){
            String str1=new StringBuilder("计算机").append("软件").toString();
            System.out.println(str1.intern()==str1);
            String str2=new StringBuilder("ja").append("va").toString();
            System.out.println(str2.intern()==str2);
        }
    }
    
    

    这段代码在JDK1.6中运行,会得到两个false,而在JDK1.7及以后的版本运行,会得到一个true和一个false。 产生差异的原因是:在JDK1.6中intern()会把首次出现的字符串复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在堆中,所以必然不是同一个引用,将返回false。而在JDK1.7中的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和StringBuilder创建的那个字符串是同一个。对str2比较返回false是因为”java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中指向的是第一次出现的引用,所以和str2不相同。

    JDK 1.6:intern()

    image
    • new str1:对应步骤1,在堆中为字符串分配内存,并在栈中创建引用
    • str1.intern():对应步骤2,在JDK1.6中intern()会把首次出现的字符串复制到永久代中(从实例2 copy 实例3),返回的也是永久代中这个字符串(str.intern()返回实例3)。
    • new str2:对应步骤3,在堆中为字符串分配内存,并在栈中创建引用
    • str2.intern():对应步骤4,由于实例4不是首次出现的”java”字符串对象,因此永久代的实例5是从首次出现的实例1复制而来,str2.intern() 返回实例5。

    JDK 1.7:intern()

    image
    • new str1:对应步骤1,在堆中为字符串分配内存,并在栈中创建引用
    • str1.intern():对应步骤2,JDK1.7中的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,如图在永久代记录实例2的引用,str1.intern() 返回引用3
    • new str2:对应步骤3,在堆中为字符串分配内存,并在栈中创建引用
    • str2.intern():对应步骤4,由于实例4不是首次出现的”java”字符串对象,因此永久代记录实例1的引用,str2.intern() 返回引用4。

    相关文章

      网友评论

      • zzzHER:66666,膜拜大佬儿
      • IT人故事会:经常看别人的分享.感谢别人的分享,感谢!关注了

      本文标题:深入理解java虚拟机(一):Java 内存区域与内存溢出异常

      本文链接:https://www.haomeiwen.com/subject/kcclhftx.html