Java虚拟机01——Java内存数据区域和内存溢出异常

运行时数据区域

Java虚拟机在执行Java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。这些区域都有各自的用途,以及建立和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而创建和销毁。根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包括如下几个运行时数据区域,以下图所示:java

内存模型.jpeg

咱们能够将上面的数据区域分为线程独有、线程共享及其余三大区域:算法

1.1. 线程独有的数据区域

1. 程序计数器(Program Counter Register)

  1. 当前线程所执行的字节码的行号指示器。
  2. 用于选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复须要依赖这个计数

2. 虚拟机栈(Java Stack)

  • 位于线程私有的内存中,生命周期与线程相同。
  • 描述了Java方法执行的内存模型。
  • 方法执行时使用栈帧(Stack Frame)来存储局部变量表、操做数栈、动态连接、方法出口等信息。
  • 若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常。
  • 若是虚拟机栈能够动态扩展,若是扩展时没法申请到足够的内存,就会抛出OutOfMemoryError异常。

3. 本地方法栈(Native Method Stack)

  • 与虚拟机栈相相似,区域在于本地方法栈为虚拟机使用到的Native方法服务。
  • 能够由虚拟机设计者本身实现。
  • 本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常

1.2. 线程共享的数据区域

1. Java堆(Heap)

  • 是Java虚拟机所管理内存中最大的一块,在虚拟机启动时建立。
  • 在Java虚拟机规范中的描述是:全部的对象实例以及数组都要在堆上分配。随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术致使某些对象并无分配在堆上。
  • Java GC工做的主要区域。现代收集器基本都采用分代收集算法,因此Java堆中还能够细分为新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。
  • 若是在堆中没有内存完成实例分配,而且堆也没法再扩展时,将会抛出OutOfMemoryError异常。

2. 方法区(Method Area)【Java8中去除了永久代 // TODO】

  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 它有一个别名叫作Non-Heap(非堆),目的应该是与Java堆区分开来。
  • HotSpot虚拟机选择把GC分代收集扩展至方法区,即便用永久代来实现方法区,所以也有人将此区域称为“永久代”;JDK 1.7的HotSpot中,已经把本来放在永久代的字符串常量池移出,并逐步改成采用Native Memory来实现方法区的规划。
  • 根据Java虚拟机规范的规定,当方法区没法知足内存分配需求时,将抛出OutOfMemoryError异常。

3. 运行时常量池(Runtime Constant Pool)

  • 运行时常量池是方法区的一部分。
  • 用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。
  • 当常量池没法再申请到内存时会抛出OutOfMemoryError异常。

1.3. 其余区域

直接内存(Direct Memory)

  • 直接内存并非虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
  • 这部份内存也可能致使OutOfMemoryError异常出现。

对象的建立

Java是一门面向对象的语言,在Java程序运行的过程当中无时不刻都有对象被建立。在语言层面,建立对象一般是一个new关键字,可是,在虚拟机中,建立对象包括以下流程:shell

类加载 --> 分配内存 --> 内存空间初始化零值 --> 对象头设置 --> init初始化数组

  • 虚拟机遇到一个new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的引用,而且检查这个符号引用表明的类是否已被加载、解析和初始化过。若是没有,那必须先执行相应的类加载过程。
  • 在类加载经过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在加载后就肯定。

分配内存的方式为:
  “指针碰撞”:在内存规整状况下,将指针向空闲空间挪动一段与对象大小相等的距离。
  “空闲列表”:在内存不规整状况下,虚拟机维护一个记录内存可用的列表,分配的时候从列表中找到一块空间划分给对象。
并发状况下的内存分配:
  同步:对分配内存空间的动做进行同步处理———采用CAS配上失败重试的方式,保证更新操做的原子性
  本地线程分配缓冲(TLAB):把内存分配动做按照线程划分在不一样空间中。即每一个线程在Java堆中预先分配一块内存TLAB,只有TLAB用完并从新分配新的TLAB时才须要同步。bash

  • 将分配到的内存空间都初始化零值,如int a,a默认为0,若是使用TLAB,则这个工做提早到TLAB
  • 对象头设置:对象是哪一个类的实例,对象的哈希码,对象的GC分代年龄等信息。
  • 执行init方法,即执行程序定义的构造方法。

对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储布局能够分为3块区域:对象头(Header)、实例数据(Instance Data)和对其填充(Padding)数据结构

  • 对象头:
    1.用于存储对象自身运行时数据(Mark Word):哈希码GC分代年龄锁状态标志线程持有锁偏向线程ID偏向时间戳等。被设计成非固定的数据结构,能服用存储空间
    2.类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。若是对象是Java数组,还必须有一块记录数组长度的数据。
  • 实例数据: 在程序代码中定义的各类类型的字段内容。这部分的存储会周到虚拟机分配策略的影响。HotSpot虚拟机的默认分配策略:相同宽度的字段老是被分配到一块儿。父类中定义的变量会出如今子类前,子类中较窄的变量也可能会插入到父类变量的空隙之中。
  • 对齐填充: 仅仅起到占位符的做用。HotSpot要求对象的起始地址必须是8字节的整数倍。当对象实例数据部分没有对齐时,就须要经过对齐填充来补充。

对象的访问定位

创建对象是为了使用对象。咱们的Java程序须要经过栈上的reference数据来操做堆上的具体对象。目前主流的访问方式有两种:句柄和直接指针。并发

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

句柄.jpg

直接访问:reference指针存储的直接就是对象地址测试

直接地址.jpg

  使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(如垃圾收集时)时只会改变句柄中的实例数据指针,而reference自己不须要修改优化

  直接访问最大的好处就是速度快。节省了一次指针定位的时间开销。HotSpot虚拟机使用第二种方式进行对象的访问。

OutofMemoryError异常实战

堆溢出

-Xms 堆最小值 -Xmx 堆最大值 -XX:HeapDumpOnOutOfMemoryError能够在虚拟机出现异常时将堆存储快照

-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
复制代码
public class HeadOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}
复制代码

运行结果:

image.png

虚拟机栈和本地方法栈溢出

-Xss 设置栈的大小

  • 若是线程请求的栈深度大于虚拟机所容许的最大深度,抛出StackOverflow异常
  • 若是虚拟机在扩展栈时没法申请到足够的空间,则抛出OutOfMemoryError异常
-Xss228k
复制代码
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength ++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF stackSOF = new JavaVMStackSOF();
        stackSOF.stackLeak();
    }
}
复制代码

运行结果:

image.png

实验结果代表,在单线程下,当内存没法分配的时候,虚拟机抛出的都是StackOverflow异常

测试:建立线程致使内存溢出异常

public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {

        }
    }

     public void stackLeakByThread() {
        while (true) {
            new Thread(() -> {
                dontStop();
            }).start();
        }
     }

    public static void main(String[] args) {
        JavaVMStackOOM javaVMStackOOM = new JavaVMStackOOM();
        javaVMStackOOM.stackLeakByThread();
    }
}
复制代码

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

String.intern()方法返回的是常量池中的对象,若是池中没有对象,则建立对象返回引用

在JDK 1.6及以前的版本中,因为常量池分配在永久代内,咱们能够经过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量,测试代码:

public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
复制代码
相关文章
相关标签/搜索