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

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

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

[TOC]

运行时数据区域

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


Java内存区域与内存溢出异常_第1张图片
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内存区域与内存溢出异常_第2张图片
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;
    }
}

对象的定位访问

两种主流方式:

  • 句柄
  • 直接指针

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


Java内存区域与内存溢出异常_第3张图片
句柄方式访问对象

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


Java内存区域与内存溢出异常_第4张图片
直接指针方式

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

OutOfMemoryError异常实战

你可能感兴趣的:(Java内存区域与内存溢出异常)