[深入理解JVM虚拟机]第2章-Java内存区域与内存溢出异常

2.0引-Java内存区域中,栈内存和堆内存分别装什么,为什么?

  • 栈:解决程序的运行问题,即程序如何执行,或者说如何处理数据。
  • 堆:解决的是数据存储的问题,即数据怎么放,放在哪儿。

参考链接https://www.cnblogs.com/gdufs/p/6407432.html

2.2运行时数据区

线程隔离的运行时数据区:程序计数器、Java虚拟机栈、本地方法栈

  • 程序计数器:当前线程所执行的字节码的行号指示器,由于线程私有,故借助程序计数器线程切换后能恢复到正确的执行位置。
  • Java虚拟机栈:每个方法在执行时会创建一个栈帧。每一个方法从调用至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 本地方法栈 ?todo

线程共享的运行时数据区:Java堆、方法区(其中包含运行时常量池)

  • Java堆:存放对象实例。是垃圾收集器管理的主要区域。
  • 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。?
  • 运行时常量池:是方法区的一部分。?todo

此外,直接内存不属于运行时数据区,但它也可能会产生内存溢出。Java中NIO类可以直接分配堆外内存。

2.3(基于HotSpot虚拟机)对象

对象的创建步骤

  • step1:类加载。new指令的符号参数在常量池定位到类的符号引用,检查这个符号引用代表的类是否被加载、解析、初始化过,没有的话首先进行类加载。
  • step2:为新对象分配内存。
    • 问题一:如何划分可用空间:
      • 方法一:指针碰撞。如果虚拟机堆中的内存是规整的,则把堆空闲内存指针移对象大小等大小距离即可。
      • 方法二:空闲列表。如果堆内存不规整,就要维护一个空闲列表记录空闲的内存块。
    • 问题二:修改指针所指向的位置,如何在并发情况下保证此操作是线程安全的。
      • 方法一:对分配内存空间的动作进行同步:虚拟机采用CAS?+失败重试的方式保证更新操作的原子性。
      • 方法二:本地线程分配缓冲(TLAB):即每个线程预先在队中分配一小块内存,是内存分配的动作在不同空间进行。只需在分配新TLAB时,进行同步锁定。选择是要用TLAB可通过参数-XX:+/-UseTLAB设定。
  • step3:将分配到的内存空间初始化为零值。
  • step4:对对象进行必要的设置。包括这个对象是哪个类的实例等。
  • step5:从虚拟机角度,step1-4完成了对象的创建。但从Java程序视角来看,还需要对对象进行初始化(init方法)。

对象的内存布局

HotSpot虚拟机中,对象内存存储=对象头(Mark Word)+实例数据+对齐填充。

其中,对象头包含哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。它是一个非固定的数据结构,根据对象的状态复用自己的存储空间。

对象的访问定位

  • 两种,一种是通过句柄访问对象,一种是通过直接指针访问对象。
    • 通过句柄访问对象:Java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据地址(位于Java堆)及对象类型数据地址(位于方法区)
    • 通过直接指针访问对象:reference中存储的就是对象地址,对象中包括:对象类型数据指针(位于Java堆)+对象实例数据(位于方法区)。
  • 对比
    • 不同点:就是reference存储的是什么、Java堆中是否有句柄池、对相关类型数据的地址存在句柄池还是对象的对象头中。
    • 各自优势:句柄方式:reference中存储的是稳定的句柄地址,当对象被移动reference本身不需要修改;直接指针方式:快,节省了一次指针定位的开销。 - HotSpot使用直接指针访问方式。

2.4OutOfMemoryError异常

虚拟机内存除程序计数器外的其他几个运行时区域都可能发生OutOfMemoryError(OOM)异常。

2.4.1Java堆溢出

  • 产生堆溢出:当把堆设置为不自动扩展(设置堆最小值-Xms参数=堆最大值参数-Xmx参数)时,不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么创建的所有对象超过堆的容量限制后,就会产生OutOfMemoryError异常。
  • 排查方法
    • 步骤1 通过内存映象分析工具对Dump出来的堆转储快照?进行分析,重点是确认是内存泄漏(指内存中有许多不必要对象)还是内存溢出(内存中的对象都是有必要的)。
    • 步骤2 如果是内存泄漏:通过工具,查看泄漏对象到GC Roots的引用链,就可以较准确地定位泄露代码的位置。
    • 如果是内存溢出:在物理内存允许的情况下,调大虚拟机的堆参数;此外,检查代码中睡否某些对象生命周期过长,持有状态时间过长?。

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

  • 虚拟机参数:HosSpot虚拟机不区分虚拟机栈和本地方法栈,所以-Xoss参数(设置本地方法栈大小)是无效的。栈容量只由-Xss参数控制。

  • 关于虚拟机栈和本地方法栈,Java虚拟机规范描述了两种异常:已使用的栈空间太大:*Error;内存太小:OutOfMemoryError。但是这本质是对同一件事情的两种描述而已。

  • 由栈内存原因产生*Error异常

    • 单线程下:方法一:减少栈内存容量。方法二:定义大量本地变量。单线程下,经测试这两种方法均抛出*Error异常。
  • 产生OutOfMemoryError异常

    • 多线程下:通过不断建立线程产生OutOfMemoryError(内存溢出)异常,但这与栈内存容量是否足够大并无关联。事实上,反而每个线程分配的栈内存越大,越容易造成OutOfMemoryError异常。因为OS给每个进程分配的内存(瓜分给:Xmx最大堆容量+MaxPermSize最大方法区容量+最大栈容量(包括虚拟机栈和本地方法栈)+程序计数器占用的(较小,可忽略不计)+虚拟机进程本身耗费的(暂略))是有限制的,所以若每个线程分配的栈内存越大,可以建立的线程数就越少,就越容易在建立线程时把内存耗尽。
  • 排查方法

    • 针对*Error,可以阅读错误堆栈,找到问题所在。
    • 多线程下,针对OutOfMemoryError,在不能减少线程数或更换64位虚拟机的情况下,则可以通过减少最大堆容量和减少栈容量来换取更多线程

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

  • 方法区:
    • 造成内存溢出:生成大量类。主流框架当有大量类增强,就可能会造成内存溢出。方法区溢出是常见的内存溢出异常,因为一个类被判定回收的条件非常苛刻。
    • 需要注意方法区溢出的场景:动态生成大量类;使用CGLib字节码增强;动态语言;大量JSP的应用;基于OSGi的应用。
  • 方法区中的运行时常量池:
    • 造成内存溢出:JDK1.6中,String.intern()不断建立常量,且保持引用保证不被GC,会造成内存溢出。JDK1.7以后String.intern()使用引用的方法,不再复制,不会造成内存溢出。
    • 排查方法:JDK1.6中,异常会提示OutOfMemoryError:PermGen space,说明是(HotSpot虚拟机)永久代的异常。JDK1.7后开始逐步“去永久代”。

2.4.4 本机直接内存溢出

  • 参数: -XX:MaxDirectMemorySize 指定,否则默认与Java堆最大值一样。
  • 排查方法:Heap Dump文件? 不会看到明显异常,如果发现OutOfMemoryError后Dump文件很小,程序中又使用了NIO,可以考虑是否是本机直接内存溢出。
上一篇:08: CORS实现跨域请求


下一篇:Java虚拟机之JVM系统和内存模型