Java常用的内存区域与内存溢出异常

1、java常用的内存区域
(1)程序计数器(program counter register):
       一块较小的内存空间,每一个线程都有它自己的PC寄存器,也是该线程启动时创建的(线程私有)。PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。若thread执行Java方法,则PC保存下一条执行指令的地址。若thread执行native方法,则PC的值为undefined。
(2)java虚拟机栈(vm stack):
     虚拟机只会直接对Javastack执行两种操作:以帧为单位的压栈或出栈。每个帧代表一个方法,Java方法有两种返回方式,return和抛出异常,两种方式都会导致该方法对应的帧出栈和释放内存。 

帧的组成:局部变量区(包括方法参数和局部变量,对于instance方法,还要首先保存this类型,其中方法参数按照声明顺序严格放置,局部变量可以任意放置),操作数栈,帧数据区(用来帮助支持常量池的解析,正常方法返回和异常处理)。

栈也是线程私有,生命周期与线程相同,虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(stack frame)用于存储局部变量表/操作数栈/动态链接/方法出口等信息.每个方法从调用至完成的过程就对应着一个栈帧在虚机栈中从入栈到出栈的过程.
(3)本地方法栈(native method stack):
      保存native方法进入区域的地址, 与虚拟机栈发挥的作用类似,区别在于虚拟机栈执行的是java方法服务,而本地方法栈执行的是本地(native)方法服务。某线程在调用本地方法时,就进入了一个不受JVM限制的领域,也就是JVM可以利用本地方法来动态扩展本身。
(4)方法区(method area):
     与堆一样被所有线程共享(全局共享),用于存储已被虚拟机加载的类信息/常量/静态变量/即时编译器编译后的代码等数据,方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,也叫做非堆(non-heap),也有称为"永久代"的.包括运行时常量池(runtime constant pool)
  --运行时常量池(runtime constant pool):
      class文件中除了有类的版本/字段/方法/接口等描述信息外,还有就是常量池,用于存放编译器生成的各种字面量和符号引用.
(5)堆(heap):
      它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。堆是内存中最大的区域,被所有线程共享.作用是存放所有对象实例及数组(暂不考虑逃逸分析技术).堆也是GC管理的主要区域,所以也被称作是"GC堆",由于收集器的分代收集算法,所以java堆还可以细分为:新生代和老年代(再细致一点的还有:eden空间-from survivor空间- to surviver空间).从内存分配的角度看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区(TLAB).
(6)直接内存(Direct Memory):
       直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。 在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。

对象访问
对象访问在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中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
 

2、各种内存溢出异常(OutOfMemoryError)

注:①堆的大小设置:最小值-Xms参数和最大值-Xmx

        ②通过-XX:Permsize和- XX:MAXPermsize来控制方法区的大小(常量池属于方法区)

        ③本机直接内存溢出(可通过-XX:MaxDirectMemorySize指定)

    使用IntelliJ设置内存大小:


(1).java堆溢出(通过疯狂创建类的实例来使其溢出)
限制java堆的大小为20M,并且禁止自动扩展(将堆的最小值-Xms参数和最大值-Xmx都设置为20M),eg:

public class HeapOOM{  
   static class OOMObject {  }  
   public static void main(String[] args) {  
       List<OOMObject> list = new ArrayList<OOMObject>();  
       while(true) {  
           list.add(new OOMObject());  
       }  
   }  
}  


2.虚拟栈溢出和本地方法栈溢出

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


但是通过这种方法递归产生的异常却不是OutOfMemoryError,而是StackOverflowError异常,这是因为
        如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError
        如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError
当栈空间无法继续分配时,到底是内存太小还是已使用的栈空间太大,其本质是对同一种事情的两种描述。
事实证明,在单线程的情况下,无论是栈帧太大,还是虚拟机栈容量太小,当内存无法继续分配时,抛出的都是StackOverflowError。

建立多线程倒是能够产生OutOfMemoryError

public class JavaVMStackOOM {  
    private void dontStop() {  
        while (true) {}  
    }  
  
    public void stackLeakByThread() {  
        while (true) {  
            Thread thread = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    dontStop();  
                }  
            });  
            thread.start();  
        }  
    }  
  
    public static void main(String[] args) throws Throwable{  
        JavaVMStackOOM oom = new JavaVMStackOOM();  
        oom.stackLeakByThread();  
    }  
}  


 但是这种情况下产生的内存溢出异常和栈分配空间大小不存在任何关系,准确的说是操作系统分配给每个进程的内存都是有限的,在减去了堆和方法区还有虚拟机的其他一些内存后,剩下的栈的空间也是有限的,所以线程数越多(因为栈是线程独享的),就越容易把剩下的内存耗尽。如果是在不能够减少线程数或者是不能更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程,这种通过“减少内存”的手段来解决内存溢出的方式比较难想到。
3.常量池溢出
通过-XX:Permsize和- XX:MAXPermsize来控制方法区的大小(常量池属于方法区)

public class RuntimeConstantPoolOOM {  
  public static main (Strings[] args) {  
  //使用List保持常量池引用,避免Full GC回收常量池行为  
  List<String> list= new ArrayList<String>();  
     
  int i=0;  
  while(true) {  
           list.add(String.valueOf(i++).intern());  
    }  
  }  
}  


4.方法区溢出
方法区用来存放class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。运行时通过产生大量的类(反射和动态代理或GCLib字节码增强等)能够产生OutOfMemoryError

5.本机直接内存溢出(可通过-XX:MaxDirectMemorySize指定)
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制

配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常

直接内存(堆外内存)与堆内存比较

直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值