随笔---20200516~20200517---堆

14 篇文章 0 订阅

1. 堆的核心概述

  • 1个进程对应1个JVM实例
    1个进程有多个线程、多个线程共享方法区和堆
    1个线程拥有专属的程序计数器、本地方法栈、虚拟机栈

  • 1个JVM实例只有一个堆内存,堆也是Java内存管理的核心区域。

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

    • 堆内存的大小是可以调节的
  • 堆可以物理上不连续,但逻辑上连续

  • 可以在堆里划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)

  • 《java虚拟机规范》对java堆的描述是:所有的对象实例以及数组应当在运行时分配在堆上。

    • “几乎”所有的对象实例都在这里分配内存
  • 数组和对象 可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置

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

  • 堆,是GC执行垃圾回收的重点区域。

内存细分

  • java7之前 内存逻辑上分为3部分:新生区(新生代/老年代)+养老区(老年区/老年代)+永久区(代)
    • Young Generation Space: Eden,Survivor
    • Tenure generation space
    • Permanent Space
  • java8之后 内存逻辑上分为3部分:新生区+养老区+元空间
    • Young Generation Space: Eden,Survivor
    • Tenure generation space
    • Meta Space
  • -Xms 只包括 新生代老年代
set configuration
-XX:+PrintGCDetails
运行(jdk8)
Heap
 PSYoungGen      total 38400K, used 4037K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)
  eden space 33280K, 12% used [0x00000000d5f00000,0x00000000d62f1780,0x00000000d7f80000)
  from space 5120K, 0% used [0x00000000d8480000,0x00000000d8480000,0x00000000d8980000)
  to   space 5120K, 0% used [0x00000000d7f80000,0x00000000d7f80000,0x00000000d8480000)
 ParOldGen       total 87552K, used 0K [0x0000000081c00000, 0x0000000087180000, 0x00000000d5f00000)
  object space 87552K, 0% used [0x0000000081c00000,0x0000000081c00000,0x0000000087180000)
 Metaspace       used 3493K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 388K, committed 512K, reserved 1048576K

jdk8 是 Metaspace,jdk7只有 PSPermGen

set configuration
-XX:+PrintGCDetails
运行(jdk7)
Heap
 PSYoungGen      total 38400K, used 4037K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)
  eden space 33280K, 12% used [0x00000000d5f00000,0x00000000d62f1780,0x00000000d7f80000)
  from space 5120K, 0% used [0x00000000d8480000,0x00000000d8480000,0x00000000d8980000)
  to   space 5120K, 0% used [0x00000000d7f80000,0x00000000d7f80000,0x00000000d8480000)
 ParOldGen       total 87552K, used 0K [0x0000000081c00000, 0x0000000087180000, 0x00000000d5f00000)
  object space 87552K, 0% used [0x0000000081c00000,0x0000000081c00000,0x0000000087180000)
 PSPermGen       total 167552K, used 2936K [0x0000000081c00000, 0x0000000087180000, 0x00000000d5f00000)
  object space 21504K, 13% used [0x0000000081c00000,0x0000000081c00000,0x0000000087180000)

2. 设置堆内存大小与OOM

  • "-xms"用于表示堆区的起始内存,等价-XX:InitialHeapSize
  • "-xmx"用于表示堆区的最大内存,等价-XX:MaxHeapSize
  • 一旦堆区的内存超过"-xmx"所指定的最大内存时,将会抛出OOMError异常
  • 通常-Xms和-Xmx配置相同的值,目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分割计算堆区的大小,从而提高性能。
  • 默认情况下,初始内存大小:物理电脑内存大小/64;最大内存大小:物理电脑内存大小/4

查看内存大小:

    public static void main(String[] args) {
        long initialMemory = Runtime.getRuntime().totalMemory()/1024/1024;
        long maxMemory = Runtime.getRuntime().maxMemory()/1024/1024;
        System.out.println("-Xms" + initialMemory + "M");
        System.out.println("-Xmx" + maxMemory + "M");
    }

