zoukankan      html  css  js  c++  java
  • JVM 内存区域总结

    运行时数据区域

    ​ Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。从下面这张图可以看出来,Java数据区域分为五大数据区域

    程序计数器

    ​ 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

    为什么需要程序计数器?

    ​ 我们知道对于一个处理器(如果是多核cpu那就是一个内核),在一个确定的时刻都只会执行一条线程中的指令。因此,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计器,不同线程之间的程序计数器互不影响,独立存储。我们称这类内存区域为“线程私有”的内存

    注意:如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError(OOM,内存溢出异常)情况的区域。

    Java虚拟机栈

    ​ 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

    ​ 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

    我们常说的栈就是虚拟机栈,或者说是虚拟机中局部变量表部分。

    局部变量表存放的是编译期可知的8种基本数据类型对象引用返回地址类型。(其中64位长度的long和double会占2个局部变量空间,其他的数据类型只占1个)

    本地方法栈

    ​ 本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

    此区域也会抛出StackOverflowError和OutOfMemoryError异常

    java堆

    ​ 对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建(也就是说,在JVM中只有一个堆)。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

    ​ 这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

    根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

    方法区

    ​ 方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

    根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

    补充

    运行时常量池

    ​ 是属于方法区的一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() 方法生成的常量)都可以将常量放入池中。

    内存有限,无法申请时抛出 OutOfMemoryError。

    直接内存

    不是虚拟机运行时数据区的一部分,但会被频繁地使用。

    ​ 在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
    OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

    HotSpot虚拟机对象探秘

    对象的创建

    ​ 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。

    ​ 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

    而根据java堆中的内存是否规整,为对象分配内存的方式分为以下2种:

    指针碰撞

    ​ 假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。

    空闲列表

    ​ 如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。

    因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

    垃圾回收过程请参考其他文章。

    除了考虑如何划分空间之外,还需要考虑一个线程安全问题。解决这个问题有两种方案:

    ​ 一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。

    ​ 另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

    ​ 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。

    ​ 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

    ​ 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。执行new指令之后会接着执行<init>方法,这样一个真正可用的对象就完全产生出来。

    简单地说,对象的创建的步骤:为对象分配内存空间 --> 对分配的空间初始化为零 --> 对对象进行必要的设置 --> 数据初始化

    对象的内存布局

    在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)

    对象头(Header) :包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

    实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

    对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

    对象的访问定位

    ​ 对象的使用,是通过栈上的引用数据来操作堆上的具体对象。栈对堆的访问方式,主流的有2种:

    使用句柄访问

    ​ Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图:

    使用直接指针访问

    ​ Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。如图:

    两者比较:

    使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。

    直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。

    对于虚拟机Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的。

    欢迎访问个人博客:http://www.itle.info/2020/08/25/JVM%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/

  • 相关阅读:
    最快效率求出乱序数组中第k小的数
    调整数组顺序使奇数位于偶数前面
    分治算法的完美使用----归并排序
    快速排序分区以及优化方法
    分治法以及快速排序
    高效求a的n次幂的算法
    最长连续递增子序列(部分有序)
    在有空字符串的有序字符串数组中查找
    旋转数组的最小数字(改造二分法)
    递归----小白上楼梯
  • 原文地址:https://www.cnblogs.com/luler/p/13936800.html
Copyright © 2011-2022 走看看