美文网首页
Java内存区域与内存溢出异常

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

作者: wang_zd | 来源:发表于2019-01-08 21:16 被阅读6次

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

    @(Java虚拟机)[jvm, 内存]

    [TOC]

    运行时数据区域

    Java虚拟机执行Java程序时会将内存分为不同的数据区域。


    Java虚拟机运行时数据区

    程序计数器

    PC可看作当前线程执行字节码的行号指示器。执行Java方法时PC记录的是正在执行虚拟机字节码指令的地址,如果是Native方法,则计数器的值为空。在Java虚拟机规范中唯一一个没有规定OutOfMemoryError的区域

    Java虚拟机栈

    线程私有,和线程的生命周期相同。
    每个方法执行时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息
    每个方法从调用到执行完毕,就是栈帧的入栈出栈过程。
    (传说中堆内存(Heap)和栈内存(Stack)中的栈)-->关注的局部变量部分

    规范中规定了两种异常:

    1. StackOverflowError :线程请求的栈深度超过虚拟机的允许范围。
    2. OutOfMemoryError: 虚拟机可以动态扩展(规范中允许固定长度的虚拟机栈),扩展时无法申请足够的内存。

    本地方法栈

    为虚拟机使用到Native方法服务,规范没有强制规定实现。
    同上也有两个异常

    Java堆

    Java虚拟机管理内存最大的一块,线程共享,虚拟机启动时创建,存放所有对象实例和数组(非绝对)。
    Java堆是GC的主要区域,现在垃圾收集器基本都采用分代收集算法。后面详解
    Java堆可以处于物理上不连续的内存空间,逻辑连续即可。实现可以是固定和可扩展的。主流按可扩展实现(通过-Xmx和-Xms控制)

    规定异常:
    OutOfMemoryError:没有内存完成分配实例并无法扩展

    方法区

    线程共享,存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。规范把方法区描述为堆的一部分,但是有个别名Non-Heap,目的应该是区分Java堆。

    HotSpot把GC分代手机扩展至方法区。实现方法区不受规范约束。HotSpot用永久代实现方法区会有一定问题(-XX:MaxPermSize上限)容易内存溢出,而且有极少数方法(例如String.intern())在不同的虚拟机有不同表现。HotSpot已经逐步放弃使用永久代实现方法区,改为采用Native Memory实现。在1.7JDK中将放在永久代的字符串常量池移除。

    和Java堆一样可使用不连续的内存,可选择固定大小和可扩展,可选择不实现垃圾收集。该区域内存回收的主要目标是常量池和类型的卸载。

    规定异常:
    OutOfMemoryError:方法区无法满足内存分配需求时。

    运行时常量池

    运行时常量池是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息,还有常量池,用于存放编译期生成的各种字面量和符号引用。

    运行时常量池具备动态性,Java语言不要求常量编译期才能生成,运行期间也可将新的常量放入池中,如String的intern方法。

    异常同方法区

    直接内存

    直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域,这部分内存频繁使用,也可导致OutOfMemoryError。
    NIO,引入了基于通道与缓冲区的IO方式,可以使用Native函数库直接分配堆外内存,通过存储在Java堆上的DirectByteBuffer对象作为这块内存的引用进行操作。
    受本机总内存大小和处理器寻址空间的限制。配置虚拟机参数时不能忽略。不能使内存区域总和大于物理内存限制。


    HotSpot虚拟机对象探秘

    剖析HotSpot的Java堆的对象分配,布局和访问过程。

    对象的创建

    虚拟机遇到new时,先检查这个指令的参数是否能找到一个类的符号引用,在检查该代表的类是否被加载,解析和初始化过。没有则先执行类加载过程。

    类加载检查通过后,虚拟机需要为新生对线分配内存,大小在类加载完成后便已经确定。

    分配内存方式分为两种:

    • 指针碰撞:Java堆内存规整,分配时移动指针即可
    • 空闲列表:Java堆内存不规整,需要记录那些内存块可用。分配完时应更新记录。

    Java堆是否规整又由垃圾收集器是否带压缩整理功能决定。

    Java堆是线程共享的,存在线程安全问题。解决办法两种:

    • 对分配内存动作同步处理:虚拟机采用CAS+失败重试保证更新操作的原子性。
    • 把内存分配按线程划分在不同空间下:每个线程在Java堆中预先分配一块内存,称本地线程分配缓冲(TLAB),TLAB用完时并重新分配TLAB才需要同步锁定。通过-XX:+/-UseTLAB设定是否使用TLAB

    内存分配完成后需要对空间初始化话零值。可以在Java代码中使用不赋初始值的字段。然后虚拟机需要对对象进行设置,如该对象是那个类的实例,如何找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息。这些信息存放在对象头中。
    从Java视角看对象的< init >方法还没执行,还需要执行init方法。

    对象的内存布局

    Java对象.png

    Java对象头mark word存储自身运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。

    Java数组对象头:

    • java数组对象头在未压缩指针情况下16B+8B(存储数组长度)=24B
    • java数组对象头在压缩指针情况下12B+4B(存储数组长度)=16B

    reference压缩下占4B

    测试代码:
    使用详情jvm-obj-size

    public class ObjSizeFetcherTest {
     public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
            long size = 0;
            long fullSize = 0;
            
            Object obj=new Object();
            size = ObjSizeFetcher.sizeOf(obj);
            fullSize = ObjSizeFetcher.fullSizeOf(obj);
            msg("obj", size, fullSize);
            // +UseCompressedOops: size = 16=12(对象头)+0(实例数据)+4(对齐填充), fullSize = 16
            // -UseCompressedOops: size = 16=16(对象头)+0(实例数据)+0(对齐填充),fullSize = 16
            
            A a = new A();
            size = ObjSizeFetcher.sizeOf(a);
            fullSize = ObjSizeFetcher.fullSizeOf(a);
            msg("a", size, fullSize);
            // +UseCompressedOops: size = 16=12(对象头)+4*1(实例数据)+0(对齐填充), fullSize = 16
            // -UseCompressedOops: size = 24=16(对象头)+4*1(实例数据)+4(对齐填充), fullSize = 24
    
            B b = new B();
            size = ObjSizeFetcher.sizeOf(b);
            fullSize = ObjSizeFetcher.fullSizeOf(b);
            // +UseCompressedOops: size = 24=12(对象头)+4*2(实例数据)+4(对齐填充), fullSize = 24
            // -UseCompressedOops: size = 24=16(对象头)+4*2(实例数据)+0(对齐填充), fullSize = 24
            msg("b", size, fullSize);
    
            size = ObjSizeFetcher.sizeOf(new int[0]);
            fullSize = ObjSizeFetcher.fullSizeOf(new int[0]);
            msg("int[0]", size, fullSize);
           // +UseCompressedOops: size = 16=12(对象头)+4(存放长度)+0(实例数据)+0(对齐填充), fullSize = 16
           // -UseCompressedOops: size = 24=16(对象头)+8(存放长度)+0(实例数据)+0(对齐填充), fullSize = 24
            
            size = ObjSizeFetcher.sizeOf(new Integer[0]);
            fullSize = ObjSizeFetcher.fullSizeOf(new Integer[0]);
            msg("Integer[0]", size, fullSize);
            // +UseCompressedOops: size = 16, fullSize = 16
            // -UseCompressedOops: size = 24, fullSize = 24
            
            size = ObjSizeFetcher.sizeOf(new int[2]);
            fullSize = ObjSizeFetcher.fullSizeOf(new int[2]);
            msg("int[2]", size, fullSize);
            // +UseCompressedOops: size = 24, fullSize = 24
            // -UseCompressedOops: size = 32, fullSize = 32
            
            size = ObjSizeFetcher.sizeOf(new Integer[2]);
            fullSize = ObjSizeFetcher.fullSizeOf(new Integer[2]);
            msg("Integer[2]", size, fullSize);
            // +UseCompressedOops: size = 24, fullSize = 24
            // -UseCompressedOops: size = 40, fullSize = 40
            
            size = ObjSizeFetcher.sizeOf(new float[0]);
            fullSize = ObjSizeFetcher.fullSizeOf(new float[0]);
            msg("float[0]", size, fullSize);
            // +UseCompressedOops: size = 16, fullSize = 16
            // -UseCompressedOops: size = 24, fullSize = 24
            
            size = ObjSizeFetcher.sizeOf(new Float[0]);
            fullSize = ObjSizeFetcher.fullSizeOf(new Float[0]);
            msg("Float[0]", size, fullSize);
            // +UseCompressedOops: size = 16, fullSize = 16
            // -UseCompressedOops: size = 24, fullSize = 24
            
            size = ObjSizeFetcher.sizeOf(new A[0]);
            fullSize = ObjSizeFetcher.fullSizeOf(new A[0]);
            msg("A[0]", size, fullSize);
            // +UseCompressedOops: size = 16, fullSize = 16
            // -UseCompressedOops: size = 24, fullSize = 24
    
            size = ObjSizeFetcher.sizeOf(new char[2]);
            fullSize = ObjSizeFetcher.fullSizeOf(new char[2]);
            msg("char[2]", size, fullSize);
            // +UseCompressedOops: size = 24, fullSize = 24
            // -UseCompressedOops: size = 32, fullSize = 32
    
            String s = new String("aaaaaaaa");
            size = ObjSizeFetcher.sizeOf(s);
            fullSize = ObjSizeFetcher.fullSizeOf(s);
            msg("s", size, fullSize);
            //String有2个变量
            // +UseCompressedOops: size = 24=12+4(hash)+4(value指针)+4(padding),fullSize = 56
            // -UseCompressedOops: size = 32, fullSize = 72
    
            C c = new C(a, b);
            size = ObjSizeFetcher.sizeOf(c);
            fullSize = ObjSizeFetcher.fullSizeOf(c);
            msg("c", size, fullSize);
            // +UseCompressedOops: size = 24=12(头)+2*4(引用)+4(填充), fullSize = 64=12+2*4(引用)+16(A)+24(B)+4(padding)
            // -UseCompressedOops: size = 32, fullSize = 80
    
        }
    
        private static void msg(String obj, long size, long fullSize) {
            System.err.println(obj + "--> size = " + size + ", fullSize = " + fullSize);
        }
    
    }
    
    class A {
        int a;
    }
    
    class B {
        int a;
        int b;
    }
    
    class C {
        A a;
        B b;
    
        public C(A a, B b) {
            this.a = a;
            this.b = b;
        }
    }
    

    对象的定位访问

    两种主流方式:

    • 句柄
    • 直接指针

    句柄方式需要在内存中划分一块作为句柄池。


    句柄方式访问对象

    直接指针方式需要考虑如何放置访问类型数据的相关信息,reference中存储的是对象的地址。


    直接指针方式

    两种方式各有优势:
    句柄方式最好的是reference中稳定的句柄地址。对象移动改变句柄的实例数据指针即可。reference不需改变。
    直接访问方式最好的是速度快,节省指针定位开销。HotSpot采用直接访问方式。

    OutOfMemoryError异常实战

    相关文章

      网友评论

          本文标题:Java内存区域与内存溢出异常

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