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

Java虚拟机内存划分
这里写图片描述

  1. 程序计数器:
    (1)线程私有。
    (2)记录当前线程所执行的程序码位置。因为一个程序可能会出现多个线程,而多个线程执行时又是交替执行的,所以就需要记录各个线程的执行位置,以便之后继续执行。
    (3)如果一个线程正在执行Java方法,则其计数的值为正在执行的虚拟机字节码指令的地址;如果是Native方法(调用C,C++等其他语言),则计数为 undefined。
    (4)唯一一个没有规定任何OutOfMemoryError异常情况的内存区域。

  2. Java虚拟机栈:
    (1)线程私有,生命周期与线程相同。
    (2)当每个方法运行时,都会创建一个栈帧(Stack Frame)用来保存局部变量表,操作数栈,动态连接,方法出口(方法返回地址)等信息。而每个方法从被调用到执行完成的过程,就是一个栈帧在虚拟机栈中从入栈到出栈的过程。

    1. 局部变量表:
      存储着编译器可知的基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。64位的long和double会占用2个局部变量空间(Slot),其他1个。
      局部变量表所需的内存空间在编译期完成分配,即进入一个方法时,该方法在帧中所要分配的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    2. 操作栈:
      栈深度在编译期确定,在栈帧生成时置为空。在执行方法操作时,存储JVM从局部变量表中复制的常量或变量,提供提取及结果入栈。也用于存放执行方法所需的参数和方法运行的结果。
      可以存放JVM中定义的任意一种类型的变量。
      任意时刻,操作栈都是固定深度的,long,double占用2个深度,其余占1个。

    3. 动态连接:
      每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

    4. 方法返回地址:
      顾名思义,就是当前栈帧所代表的方法被调用的地址。
      一个方法被执行后有2种退出方式:1.遇到方法返回的字节码指令。2.遇到异常并没有在方法中进行处理。
      方法退出实际上就是该方法对应的栈帧在Java虚拟机栈中出栈的过程。一般来说执行的操作有:回复调用者的局部变量表,操作数栈。如果有返回值,则将返回值放入调用者栈帧的操作数栈中。程序计数器移动至调用处指令的后一条指令

    该区域规定了两种异常
    1.当线程请求的栈深度超过了虚拟机所允许的深度,则会抛出StackOverFlowError;
    2.如果虚拟机栈可以动态扩展(现在大部分都是可以的)则如果在扩展时无法申请到足够的内存会抛出OutOfMemoryError。

    这里结合下《java虚拟机规范中文版》第二章第五节中的描述:
    1.如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量的时候,java虚拟机将抛出一个StackOverFlowError异常。
    2.如果java虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutOfMemoryError异常。

    个人理解:一般在使用无限深度的递归时会抛出1,原因是因为程序不停的在方法中调用新的方法,所以达到一个量级后就会超过Java虚拟机栈所能容纳栈帧的最大深度,但是这里有一个问题,就是在无限深度的递归时也可能出现Java虚拟机栈内存的耗尽(因为每个方法的调用都会产生栈帧,也会往局部变量表中存东西,这些都要内存分配),那么这里是如何区分栈深度耗尽和栈内存耗尽的呢?这里结合了下查阅的资料。

    首先看下下面的代码:
    单线程的情况下,下面的代码都是抛出StackOverFlowError异常,也就是说在单线程的情况下,无论是由于Java虚拟机栈深度不足或者Java虚拟机栈内存不足,都会抛出StackOverFlowError异常。

public class MyJVMTest {
    public static void main(String [] args){
        MyJVMTest myJVMTest=new MyJVMTest();
        myJVMTest.StackTest();
    }
    public void StackTest(){
        StackTest();
    }
}

这里写图片描述

多线程情况:
因为运行后公司电脑CPU瞬间爆炸(仿佛闻到了CPU的香味_ (:3」∠*)_),建议慎重运行。跑了4-5分钟,还是没有耗尽内存,查看相关的资料,理论上是可以抛出如下异常的:
Exception in thread”main”java.lang.OutOfMemoryError:unable to create new native thread
综合了下资料后,简单地说:
a)StackOverflowError(方法调用层次太深,内存不够新建栈帧)
b)OutOfMemoryError(线程太多,内存不够新建线程)

    public static void main(String [] args){
        MyJVMTest myJVMTest=new MyJVMTest();
        myJVMTest.StackOutOfMemoryError();
    }
    //JVM Stack OOME
    public void StackOutOfMemoryError(){
        for(int i=0;;i++){
            System.out.println(i);
            Thread thread=new Thread(new Runnable(){
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    while(true){
                    }
                }
            });
            thread.start();
        }
    }

