运行时数据区(三)之堆(三)

概述

  • 一个jvm实例只存在一个堆内存, 堆也是java内存管理的核心区域。
  • java堆区再jvm启动的时候被创建, 其空间的大小也确定了, 是jvm管理最大的一块内存空间。
  • 堆内存的大小是可以调节的。再运行之前设置~Xms10m ~Xmx10m 初始堆的大小为10MB 最大10mb
  • 堆可以处于物理上不连续的内存空间 但在逻辑上它应该是连续的
  • 所有线程共享java堆, 在这里还可以划分线程私有的缓冲区(TLAB)
  • java虚拟机规范 中队java堆的描述是: 所有(实际是几乎)的对象实例以及数组都应当再运行时分配在堆上。
  • 数组和对象可能永远不会存储再栈上, 因为栈帧中保存引用, 这个引用指向对象或者数组在堆中的位置
  • 在方法结束后, 堆中的对象不会马上被移除,仅仅在垃圾回收的时候才会被移除
  • 堆, 是GC执行垃圾回收的重点区域
  • 在这里插入图片描述

堆的内存细分

  • jdk7 之前堆内存逻辑上分为三部分: 新生区+养老区 + 永久区 事实上是新生区 + 养老区
  • java8 及之后堆内存逻辑上分为 新生区 + 养老区 + 元空间 事实上是新生区 + 养老区
  • 新生区=新生代=年轻代 养老区=老年区=老年代 永久代=永久区(方法区的具体实现下章在讲)
  • 在这里插入图片描述

对空间大小的设置

  • -Xms 用来设置堆空间(年轻代 + 老年代)的初始化内存大小
    • -X 是jvm的运行参数
    • ms 是memory start
  • -Xmx 用来设置堆空间的(年轻代+老年代)的最大内存大小
  • 手动设置 -Xms -Xmx
  • 开发中建议将初始堆内存和最大的堆内存设置成相同的值
  • 在这里插入图片描述

在这里插入图片描述

OOM

  • OutOfMemory

在这里插入图片描述

在这里插入图片描述

年轻代与老年代

  • 存储在jvm中的java对象可以被划分为二类

    • 一类是生命周期较短的顺时对象, 这类对象的创建和消亡都非常的迅速。
    • 另外一类对象的生命周期却非常长, 在某些极端的情况还能够与jvm的生命周期保持一致
    • java堆区进一步细分为 年轻代和老年代
    • 年轻代又可以划分为Eden空间、surviver0(from)、survivor1(to)
    • 在这里插入图片描述
  • 默认的NewRatio为1:2 设置 老年代是新生代的2倍
    在这里插入图片描述

  • 默认情况下eden空间和survivor空间的比例为8:1:1

  • 可以利用 -XX:SurvivorRation调整这个空间的比例。

  • 实际上运行的适合不是8:1:1, 因为还有自适应的内存分配策略

  • 要使就是8:1:1就可以用-XX:SurvivorRatio =8显式赋给

  • 在这里插入图片描述

  • 几乎所有的的java对象都是在Eden区给new出来的(如果Eden装不下的话就直接进去老年区了)

  • 绝大部分的人java对像的销毁都是在新生代进行了、

    • 可以使用选项“-Xmn” 设置新生代最大内存的大小,一般不设置

