Java虚拟机(一):Java内存区域与内存溢出异常

1、Java内存区域

      由图可知,java运行时内存区域主要划分为6个区域,分别为程序计数器、Java虚拟机盏、本地方法盏、Java堆、方法区(内含运行时常量池)、和直接内存区(分配空间时直接在本地堆中分配)。
(1)、程序计数器   
        线程隔离的数据区:每个线程一个独立的计数器,线程之间互不影响。
        可以看成当前线程执行字节码的行号指示器。如果线程正在执行java代码,其值是字节码指令地址,如果是本地Native方法,计数器为空。
(2)、虚拟机栈
         线程隔离的数据 区: 它的生命周期和线程相同

        描述的是java方法执行的内存模型:每个方法在执行的同时多会创建一个栈帧用于存储局部变量表、操作数栈、动态链表、方法出口等信息。

每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

       局部变量表存放了编译期可知的各种基本数据类型和对象引用,所需内存空间在编译期确定。

       操作数栈是为运行过程中、字节码指令执行服务。

       参数设置:-Xoss参数设置本地方法栈大小(对于HotSpot无效),-Xss参数设置栈容量 例: -Xss128k

(3)、本地方法栈

       线程隔离的数据区:桶虚拟机栈作用类似。

       主要用于虚拟机执行Java本地(Native) 方法。 Sun HotSpot虚拟机把本地方法栈和虚拟机栈合二为一

(4)、Java堆
       线程共享的数据区:存放运行时对象,由所有线程共享访问。
      Java堆是垃圾回收的主要区域, 是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。所有的对象实例和数组都在堆上分配 。Java堆细分为新生代和老年代 不管怎样,划分的目的都是为了更好的回收内存,或者更快地分配内存。 Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

       参数设置: -Xms 参数设置堆最小值, -Xmx 参数设置堆最大值 , 若-Xms=-Xmx,则可避免堆自动扩展。
(5)、方法区
       线程共享的数据区:

      方法区它用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

       运行时常量池(Runtime Constant Pool)是方法区的一部分 Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分 内容将在类加载后进入方法区的运行时常量池中存放。
      参数设置: -XX:MaxPermSize 设置上限 , -XX:PermSize 设置最小值。
(6)、 直接内存

         直接内存并不是虚拟机运行时数据区的一部分。在NIO中,引入了一种基于通道和缓冲区的I/O方式,它可以使用native函数直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

      参数设置:-XX:MaxDirectMemorySize设置最大值,默认与java堆最大值一样。

2、对象创建与内存布局

   (1)、对象的创建        
       1).检查 

        虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程.

        2) .分配内存 

        接下来将为新生对象分配内存,为对象分配内存空间的任务等同于把一块确定的大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”。如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”。选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

        3).设置

       内存分配完后,虚拟机将内存设置为零,然后设置对象头,具体包括设置对象类型元数据信息、对象哈希吗,对象分带年龄,以及锁相关的信息。

        4). Init

     执行new指令之后会接着执行Init方法,进行构造函数初始化,按照程序员要求完成对象状态设置,这样一个对象才算产生出来。

 (2)内存布局
       1).  在HotSpot虚拟机中,对象在内存中储存的布局可以分为3块区域:对象头、实例数据和对齐填充。

       对象头包括两部分:

          a) 储存对象自身的运行时数据,如哈希码、GC分带年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。

          b) 另一部分是指类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

      2).  对象的访问定位

  •  使用句柄访问

      Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址。

      优势 :reference中存储的是稳点的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

  • 使用直接指针访问

      Java堆对象的布局就必须考虑如何访问类型数据的相关信息,而refreence中存储的直接就是对象的地址。

       优势:速度更快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本

3、内存溢出异常
 (1)java堆溢出
         Java堆用于存储对象实例,只要不断的创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在数量到达最大堆的容量限制后就会产生内存溢出异常。
        如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。 如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

实例: 虚拟机参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

代码:
package com.xl.jvm;
import java.util.ArrayList;
import java.util.List;
/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {
 
 static class OOMObject {
 }
 public static void main(String[] args) {
  List<OOMObject> list = new ArrayList<OOMObject>();

  while (true) {
   list.add(new OOMObject());
  }
 }
}


结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7348.hprof ...
Heap dump file created [27860758 bytes in 0.164 secs]

(2)、虚拟机盏和本地栈溢出
        对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
          如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError。

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

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

         如果是多线程导致的内存溢出,与栈空间是否足够大并不存在任何联系,这个时候每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。解决的时候是在不能减少线程数或更换64为的虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

实例:虚拟机参数:-Xss128k
代码:
  
结果:
Exception in thread "main" java.lang.StackOverflowError
 at com.xl.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)
 at com.xl.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
stack length:991
 package com.xl.jvm;

public class JavaVMStackSOF {
 private int stackLength = 1;

 public void stackLeak() {
  stackLength++;
  stackLeak();
 }

 public static void main(String[] args) throws Throwable {
  JavaVMStackSOF oom = new JavaVMStackSOF();
  try {
   oom.stackLeak();
  } catch (Throwable e) {
   System.out.println("stack length:" + oom.stackLength);
   throw e;
  }
 }
}

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

       String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在永久代中,可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。

       Intern():JDK1.6 intern方法会把首次遇到的字符串实例复制到永久代,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是一个引用。JDK1.7 intern()方法的实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。

实例:虚拟机参数:-XX:PermSize=10M -XX:MaxPermSize=10M
代码:
package com.xl.jvm;

import java.util.ArrayList;
import java.util.List;

public class RuntimeConstantPoolOOM {
 public static void main(String[] args) {
  // 使用List保持着常量池引用,避免Full GC回收常量池行为
  List<String> list = new ArrayList<String>();
  // 10MB的PermSize在integer范围内足够产生OOM了
  int i = 0;
  while (true) {
   list.add(String.valueOf(i++).intern());
  }
 }
}


参考资料: 《深入理解java虚拟机》




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值