这里写图片描述


3. 本地方法栈:
(1)与Java虚拟机栈没有实际区别,只不过Java虚拟机栈是虚拟机用来执行Java方法(字节码)的,而本地方法栈是用来执行Native方法的,所以不同的虚拟机实现对这一块可以有不同的实现。
(2)与Java虚拟机栈一样,规定了StackOverFlowError和OutOfMemoryError异常。

4. Java堆:
(1)Java堆被所有线程共享。在虚拟机启动时创建。
(2)Java堆的唯一目的就是用来存放对象实例。几乎所有的对象实例都存储在Java堆中,但是因为现在新技术的出现,所以这点也不是绝对的了。
(3)是垃圾回收器管理的主要区域。
(4)可以是物理上不连续的内存空间,只要逻辑上连续即可。并且可以是固定大小的,也可以是可扩展的(现在大部分主流实现都是可扩展的)
(5)如果当堆中没有内存完成实例分配,并且也无法再被扩展时,会抛出OutOfMemoryError异常。
顺便,正好讲下遇到的其他几种OOME

//1
    public static void main(String [] args){
        MyJVMTest myJVMTest=new MyJVMTest();
        myJVMTest.MemoryTest();
    }
    //GC OOME
    public void MemoryTest(){
        Map<String,Object> memory=new HashMap<String,Object>();
        for(int i=0;;i++){
            double x=i;
            System.out.println(x);
            memory.put(Integer.toString(i), x);
        }
    }

这里写图片描述

//2
public class MyJVMTest {
    public static void main(String [] args){
        MyJVMTest myJVMTest=new MyJVMTest();
        myJVMTest.MemoryTest();
    }
    public void MemoryTest(){
        Map<String,Object> memory=new HashMap<String,Object>();
        for(int i=0;;i++){
            int[] testStr=new int[2000000];
            memory.put(Integer.toString(i), testStr);
        }
    }
}

这里写图片描述

上面2个其实都是Java堆抛出的,第一个所报的GC overhead limit exceeded异常,可以看做是垃圾回收器发出的警报,具体出现原因如下:
GC overhead limt exceed检查是Hotspot VM 1.6定义的一个策略,通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生。Sun 官方对此的定义是:“并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。“
对于上面的异常代码1,还可以引申出一些东西,就是Java的自动装箱机制和Java中所有的基本类型是否都是存在Java虚拟机栈中的?
第一点就是double类型在被put入Map中时,因为自动装箱机制的存在,所以会被包装为Double类型.
第二点比较容易被误导,实际上在方法中定义的基本变量是存放在Java虚拟机栈中的,比如方法中int i=0;其中变量i和0都是存在Java虚拟机栈中的局部变量表里的。而类中的基本类型成员,是全局变量,在实例化为对象时,是和对象实例一起存在Java堆中的。
所以在上面的代码1中,是由Java堆抛出的异常。而不是Java虚拟机栈抛出的。
还有一点就是上面的代码2中,如果将2000000改为20,则也会出现代码1的异常,这里对照Sun官方定义,也能很清楚的理解GC OOME出现的原因。

5. 方法区:
(1)方法区(Method Area)与Java堆一样,都是所有线程共享的内存区域。用来存放类信息,常量,静态变量,即时编译器编译后的代码等数据。Java虚拟机规范描述中方法区为Java堆的逻辑部分,但是方法区有一个别名Non-Heap(非堆),目的应该是与Java堆区分开来。
(2)方法区与Java堆一样,可以是物理上不连续的内存空间,大小也可以是固定的或者可扩展的。另外,还可以选择不实现垃圾回收器,相对而言该区域垃圾回收行为在该区域是比较少出现的,但也不是说存储在这里的数据就是永久的,通常这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。而且对该区域的垃圾回收效果一般比较难以让人满意。尤其是对类的卸载,条件极其苛刻。
(3)根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

6. 运行时常量池:
(1)方法区的一部分,Class文件中除了有版本,字段,方法,接口等描述信息外,还有一项是运行时常量池,用来存放编译时产生的各种字面量和符号引用,这些将在类加载后存放到方法区的运行时常量池中。
(2)Java虚拟机对Class文件中的每一部分都有严格的规定,但是对于运行时常量区来说,没有说明细节要求。不过一般来说除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
(3)运行时常量池相对于Class文件中常量池的另外一个重要特征是具备动态性,即并非只有被预置入Class文件常量池中的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池内。
(4)既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值