对象分配的过程

  • 为新对象分配内存是一件非常严谨和复杂的, jvm设计者不仅要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关, 所以还需考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
  • new 的对象先房贷Eden区。 此区有大小限制
  • 当Eden区的空间填满的时(触发的是YGC/MinorGc), 程序有需要创建对象, JVM的垃圾回收器将对Eden区进行垃圾回收, 将Eden区中的不在被其他对象所引用的对象进行销毁,在加载新的对象放到Eden区
  • 然后将Eden区中的剩余对象移动到幸存者0区此时为每一个对象分配一个年龄计数器age赋值为1
  • 始终放到survivor这两个区中空的哪一个谁空谁是to区。
  • 如果再次触发Gc, 此时Eden幸存下来对象age赋值为1以及上在幸存者0区的并且被没有回收age=2,就会放到幸存者1区
  • 如果再次Gc, 此时会重新放回幸存者0区, 接着再去幸存者1区
  • 啥时候能去养老区呢, 可以设置次数, 默认时年龄计数器达到15(阈值)次, 晋升(promotion)到老年代
  • 幸存者满了不会触发YGC只有Eden满的时候会触发,触发的时候会对Eden以及survivor一起回收在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
  • 可能幸存者区age不到15就会晋升到老年代
  • 对象也有可能对象就在老年代中new出来的
  • 在老年区,相对悠闲。当养老区内存不足的时候, 再次触发Gc:Major GC, 进行养老区的内存清理
  • 如果养老区执行了Major Gc之后还是发现依然无法进行对象的保存, 就会产生OOM异常
  • 总结:
    • 针对于幸存者区:复制之后有交换, 谁空谁是to
    • 关于垃圾回收: 频繁在新生代手机, 很少在养老区手机, 几乎不在元空间收集。
  • 对象分配的特殊情况
  • 新生代放不下(特别大的对象)直接放到老年代
  • 老年代放不下就进行FGc/MajorGcha还是不够就OOM在这里插入图片描述

Minor Gc、MajorGc、Full Gc

  • jvm在进行GC时, 并非每次都对上面三个内存(新生代, 老年代, 方法区)区域一起回收的, 大部分时候回收的都是指新生代
  • 针对HotSpot Vm的实现, 它里面的GC按照回收区域又分为两大中类型:一种时部分收集(Partial GC), 一种时整堆收集(Full GC)。
  • 部分收集: 不是完整收集整个java堆的垃圾收集。其中又分为
    • 新生代收集(MinorGc/Young GC):只是新生代(eden/s0/s1)的垃圾回收
    • 老年代收集(MajorGc/Old GC):只是老年代收集
      • 目前, 只有CMS (垃圾回收器)GC会有单独收集老年代的行为, 其余很少单独收集老年区
      • 很多时候Major GC会和FullGc混淆使用, 具体分辨时老年代回收还是整堆回收
    • 混合收集(MIXed GC):收集整个新生代以及部分老年代的垃圾回收
      • 目前只有G1(垃圾收集器) GC会有这种行为
  • 整堆收集(FullGC)收集整个java堆和方法区的垃圾收集
  • 年轻代GC(MInor GC)触发机制
    • 当年轻代空间不足的时,就会触发Minor GC,这里年轻代满指的时Eden满, Survivo满不会引发GC 。每次MinorGc会清理年轻代的内存
    • 因为java对象大多都是具备朝生夕灭的特性, 所以MinorGC非常频繁, 一般回收速度非常快。
    • Minor GC会引发STW(stop the world), 暂停其他用户的线程, 等垃圾回收结束, 用户线程才恢复运行
    • 在这里插入图片描述
  • 老年代(Major GC/FULL GC)
    • 发生在老年代的GC对象从老年代消失时, 我们说Major Gc 或者 FullGc 发生了
    • 出现了Major GC, 经常会伴随着至少一次的MinorGc(非绝对的, 在Parallel Scavenge收集器的收集策略里面有直接进行MajorGC的策略选择过程)
      • 也就是在老年代空间不足时会先进行MinorGC。如果空间还是不足就触发MajorGc
    • Major GC会比 MinorGc 慢10倍以上, STW时间更长
    • 如果Major GC后内存还是不够就报OOM 之前一定经历FullGC
  • fullGC 触发机制
    • 触发情况
      • 调用System.gc() 但是不必然执行
      • 老年代空间不足
      • 方法区空间不足
      • 通过MinorGC后进入老年代的平均大小大于老年代的可用内存
      • 又Eden区、survivor、s0 区向s1区复制时, 对象大小大于ToSpace可用内存, 则把该对象转到老年代的可用内存小于该对象的大小

堆空间分代思想

  • 在这里插入图片描述
    在这里插入图片描述

