JVM Weekly-2

JVM可视化大创周报-2

Posted by Chase Gu on July 10, 2020

第三章:垃圾收集器与内存分配策略

3.3 垃圾回收算法

两大类:引用计数式垃圾收集、追踪式垃圾收集

分代收集理论

  • 弱分代假说:绝大多数对象都是朝生夕灭的

  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡

  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数

    由上述两种假说推出。举个例子:如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这种跨代引用也随即被消除了。

    故在新生代上建立全局的数据结构——记忆集(Remembered Set),这个结构吧老年代划分成若干小块,表示出老年代的哪一块内存会存在跨代引用。此后发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

部分专业术语
  • 部分收集(Partial GC)
    • 新生代收集(Minor GC/Young GC)
    • 老年代收集(Major GC/Old GC):目前只有CMS收集器会有单独收集老年代的行为
    • 混合收集(Mixed GC):目前只有G1
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

标记-清除算法

  • 效率不稳定
  • 内存空间碎片化

标记-复制算法

新生代中的对象有98%熬不过第一轮收集。因此不需要按照1:1的比例来划分新生代的内存空间

  • 将新生代划分为Eden和两块较小的Survivor,每次分配只使用Eden和其中一块Survivor,存活的对象复制到另一块Survivor上
  • HotSpot默认Eden和Survivor的大小比例是8:1
  • 分配担保:当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(大多数是老年代)进行分配担保

标记-整理算法

对象移动操作必须全程暂停用户应用程序才能进行——Stop The World

Q:标记复制需要暂停吗?

  • 关注吞吐量的Parallel Scavenge收集器基于标记-整理

  • 关注延迟的CMS收集器基于标记-清除

    暂时容忍空间碎片,碎片化程度影响到对象分配时,再用标记-整理算法收集一次

3.4 HotSpot的算法细节实现

根节点枚举

  • 可达性分析算法耗时
    • 查找引用链过程:耗时最长,已经可以并发
    • 根节点枚举:一定要Stop The World

如何理解?

准确式垃圾收集:虚拟机有办法得知哪些地方存折对象引用:使用OopMap的数据结构实现

一旦类加载动作完成的时候,HotSpot就会把对象内什么便宜量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置时引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找

Q:P82代码没有看懂

安全点

  • 抢先式中断
  • 主动式中断√
    • 轮询标志,若中断标志为真,则自己在最近的安全电商主动中断挂起
    • 汇编test %eax和一个地址。暂停时,设置该地址不可读,则会产生一个自陷异常信号,然后在预先注册的异常处理器中挂起

安全区域

指能够确保在某一段代码片段中,引用关系不会发生变化

可以把安全区域看作被拓展拉伸了的安全点

记忆集与卡表

记忆集有三种记录精度

  • 字长精度
  • 对象精度
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针

卡表是卡精度的实现方式

按照AD的理解,只有新生代才有卡表。

// 在下面的写屏障中我们可以知道,在赋值之后,能够修改该对象对应的卡表。

写屏障

赋值前:写前屏障;赋值后:写后屏障

目前只有G1用到了写前屏障,其他的只用到了写后屏障

伪共享

现代中央处理器的缓存系统是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

解决方案:先判断卡表是否已被修改,如果没有才修改

使用-XX:+UseCondCardMark开启卡表更新判断

并发的可达性分析

(黑色灰色白色代表含义见P87)

  • 产生“对象消失”的条件

    • 赋值器插入了一条或多条从黑色对想到白色对象的新引用
    • 赋值器删除了全部从灰色对象到白色对象的直接或间接引用
  • 增量更新:破坏第一个条件

    原始快照:破坏第二个条件

  • CMS基于增量更新,G1和Shenandoah使用原始快照

经典垃圾收集器

JDK1.7和JDK1.8默认使用Parallel Scavenge和Parallel Old

JDK1.9默认使用G1

  • Serial

    • 标记-复制

    • 单线程,Stop The World

  • ParNew收集器

    • 标记-复制
  • Parallel Scavenge

    • 标记-复制

    • 关注吞吐量

  • Serial Old

  • Parallel Old

  • CMS

  • G1

