JVM虚拟机运行时数据区-堆空间

堆的核心概述

  • 一个JVM实例只有一个堆内存,堆也是Java内存管理的核心区域
  • Java堆着Jvm 启动的时候就会被创建,其空间大小就确定了。是JVM管理的最大的一块内存空间
    • 堆内存的大小是可以调节的 -Xms10m -Xmx20m 最启动初始化内存10M。 启动时最大内存20M

堆的核心概述: 内存细分

现代垃圾收集器大部分都基于分代收集理论设计, 堆空间细分为:

  • Java 7 及以前堆内存逻辑上分为三部分: 新生区 + 养老区 + 永久代
    • Young Generation Space 新生区 Young/New
      • 又被花费为 Eden 区 和 Survivor 区
    • Tenure generation space 养老区 Old/Tenure
    • Permanent Space 永久区 Perm
  • Java 8 及之后堆内存逻辑上分为: 新生区 + 养老区 + 元空间
    • Young Generation Space 新生区 Young/New
      • 又被花费为 Eden 区 和 Survivor 区
    • Tenure generation space 养老区 Old/Tenure
    • Meta Space 元空间 Meta
      约定: 新生区 = 新生代 = 年轻代 养老区 = 老年区 = 老年代 永久区 = 永久代 = 元空间

堆空间大小的设置

  • Java 堆区用于存储 java 对象实例,那么堆的大小在JVM启动时就已经设定好了。 大家可以通过选项 —Xmx 和 -Xms 来设置
    • Xms用于表示堆区的起始内存。等价于 -XX:InitialHeapSize
    • Xmx用于表示堆区的最大内存。等价于 -XX:MaxHeapSize
  • 一旦堆区中的内存超过 -Xmx 所指定的最大内存时,将会抛出 OutOfMemoryError 异常
  • 通常会将 -Xms 和 -Xmx 配置相同的值,其目的是为了能够在Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小。从而提供性能
  • 默认情况下, 初始内存大小: 物理电脑内存大小 /64
  • 最大内存大小: 物理电脑内存大小的 /4
// 返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 /1024;
// 返回java 虚拟机试图使用的最大堆内存容量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 /1024;
System.out.println("-Xms: " + initialMemory + "m");
System.out.println("-Xmx: " + maxMemory + "m");

如何查看你运行的Java代码所占用的进程号 ?
通过 java 的 jps命令行

C:\Windows\System32>jps
18720 Main
19728 Jps
15140 RemoteMavenServer36
21252 Launcher
20188

找到对应的程序名称 前面的就是你的进程号 , 然后通过 jstat 命令 就可以查看你当前的程序的内存空间

jstat -gc 18720

C:\Windows\System32>jstat -gc 18720
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
24064.0 25600.0 608.0   0.0   20992.0   3225.8   101888.0   43243.7   51328.0 48272.7 7552.0 6736.6    120    0.353   6      0.285    0.638

通过这个命令行可以查看你的具体占用了多少内存。 不过 可用内存只有 新生代中的 eden 区 和 幸存者区中一个 和 老年代 的总量 , 因为 幸存者 区,因为 复制算法 所以会导致其中的一个区是空的

年轻代和老年代

  • 存储在JVM的java对象可以划分为两类:
    • 一类是生命周期比较短的瞬时对象,这类对象的创建和消亡非常迅速
    • 另外一类的生命周期比较长,在某些极端的情况下还能够与JVM的生命周期保持一致
  • Java 堆内进一步细分的话,可以划分为 年轻代(YoungGen) 和老年代 (OldGen)
  • 其中年轻代又可以划分为 Eden 空间, Survivor0 空间和 Survivor1 空间(有时候后也叫作 from 区 和 to 区)
  • 在HotSpot中,Eden空间和另外两个Survivor空间缺省值所占的比例为 8:1:1
  • 当然开发人员可以通过选项“-XX:SurvivorRatio” 调整这个空间比例,比如:-XX:SurvivorRatio=8
  • 几乎所有的Java对象都是在Eden区被new出来的,除非你的这个对象刚开始的时候大到eden区装不下了
  • 绝大部分的Java对象销毁都在新生代进行了
    • IBM公司的专门研究表明,新生代的 80% 以上的对象都是 朝生夕死的
  • 可以使用选项-Xmn 设置新生代最大内存大小
    • 这个参数一般使用默认值就可以了
      在这里插入图片描述

下面这参数开发中一般不会调整:

在这里插入图片描述

  • 配置新生代与老年代在堆结构中的占比
    • 默认-XX:NewRatio = 2, 表示新生代占1, 老年代站2, 新生代占整个堆的 1/3
    • 可以修改为-XX:NewRatio=4, 表示新生代占1, 老年代占4, 新生代占整个堆的 1/5
      虽然 Eden 和 S0 S1 比例为 8:1:1, 但是 会有一个自适应的关系。所以并不是特别准确。
      **可以通过 -XX:SurvivorRatio=8 来指定新生代为8 **

写一个main 方法,让其阻塞。 通过 VisualVM 来查看 新生代比例
启动参数为 -Xms600m -Xmx600m -XX:+PrintGCDetails。 设置初始化堆大小和 最大堆大小为 600M
查看Visual GC
在这里插入图片描述
既然如此,我们再通过 jstat 来查看 新生代所占用的内存空间
在这里插入图片描述
其实这个里面还存在一个自适应的机制。
可以通过 XX:-UseAdaptiveSizePolicy来关闭 自适应的内存分配策略。 通过 - 这个符号代表不适用这个属性。 + 则是代表使用这个属性. 但是不知道为什么不好使,我们还是通过上面提到的那个 -XX:SurvivorRatio=8 来执行
在这里插入图片描述
上面的图是Java程序的一个启动参数 。 现在来查看被加过 -XX:SurvivorRatio=8 参数的 新生代吧
在这里插入图片描述