内存分配策略(总结)

  • 如果对象在Eden出生并经过一次MinorGc后仍然存活, 并且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将对象年龄设置为1.对象在Survivor区中每熬过一个MinorGC, 年龄就增加一岁, 当他的年龄增加到一定程度(默认15)晋升到老年代
    在这里插入图片描述
  • 对象分配过程:Tlab
    • 为什么有TLAB?(由于是共享资源)
      • 堆区是线程共享区域, 任何线程都可访问到堆区的共享数据
      • 由于对象实例的创建再JVM中非常频繁, 因此再并发环境下从堆区(共享空间)中划分内存空间是线程不安全的,
      • 为避免多个线程操作同一地址, 需要使用加锁等机制, 进而影响分配速度。
    • 什么是TLAB?
      • 从内存模型而不是垃圾回收的角度, 从Eden区中进行划分TLAB, JVM为每一个线程都分配了一个私有的缓冲区域, 包含在Eden空间内。每个线程优先分配在自己的TLAB如果用完了再用共享空间
      • 多线程同时分配内存时, 使用TLAB可以避免线程安全问题, 同时还能提升内存分配的吞吐量, 而这种分配策略叫快速分配策略
      • 尽管不是所有对象都可以在TLAB中分配成功(由于它的内存很小仅占整个Eden空间的1%), 但是jvm确实是将TALB作为内存分配的首选
      • 一旦TLAB分配失败, 就会加锁的在堆的共享空间分配
        在这里插入图片描述

堆空间的参数设置

在这里插入图片描述

  • 空间分配担保参数
    • 再发生MinorGC之前, 虚拟机会检测老年代最大可用的连续空间是否大于新生代所有对象的总空间
    • 如果大于就进行minorGc
    • 小于查看HandlePromotionFaulure 参数的设置
      • 如果为true, 继续检查老年代可用连续空间是否大于历次晋升到老年代的对象的平均大小(jdk7及以后都是true, 修改这个参数也没有意义默认还是true)
        • 如果大于尝试MinorGc, 但是这次MinorGc是有风险的,
        • 如果小于, 则改为fullGC
      • false直接FullGc;

堆是分配对象存储的唯一选择

  • 不一定
  • 由于JIT(即时编译器)与逃逸分析技术成熟, 栈上分配、标量替换优化技术将会导致一些微妙的变化, 所有对象都分配到堆上也渐渐变得不绝对了。
  • 如果经过逃逸分析后发现, 一个对象并没有逃逸出方法的, 就可能被优化成栈上分配。

逃逸分析

  • 是一种可以有效的减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
  • 通过逃逸分析, javaHotspot编译器能够分析出一个新的对象的引用的适用范围从而决定是否要将这个对象分配到堆上
  • 逃逸分析的基本行为就是分析对象动态作用域
    • 当一个对象再方法中被定义后, 对象只在方法内部使用, 则认为没有发生逃逸
    • 一个对象在方法中被定义后, 他被外部方法所引用, 则认为发生逃逸, 例如作为调用参数传递到其他地方中。
  • 如何快速的判断是否发生了逃逸, 看new的对象是否可以被其他方法调用在这里插入图片描述
  • 从7开始就已经默认开启了
  • 结论: 开发中能使用局部变量, 就不要在方法外定义。
逃逸分析: 代码优化
  • 栈上分配: 将堆分配转化为栈分配, 如果一个对象在子程序中被分配, 要使指向该对象的指针永远不会逃逸, 对象可能使栈分配的候选,而不是堆分配。
    • 即使编译器(JIT)在编译期间根据逃逸分析的结果发现, 一个对象没有逃逸的话, 就可能优化成栈上分配, 继续在调用栈内执行, 最后线程结束, 栈空间回收, 同时局部变量也回收, 没有GC
    • 开启栈上分配速度快还有可能不发生GC
      在这里插入图片描述
  • 同步省略
    • 线程同步的代价使相当高的, 同步的后果使降低并发现和性能
    • 在这里插入图片描述
  • 分离对象或标量替换
    • 有的对象可能不需要作为一个连续的内存结构存在也可以被访问到, 呢么对象的部分可以不存储在内存, 而是存在cpu寄存器中
    • EliminateAllocations是标量替换在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值