实战:内存分配与回收策略

  • 对象超过一定阈值可能会直接分配在老年代

  • 分配担保:回收之后的对象S放不下,全部放到老年代,然后把新的对象放到Eden

  • 年龄:第一次移动到S,设置年龄为一岁,15岁到达老年代

  • 动态对象年龄判定

    如果Survivor空间中相同年龄所有对象大小的综合大于Survivo空间的一般,年龄大于或等于该对象的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。

  • 空间分配担保

    发生Minor GC之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(/*老年代最大可用连续空间 > 新生代所有对象总空间*/) {
    // 本次Minor GC安全
} else {
    if (/*-XX:HandlePromotionFailure参数允许担保失败*/) {
        if (/*老年代最大可用的连续空间 > 历次晋升到老年代对象的平均大小*/) {
            // 尝试一次Minor GC
        } else {
            // Full GC
        }
    } else {
        // Full GC
    }
}

其他

OpenJDK

ParallelScavengeHeap.java中有

1
2
3
4
5
private static synchronized void initialize(TypeDataBase db) {
    Type type = db.lookupType("ParallelScavengeHeap");
    youngGenField = type.getAddressField("_young_gen");
    oldGenField    = type.getAddressField("_old_gen");
}

分配地址、内存应该是要用到这个Field的

进入getAddressField(),是一个接口,其实现类为BasicType

1
2
3
4
5
6
7
8
  public AddressField getAddressField(String fieldName) {
    // This type can not be inferred (for now), so provide a wrapper
    Field field = getField(fieldName);
    if (field == null) {
      return null;
    }
    return new BasicAddressFieldWrapper(field);
  }

进入getField(String),最终定位到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  public Field getField(String fieldName, boolean searchSuperclassFields,
                        boolean throwExceptionIfNotFound) {
    Field field = null;
    if (nameToFieldMap != null) {
      field = (Field) nameToFieldMap.get(fieldName);

      if (field != null) {
        return field;
      }
    }

    if (searchSuperclassFields) {
      if (superclass != null) {
        field = superclass.getField(fieldName, searchSuperclassFields, false);
      }
    }

    if (field == null && throwExceptionIfNotFound) {
      throw new RuntimeException("field \"" + fieldName + "\" not found in type " + name);
    }

    return field;
  }

其中有一步field = (Field) nameToFieldMap.get(fieldName);nameToFieldMap是一个HashMap,使用addField()对该Map进行添加

1
2
3
4
5
6
7
8
9
10
11
  /** This method should only be used by the builder of the
      TypeDataBase. Throws a RuntimeException if a field with this
      name was already present in this class. */
  public void addField(Field field) {
    if (nameToFieldMap.get(field.getName()) != null) {
      throw new RuntimeException("field of name \"" + field.getName() + "\" already present in type " + this);
    }

    nameToFieldMap.put(field.getName(), field);
    fieldList.add(field);
  }

这个方法被HotSpotTypeDataBase.java的createField方法调用,而createField方法又在CommandProcessor.java中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
new Command("field", "field [ type [ name fieldtype isStatic offset address ] ]", true) {
            public void doit(Tokens t) {
                if (t.countTokens() != 1 && t.countTokens() != 0 && t.countTokens() != 6) {
                    usage();
                    return;
                }
                if (t.countTokens() == 1) {
                    Type type = agent.getTypeDataBase().lookupType(t.nextToken());
                    dumpFields(type);
                } else if (t.countTokens() == 0) {
                    Iterator i = agent.getTypeDataBase().getTypes();
                    while (i.hasNext()) {
                        dumpFields((Type)i.next());
                    }
                } else {
                    // 略...

                    // Create field by type
                    HotSpotTypeDataBase db = (HotSpotTypeDataBase)agent.getTypeDataBase();
                    db.createField(containingType,
                                   fieldName, fieldType,
                                   isStatic,
                                   offset,
                                   staticAddress);
                }
            }
        },

这是一个Command类的对象,放在一个数组中,然后应该是根据匹配情况来执行

不知道field [ type [ name fieldtype isStatic offset address ] ]代表什么,说不定可以手动设置地址

接下来干什么

  • 查找有没有源码解读的书
  • Q:为什么OpenJDK中既有Java又有C++?二者是互补、依赖还是相互独立?
  • Debugger实现原理