深入理解JVM(一)Java内存区域与内存溢出异常

4 篇文章 0 订阅

1.1 运行的数据区域

方法区 虚拟机栈 本地方法栈
堆 程序计数器
方法区,堆线程共享 其余的是线程隔离

  1. 线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内,每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。
  2. 线程共享区域随虚拟机的启动/关闭而创建/销毁

1.2 详解

1.2.1 程序计数器

程序计数器的作用就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。此内存区域是唯一一个在Java虚拟机中没有规定任何OutOfMemoryError情况的区域
/* Native方法 指本地方法,方法调用一些不是由Java所写的代码或方法中用Java 语言直接操纵计算机硬件*/

1.2.2 Java虚拟机栈(栈内存)

  1. Java虚拟机栈是线程私有的,虚拟机栈描述的是Java方法执行的内存模型每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链表,方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  2. 栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic
    Linking)、方法返回值和异常分派( Dispatch
    Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

  3. 局部变量表存放基本数据类型,64位长度的long和double类型占用2个局部变量空间,其余的数据类型只占用一个,所需的内存空间在编译期间完成分配,进入一个方法时,这个方法所需的局部变量空间是完全确定的,方法运行期间是不会改变变量表的大小。

    线程请求的栈深度大于虚拟机所允许的深度(递归过多),抛出StackoverflowError异常
    虚拟机栈进行动态扩展,如果扩展时无法申请到足够的内存,抛出OutOfMemoryError异常
    

1.2.3 本地方法栈

作用与虚拟机栈相似,虚拟机栈为虚拟机执行Java(字节码)服务,本地方法栈为Native方法服务

1.2.4 Java堆

  1. Java堆被所有线程共享,唯一的目的存放内存实例;所有的内存实例以及数组都要在堆上分配,但是现在所有对象都在堆上分配不是那么“绝对”。
  2. Java堆是内存管理的主要区域,也成为“GC堆”,基本享用分代收集算法,所以Java堆可细分为新生代(Eden 区、From
    Survivor 区和 To Survivor 区)和老年代。
  3. Java堆可以处于物理上不连续的内存空间中,只要在逻辑上连续即可。

如果在堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常

1.2.4.1 新生代

是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

1.2.4.1.2 Eden 区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

1.2.4.1.3 ServivorFrom

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

1.2.4.1.4 ServivorTo

保留了一次 MinorGC 过程中的幸存者。
(关于垃圾回收GC详细看下一章)

1.2.4.1.5 MinorGC 的过程(复制->清空->互换)

MinorGC 采用复制算法。
1:eden、servicorFrom 复制到 ServicorTo,年龄+1,首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
2:清空 eden、servicorFrom然后,清空 Eden 和 ServicorFrom 中的对象;
3:ServicorTo 和 ServicorFrom 互换最后,ServicorTo 和ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。

1.2.4.2 老年代

主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。 MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

1.2.4.3 永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被
放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这
也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

运行时期间,当我们需要实例化任何一个类时,JVM会首先尝试看看在内存中是否有这个类,如果有,那么会直接创建类实例;如果没有,那么就会根据类名去加载这个类,当加载一个类,或者当加载器(class loader)的defineClass()被JVM调用,便会为这个类产生一个Class对象(一个Class类的实例),用来表达这个类,该类的所有实例都共同拥有着这个Class对象,而且是唯一的。

1.2.5 方法区(线程共享)

  1. 又可以称为永久代,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
  2. 方法区可以选择不实现垃圾收集,方法区的内存回收目标主要是针对常量 池的回收和对内存的卸载。

当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

1.2.6 JAVA8 与元数据

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。

1.2.7 运行时常量池

运行时常量池也是方法区的一部分。用于存放编译期生成的各种字面量和 符号引用。
当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

1.3.对象

1.3.1 对象的创建

虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池 中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载,解析,初始化过,如果没有,需要执行相应的类加载过程
类加载检查通过后,为新生对象分配内存,如果堆内存是绝对规整的,用过的在一边,空闲的在一边。中间放着一个指针作为分界点的指示器,那么分配内存就是将指针向空闲区移动,这种方式叫指针碰撞;如果内存并不规整,虚拟机就必须维护一个列表,记录哪些内存块可用,分配的时候,找到一块足 够大的空间划分给对象,并更新列表上的记录,这种方式称为“空闲列表”。

解决线程安全问题有两个方案:
一是对分配内存空间的动作进行同步处理。
另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地分配缓存(TLAB),那个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
内存分配完成后,虚拟机需要将分配到的内存空间初始化为零值(不包括对象头)。
接下来,虚拟机要对对象进行必要的设置,例如哈希码,分代年龄等信息,这些信息存放在对象的对象头之中。

1.3.2 对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头,实例数据,对齐填充。
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈 希码,GC分代年龄等。
对象头的另一部分是类型指针,及对象指向他的类元素指针,虚拟机通过 这个指针来确定这个对象是哪个类的实例。另外如果对象是个Java数组,那在对象头中还必须存一块记录数组长度的数据。
实例数据部分的对象是对象真正存储的有效信息,也就是程序代码中所定 义的各种类型的字段内容。
第三部分对齐填充不是必然存在的,没有特别意义,仅仅起到栈位符的作 用,实例部分没有对齐时,通过对齐填充来补全。

1.3.3 对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象
主流的访问方式有使用句柄和直接指针两种。
访问对象的两种方式在这里插入图片描述

句柄优点:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改
直接指针优点:速度块,节省了一次指针定位的时间开销。

参考资料:《深入理解Java虚拟机》 周志明著

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值