总结一下参数:

  • -Xms600m -Xmx600m 设置初始化堆内存大小
  • -XX:NewRatio:设置新生代与老年代的比例,默认为2`
  • -XX:SurvivorRatio:设置新生代中eden区和Survivor区的比例 默认值为8
  • -XX:-UseAdaptiveSizePolicy:关闭自适应的内存分配策略, 暂时用不到
  • -Xmn: 设置新生代的空间大小。
  • Jdk命令行工具
    • jps:用来查看当前JVM进行的类似于进程号的东西
    • jstat -gc 进程号:用来查看该进程的JVM的内存分配情况。
    • jinfo -flag 参数值 进程号 可以用来查看当时运行JAVA程序时的参数值时多少
  • 还有一个VisualVM 工具,可以用来查看Java 程序运行时的状态,启动时的参数设置

堆空间对象分配过程:概述

为新对象分配内存是一项非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否在内存空间中产生内存碎片

  • new 的对象先放在 eden 区, 此区有大小限制
  • 当伊甸园的空间填满的时候,程序又需要创建对象,JVM的垃圾回收器将堆伊甸园区进行垃圾回收(Minor GC), 将伊甸园区中的不在被其他对象所引用的对象进行销毁。 在加载新的对象放到伊甸园区
  • 然后将伊甸园区的幸存对象移动到 幸存者0区
  • 如果再次触发垃圾回收,此次上次幸存下来的放入到 S0 区的,如果没有被回收掉,就会放入到 S1区
  • 如果再次经历垃圾回收,此时会重新放回到 S0 区,接着再次去S1区,如此反复
  • 什么时候可以选择区养老区呢?默认是15次之后
    • 可以设置参数 -XX:MaxTenuringThreshold=<N>进行设置
  • 在养老区,相对来说比较悠闲。当养老区内存不足时,再次触发 GC: Major GC , 进行养老区的内存清理
  • 如果养老区执行了 MaJor GC 之后发现依然无法进行对象的保存,就会产生OOM 异常
    • Java.lang.OutOfMemoryError: Java heap space

注意点:
Survivor 区不会触发 YGC/Minor GC
当eden触发GC的时候,会将 Survivor区 的顺手给回收了
如果 Survivor 区的满了的话,这种属于特殊规则。他就可以直接晋升到 养老区了
如果对象过大。就会直接放入到养老区
针对性存在 S0, S1 的总结, 复制之后有交换,谁空谁是 to
关于垃圾回收,频繁在新生区收集,很少在养老区手机,几乎不再永久区 / 元空间手机

在这里插入图片描述
常用的 调优工具:

  • JDK 命令行
  • Eclipse: Memory Analyzer Tool
  • Jconsole
  • VisualVM
  • Jprofiler
  • Java Flight Recorder
  • GCViewer
  • GC Easy

堆空间的垃圾回收:Minor GC,Major GC 和 Full GC

JVM 在GC 时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代
针对HotSpot VM 的实现, 它里面的GC 按照回收区域又分为两大种类型: 一种是部分收集(Partial GC), 一种是整堆收集(Full GC)

  • 部分收集: 不是完整收集整个Java 堆的垃圾手机,其中又分为:
    • 新生代手机(Minor GC / Young GC)只是 新生代(Eden,S0,S1)的垃圾收集
    • 老年代回收(Major GC / Old GC) 只是针对老年代的垃圾收集
      • 目前,只有CMS GC 会有单独收集老年代的行为
      • 注意,很多时候Major GC 会和Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收
    • 混合收集(Mixed GC): 收集整个新生代以及部分老年代的垃圾收集
      • 目前,只有G1 GC 才会有这种行为
  • 整堆收集(Full GC) : 收集整个JAVA堆和方法区的垃圾收集

最简单的分代式策略的触发条件

  • 年轻代GC(Minor GC)触发机制:
    • 当年轻代空间不足时,就会触发Minor GC, 这里的年轻代满指的是Eden区满, S0和S1满了不会引发GC.(每次 Minor GC 会清理年轻代的内存)
    • 因为Java对象大多都是朝生夕死的特性,所以 Minor GC 非常频繁。一般回收的速度也比较快。
    • Minor GC 会引发 STW (Stop The World), 暂停其他用户的线程。 等垃圾回收结束,用户线程才会恢复运行。
  • 老年代 (Major GC / Full GC) 触发机制:
    • 指发生在老年代的GC,对象从老年代消失时,我们说 “Major GC” 或者 “Full GC”,(但并非绝对的,在Parallel Scavenge 收集器的手机策略里就有直接进行Major GC 的策略选择过程)
      • 也就在老年代空间不足时,会先尝试触发Minor GC。 如果之后还空间不足,就会触发 Major GC
    • Major GC 速度一般比Minor GC 慢10倍以上,STW 的时间更长
    • 如果Major GC 之后,内存还不足,就会出现OOM了
    • Major GC 的速度一般会比 Minor GC 慢10倍以上
  • Full GC 触发机制
    • 调用 System.gc()时, 系统建议执行Full GC, 但是不必然执行
    • 老年代空间不足
    • 方法去空间不足
    • 通过Minor GC 进入老年代的平均大小大于老年代的可用内存
    • 由eden 区, S0区(from) 向 S1区(to) 复制时,对象大小大于 to 的可用内存,这把对象转存到老年代,并且老年代的可用内存小于该对象大小
    • 注意:full gc 是开发或者调优中尽量要避免的,这样暂停时间会短一些

堆空间分代思想

为什么需要吧Java堆分带?不分代就不能正常工作了吗?

  • 经研究, 不同对象的生命周期不同。 70% - 99% 的对象都是临时对象
    • 新生代:有Eden, 两块大小相同的Survivor (又称为from/to, s0/s1) 构成。 to 区永远是空的
    • 老年代:存放新生代中经理多次GC仍然存活的对象
  • 其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一个地方,当GC的时候只需要对这块区域的地方进行回收,这样就会腾出很大的空间出来,避免扫描过多的区域。

内存分配策略(对象提升规则)

  • 优先分配到Eden
  • 大对象直接分配到老年代
    • 程序中尽量避免出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果Survivor区的相同年龄的所有对象大小的总和大于Survivor空间的一般, 年龄大于或者等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold 中要求的年龄
  • 空间分配担保
    • -XX:HandlerPromotionFailure

对象分配过程:TLAB

为什么会有TLAB(Thread Local Allocation Buffer) ?

  • 堆是线程共享区域, 任何线程都可以访问到堆区的共享数据
  • 由于对象实例的创建在JVM中非常的频繁,因为在并发环境下从堆区中划分内存空间是线程不安全的
  • 为了避免多个线程操作同一个地址,需要使用加锁等机制,进而影响分配速度。

什么是TLAB?

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
  • OpenJDK 衍生出来的JVM都提供给了TLAB的设计
  • 尽管不是所有的对象实例都能在TLAB中成功分配内存,但是JVM确实是将TLAB 作为内存分配的首选。
  • 在程序中,开发人员可以通过选项"-XX:UseTLAB" 设置是否开启TLAB 空间。
  • 默认情况下,TLAB空间的内存非常小,仅占用整个Eden空间的 1%, 当然我们可以通过选项 -XX:TLABWasteTargetPercent 设置TLAB空间所占用Eden空间的百分比大小
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试通过使用加锁机制确保数据操作的原子性。从在直接在Eden空间中分配内存。
    在这里插入图片描述

-XX:HandlePromotionFailure 是否设置空间分配担保详解

在发生Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。

  • 如果大于,则此次Minor GC 是安全的
  • 如果小于,则虚拟机会查看 -XX:HandlePromotionFaillure 设置值是否允许担保失败
    • 如果HandlePromotionFailure = true, 那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
      • 如果大于,则尝试进行一次Minor GC, 但是这次的Minor GC 依然是有风险的
      • 如果小于,则改为进行一次Full GC
    • 如果HandlePromotionFailure = false 则改为直接进行一次Full GC

在JDK update24 之后,HandlePromotionFailure 参数不会在影响到虚拟机的空间分配担保策略。 Jdk6 update24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行 Full GC

通过逃逸分析看堆空间的对象分配策略

在《深入理解JAVA虚拟机》中关于Java 堆内存中有这样一段描述:
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么的 “绝对” 了。

在Java 虚拟机中,对象是在JAVA堆中内存分配的,这是一个普遍的常识。但是,有一个特殊 情况,就是如果经过逃逸分析(Escape Analysis) 后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样就无需再堆上分配内存,也无需进行垃圾回收了,这也是最常见的堆外存储技术。

此外,前面提到的基于 OpenJdk 深度定制的TaoBao VM 其中创新的 GCIH (GC invisible heap) 技术实现了off-heap, 将生命周期较长的JAVA对象从heap 中移至 heap 外,并且GC 不能管理 GCIH内部的对象,以此降低GC 的回收频率和提升GC的回收效率的目的

逃逸分析概述
如何快速判断是否发生了逃逸分析
查看 在方法内部的 new 出来的对象是否有可能在方法外部被调用。

public void myMethod() {
    V v = new V();
    // TODO
    
    v = null;
}
    /**
     * 因为你的 在 方法内部new出来的sb对象被你返回出去,所以可以认为 sb 对象逃逸出来了这个方法的区域
     * 所以  此对象在出生在堆上
     * @param s1
     * @param s2
     * @return
     */
    public static StringBuffer createStringBufffer (String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;
    }
    
    /**
     * 此方法在 方法内部new出来,但是return的是 sb.toString()。 实则方法返回的是String 对象。所以sb对象并没有逃逸出方法的区域
     * 所以 此对象出生在栈上
     * @param s1
     * @param s2
     * @return
     */
    public static String createString (String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

参数设置:

  • 在JDK 6u23 版本之后,HotSpot 默认开启了逃逸分析。、
  • 如果使用的是较早的版本,开发人员则可以通过:
    • 选项-XX:+DoEscapeAnalysis 显式的开启逃逸分析
    • 通过选项-XX:+PrintEscapeAnalysis 查看逃逸分析的筛选结果、

逃逸分析 : 代码优化

使用逃逸分析,编译器会对代码做以下优化

  • 栈上分配,将堆分配转换为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
  • 同步省略。 如果一个对象被发现了只能从一个线程被访问,
    • 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
    • 在动态编译代码块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的的所对象是否能够被一个线程访问,而没有被发布到其他线程。如果没有那么JIT编译器在编译这个同步块的时候,就会取消这部分代码的同步,这样就能大大提高并发性和性能,这个取消同步的过程就叫做同步省略,也叫作锁消除
      例子
public void f() {
   Object o = new Object();
   synchronized(o) {
       System.out.print(o);
   }
}
// 编译器优化。
public void f() {
   Object o = new Object();
   System.out.print(o);
}

只是举例子,虽然这个代码和sb一样。

  • 标量替换 参数:-XX:+EliminateAllocations: 默认是打开,允许将对象打散分配在栈上
    分离对象或标量替换,有的对象可能不需要作为一个连续的内存结构存在也可以被访问到。那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

这里使用参数如下:

  • 参数-server 启动Server 模式,因为在Server 模式下,才可以使用逃逸分析
  • 参数-XX:+DoEscapeAnalysis : 启用逃逸分析
  • 参数-Xmx10m: 制定了堆空间最大为 10MB
  • 参数-XX:+PrintGC: 将GC打印成日志
  • 参数-XX:+EliminateAllocations: 开启了标量替换(默认打开),允许将对象打散分配在栈上,比如对象拥有 id 和 admin 两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配

逃逸分析缺点:

  • 无法保证逃逸分析的性能消耗一定能高于他的消耗,虽然经过逃逸分析可以做标量替换,栈上分配,和锁消除,但是逃逸分析自身也是需要进行一系列复杂的分析。这其实也是一个相对耗时的过程。
  • 一个极端的例子就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那么这个逃逸分析的过程消耗就白白浪费了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值