结果
-Xms128M (约等于8G/64)
-Xmx2020M (约等于 8G/4)
这个结果只计算了年轻代的两个Survivor区的其中一个,因为运行时只能往其中一个存数据。
命令行输入

jps
jstat -gc pid
  • OC(count)、OU(used) 老年代
  • S0C、S1C、S0U、S1U、EC、EU 年轻代
  • MC MU 元数据
  • CCSC CCSU ……

3. 年轻代与老年代

  • 存储在JVM中的java对象可以划分为两类
    • 一类是生命周期较短的瞬时对象,创建和消亡都十分迅速
    • 另一类生命周期非常长,极端情况下与JVM生命周期保持一致
  • 堆区分为年轻代、老年代,年轻代分为Eden区、Survivor0区、Survivor1区(或From区、to区)。
  • 配置新生代与老年代在堆结构的占比
    • 默认-XX:NewRatio = 2,表示新生代占1,老年代2,新生代占整个堆的1/3
    • 可以修改-XX:NewRatio=4,表示新生代占1,老年代4,新生代占整个堆的1/5
    • jinfo -flag NewRatio pid #查看-XX:NewRatio
  • 在HotSpot中,Eden区和另外两个Survivor区缺省所占的比例是8:1:1,对应-XX:SurvivorRatio=8(默认的8,可能实际情况不是8
    • jinfo -flag SurvivorRatio pid # 查看-XX:SurvivorRatio
    • +XX:-UseAdaptiveSizePolicy不使用自适应属性 ;+XX:+UseAdaptiveSizePolicy使用自适应属性
  • 几乎所有的java对象都是在Eden被new出来的。(eden放不下的话在老年代)
  • 绝大部分java对象的销毁都在新生代进行了(约80%)
  • -Xmn设置新生代最大内存大小,一般用默认值就可以了。(与newRatio矛盾时,以xmn为准)

4. 图解对象分配过程

  1. new的对象先放eden区。
  2. 当eden区满,程序又需要创建对象,JVM的垃圾回收器将对eden区进行垃圾回收(Minor GC/YGC),将eden区中的不再被其它对象所引用的对象进行销毁。再加载新的对象放到eden区
  3. 然后将eden区中的剩余对象移动到S0 区
  4. 如果再次触发垃圾回收,上次幸存下来的放到S0区,如果没有回收,就会放到S1区。
  5. 如果再次经历垃圾回收,此时会重新放回S0区,接着再去S1区
  6. 啥时候去养老区?可以设置次数,默认是15次。
    -XX:MaxTenuringThreshold=<N>
    survivor区满的时候不会触发Minor GC,直接晋升老年代
  • 不复制的时候,s0和s1区谁空谁是To区;复制的时候,数据从From区复制到To区
  • 垃圾回收:频繁在新生区回收,很少在养老区回收,几乎不在元空间收集。

垃圾回收流程

5. Minor GC,Major GC,Full GC

  • 部分收集
    • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代
      • 目前,只有CMS GC 会有单独收集老年代的行为
      • 注意,很多时候Major GC和Full GC混淆使用,需要具体分辨。
    • 混合收集(Mixed GC):收集整个新生代和部分老年代
  • 整堆收集(Full GC):收集整个java堆和方法区

Minor GC触发机制:

  • 当年轻代空间不足时,就触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存)
  • 因为java对象大多“朝升西灭”,所以Minor GC非常频繁,一般回收速度很快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,暂停其他用户发线程,等垃圾回收结束,用户线程才恢复运行。

老年GC(Major GC/Full GC)触发机制:

  • 指发生在老年代的GC,对象从老年代消失时,“Major GC”或“Full GC”发生了。
  • 出现了Major GC,经常会伴随至少一次的Minor GC(但不绝对,在Parellel Scavenge收集器的收集策略中有直接进行Major GC的策略选择过程)
    • 在老年代空间不足时,会优先触发Minor GC。如果空间还不足,触发Major GC
  • Major GC一般比Minor GC的速度慢10倍以上,STW的时间更长。
  • 如果Major GC后,内存还不足,就报OOM

