Java基础-JVM内存管理-堆内存

作者: HughJin | 来源:发表于2021-03-05 09:20 被阅读0次

    Java工程师知识树 / Java基础


    概要

    存在一个堆内存,堆也是 java 内存管理的核心区域。Java 堆区在 JVM 启动的时候即被创建,其空间大小也就确定了。是 JVM 管理的最大的一块内存空间。

    《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

    所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)

    《Java虚拟机规范》中对 Java 堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。( The heap is the run-time data area from which memory for all class instances and arrays is allocated)

    数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象在堆中的位置。

    在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

    堆,是 GC (Garbage Collection,垃圾收集器) 执行垃圾回收的重点区域。

    一句话描述:Java堆是JVM内存中最大的一块区域,JVM几乎所有的对象实例都在堆里分配内存并创建。堆内部区域的划分取决于JVM的垃圾回收策略,即GC策略。目前主流的GC策略大部分是基于分代收集算法的,如 parNew+CMS,或者G1等等。因此我们可以将Java堆再划分为新生代老年代

    存储数据

    堆中存储:1.运行时类实例(即对象,包含全局变量);2.运行时数组;3.静态变量;4.运行时常量池。

    不能在栈上存储数组和对象,因为栈帧被设计为创建以后无法调整大小。

    栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧:调用方法创建栈帧)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。

    对象和数组永远不会显式回收,而是由垃圾回收器自动回收。

    通常,过程是这样的:

    1. 新的对象和数组被创建并放入新生代。
    2. Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。
    3. Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。
    4. 每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。

    堆的限制

    OutOfMemoryError产生原因主要包含下面几种情况:

    • 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
    • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
    • 代码中存在死循环或循环产生过多重复的对象实体;
    • 使用的第三方软件中的BUG;
    • 启动参数内存值设定的过小;

    堆空间的大小设置:

    -Xms 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize ,eg:-Xms256M
    -Xmx 用于表示堆区的最大内存,等价于-XX:MaxHeapSize ,eg:-Xmx256M

    一旦堆区中的内存大小超过 “-Xmx" 所指定的最大内存是,将会抛出 OutOfMemoryError异常。
    通常会将 -Xms-Xmx 两个参数配置相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

    默认情况下,初始内存大小:物理电脑内存大小 / 64最大内存大小:物理电脑内存大小 / 4

    堆内存区域

    堆被分为了下面三个区域:

    新生代:1.Eden区(伊甸区);2.Survivor区(幸存者区)0.1
    老年代:养老区
    永久代(jdk7以前)/元空间(jdk8以后):1.8开始持久代被废弃,使用元空间代替,元空间MetaSpace并不是堆内存的一部分而是本地内存。

    堆区与非堆区的区别

    第一,Perm Gen(永久代)--1.8元空间

    元空间是jdk8以后才加入的,用来替换原来的永久代。也就是说,原perm区(永久代)中的方法区,也在这里。从它原来的名字就可以看出来,永久代指的就是那些变动很少的数据,稳定为主。比如我们在jvm启动时,加载的那些class文件;以及在运行时,动态生成的代理类。

    元空间的大小,默认是没有上限的。极端情况下,会一直挤占操作系统的剩余内存。

    第二、CodeCache(代码缓存区)

    CodeCahe存放的,就是即时编译器所生成的二进制代码。当然,JNI的代码也是放在这里的。

    这个空间在不同的平台,大小都是不一样的,但一般够用了。但是把这个区域调的非常的小的情况下,JVM不会溢出,这个区域也不会溢出,但是会退化成解释型执行模式,速度和JIT不可同日而语,慢个数量级也是可能的。

    第三、年轻代与老年代

    存储在 JVM 中的 Java 对象可以被划分为两类:

      1. 一类是生命周期较短的瞬时对象,这类对象在创建和消亡都非常迅速
      1. 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与 JVM 的生命周期保持一致。

    Java堆区进一步细分的话,可以划分为年轻代(YoungGen) 和老年代(OldGen)。年轻代又称为新生代/新生区。老年代又称为老年区/养老区。

    其中年轻代有可以划分为 Eden 空间 、Survivor0 空间和 Survivor1 空间(有时也叫做 from区、 to 区)

    年轻代与老年代关系

    大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。

    需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

    配置年轻代与老年代在对结构的占比:

    年轻代与老年代的比例默认时 1:2;年轻代区的默认比例时 8:1:1

    • 默认 -XX: NewRaio = 2, 表示新生代占1, 老年代占2, 新生代占整个堆的 1/3
    • 可以修改 -XX: NewRatio = 4, 表示新生代占1, 老年代占4, 新生代占整个堆的1/5。

    在 HotSpot 中,Eden 空间和另外两个 SurvIvor 空间缺省所占的比例是 8:1:1 开发人员可以通过选项 -XX:SurvivorRatio 调整整个空间比例。比如: -XX:SurvivorRatio = 8

    几乎所有的 Java 对象都是在 Eden 区被new 出来的。绝大部份的 Java 对象的销毁都年轻代在进行了。可以使用选项 -Xmn 设置年轻代最大内存大小。

    第四、对象分配与回收过程

    • 针对幸存者s0, s1区的总结:复制之后有交换,谁空谁是to。
    • 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。

    对象的分配策略

    (1)对象优先在Eden空间分配

    当我们创建一个对象时,若没有特殊情况,这个对象会被分配在新生代的Eden空间。此时,若Eden空间不足,无法提供这个对象所需的空间时,将会触发垃圾回收机制,清理Eden空间,为新对象腾出更多空间。在进行垃圾回收时,会将Eden中仍然存活的对象放入一个空闲的Survivor中,但是若Survivor空间不足以存放这些存活的对象,则由于担保机制的存在,这些对象会被放入到老年代中。

    (2)大对象直接在老年代分配

    假设我们在代码中创建了一个很大的对象(比如数组或较长的字符串),而这个对象在很长一段时间都要使用,不会轻易被当作垃圾回收。这时候将面临一个问题:若将这个对象分配在新生代的Eden中,每次进行垃圾回收时,都需要将这个大对象复制到Survivor中保留,这个复制过程是一笔很大的开销,而且由于大对象占用了大量空间,垃圾回收将会频繁发生。所以,为了避免这种情况的发生,对于较大的对象,将会被直接分配到老年代中,原因是老年代发生垃圾回收的频率较低,而且不是使用复制算法进行垃圾回收。

    那如何判断一个对象是否属于大对象呢?JVM提供一个参数-XX:PretenureSizeThreshold,通过这个参数来设定多大属于大对象。当需要为一个对象分配空间时,若此对象所需的空间大于这个参数的值,就会被判定为一个大对象,从而在老年代中为其分配空间。

    (3)长期存活的对象将进入老年代

    这个原则合情合理,老年代存在的首要目的,就是存放生命周期较长的对象,所以对于新生代中存活了很长时间的对象,就应该把他们移入老年代,而不是一直留在新生代中占用空间。每一个对象都有一个自己的年龄计数器,记录了自己的存活周期。对于新生代中的对象,初始时刻它的年龄计数器为0,每经历一次垃圾回收后,年龄计数器+1。当计数器的值到达设定好的阈值时(默认是15),就证明这个对象是一个”老油条“,于是将它转入到老年代中。

    对于生命周期长的对象,由于不会轻易死亡,所以每一次垃圾回收都会被它拖慢,而且垃圾回收对于短时间内不会死亡的对象也没有意义,所以不应该将它留在垃圾回收频繁的新生代占用空间,这就是需要将这种对象转让老年代的理由。JVM中也提供了一个参数-XX:MaxTenuringThreshold来设置对象进入老年代的阈值,当对象的年龄计数器超过这个值时将进入老年代。

    (4)动态年龄判断

    对于新生代中的对象,并不一定需要年龄计数器到达阈值才被放入老年代,有一种特殊情况会导致对象直接进入老年代。当新生代的Survivor空间中,某一个年龄的对象相加,所占空间总和超过了Survivor空间的一半,则大于或等于这个年龄的对象将直接进入老年代,而不需要到达阈值。比如说,在Survivor空间中,年龄为5的对象相加,所占空间超过了Survivor总空间的一半,则所有年龄>=5的对象,会被直接转入老年代。

    第五、新生代与老年代使用的GC算法

    对于新生代而言,这一块区域中的对象存活时间短,每一次垃圾回收都能回收大部分内存,所以适合使用复制算法进行垃圾回收,同时以老年代作为这个算法的担保空间;

    对于老年代而言,每次垃圾回收只能释放小部分空间,若使用复制算法,每次将需要做大量复制,而且此时Survivor需要较大的空间,所以不适合使用复制算法,因此在老年代中,一般使用标记—清除或者标记—整理算法进行垃圾回收。

    相关文章

      网友评论

        本文标题:Java基础-JVM内存管理-堆内存

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