Full GC触发机制:

  1. 调用System.gc()时,系统建议执行Full GC,但是不必然执行
  2. 老年代空间不足
  3. 方法去空间不足
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  5. 由Eden区、From区向To区复制时,对象的大小大于To区,则把该对象转存到老年代,但是老年代可用内存大小小于该对象大小
    *full gc是开发或调优中尽量避免的。

6. 堆空间分代思想

为什么要把java堆分代?
经研究,不同对象的生命周期不同,70%~99%的对象是临时对象。
不分代也可以,分代的唯一理由是优化GC性能。如果没有分代,那所有的对象都在一块。GC的时候要找到哪些对象没用,就要对整个堆的区域进行扫描。而将70%~99%的生命周期非常短的对象放到某一块地方,GC的时候先把这块区域进行回收,这样就会腾出很大的空间。

7. 内存分配策略(或对象提升(promotion)规则)

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

8. 为对象分配内存:TLAB

为什么有TLAB?

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据(有利于线程间通信)
  • 由于对象实例的创建在jvm中非常频繁,因此在并发环境下从堆区划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要加锁等机制,进而影响分配速度。
    什么是TLAB
  • 从内存模型而不是垃圾回收的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓冲区域,它包含在Eden中。
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此可以将这种内存分配方式称为快速分配策略
  • 据我所知所有OpenJDK衍生出来的jvm都使用了TLAB的设计

TLAB的再说明:

  • 尽管不是所有的对象实例都能在TLAB中成功分配内存,但JVM确实是把TLAB作为内存分配的首选。
  • 在程序中,可以通过-XX:UseTLAB设置是否开启TLAB空间
  • 默认情况下,TLAB空间的内存非常小,仅占有Eden的1%,可以通过 -XX:TLABWasteTargetPercent来调节占比。
  • 一旦对象在TLAB中分配内存失败,JVM会尝试使用加锁机制来确保数据操作的原子性。
    对象分配过程TLAB

9. 小结堆空间的参数设置

jdk8参数的官网链接
参数—jdk版本选择官网链接
ps:ctrl+F搜索

常用参数:

-XX:+PrintFlagsInitial #查看所有参数的默认初始值
-XX:+PrintFlagsFinal #查看所有参数的最终值
     命令行查看某个参数:jps:查看当前运行的线程
     								:jinfo -flag SurvivorRatio pid
-Xms: 初始堆空间内存(默认为物理内存的1/64)
-Xmx: 最大堆空间内存(默认为物理内存的1/4)
-Xmn: 设置新生代的大小
-XX:newRatio:设置新生代与老年代的占比
-XX:SurvivorRatio:设置新生代中Eden与S0/S1的占比
-XX:MaxTenuringThreshold=<N>设置新生代垃圾的最大年龄
-XX:PrintGCDetails:打印详细的GC日志
打印简要的GC信息 -XX:+PrintGC    -verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保

空间分配担保
其实“历次晋升”就是过去从新生代到老年代,即通过过去的平均情况来预判,有风险。

10. 堆是分配对象的唯一选择吗?

栈上分配、标量替换优化技术
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配

TaoBaoVM,创新的GCIH(GC invisible heap)实现off-heap,将生命周期较长的java对象从heap中移到heap外,并且GC不能管理GCIH内部的java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

逃逸分析:看new的对象实体是否可能在外面被调用

  • 方法返回对象,发生逃逸
  • 为成员变量赋值,发生逃逸
  • 如果当前的obj引用声明为static,仍然发生逃逸
  • 对象的作用域仅在当前方法中有效,没有发生逃逸
  • 引用成员变量的值,发生逃逸

在jdk7,HotSpot默认开启了逃逸分析
-XX:+DoEscapeAnalysis显式开启逃逸分析
-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果

tip:
能用局部变量的,就不要在方法外定义。

逃逸分析–代码优化–笔记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值