`
ldq67123
  • 浏览: 849 次
  • 性别: Icon_minigender_1
  • 来自: 广州
文章分类
社区版块
存档分类
最新评论

MaxineVM GC代码走读笔记

    博客分类:
  • JVM
阅读更多

目录

1.         MaxineVM简介... 2

2.         GC:经典复制算法... 2

3.         MaxineVM对象内存布局... 4

4.         MaxineVM线程栈内存布局... 4

5.         MaxineVM根扫描... 6

6.         对经典复制算法的改进... 9

7.         MaxineVM更新对象间引用... 11

8.         总结... 12

 

 

 

 

1. MaxineVM简介

官网的简介是:A Next Generation, Highly Productive Platform for Virtual Machine Research

MaxineVM,用Java来跑Java的研究型JVM,且性能并不差。

其源码由大部分Java+小部分C构成。

MaxineVMSun JDK 6是兼容的,无需再实现一次JDK

提供了可视化查看工具,观察MaxineVM的堆管理、对象布局、锁同步、JIT等。

MaxineVM的执行过程



 

 

2. GC:经典复制算法



 

如上图所示,复制算法的基本要点是

l  把内存分为两块,一块称为FROM,一块称为TO

l  有新对象请求分配内存时,总是从FROM中依次分配对象,用一个FREE指针来标识空闲内存

l  当内存不足无法分配时,启动GC

l  从根(root,如线程栈、类静态变量、JNI全局引用等)处开始查找可达对象(总是在FROM中),先标记为已复制,再复制到TO;递归遍历该对象字段,把它们也当作可达对象。对象的引用关系相当于单向有环图,遍历算法可用BFSDFS

l  当遍历复制完毕后,还需要更新对象的引用关系,借助一个额外字段forwarding指针来做复制关系记录

GC算法伪代码如下:

def copy_gc()

    $free = $to

    for ($obj in $root)

       $newRef = move($obj)

       set_ptr($obj, $newRef)

   

    $scan = $to

    while ($scan < $free)

       $obj = cast_to_obj($scan)

       for ($child in fields($obj))

           if ($child < $from+size($from) && $child >= $from)

              set_ptr($child, $child.forwarding)

       $scan += sizeof($obj)

   

    swap_ptr($to, $from)

   

def move($obj)

    if ($obj.marked)

       return $obj.forwarding

    $newObj = $free

    $free += sizeof($obj)

    copy($obj, $newObj, sizeof($obj))

    $obj.forwarding = $newObj

    $obj.marked = true

   

    for ($child in fields($obj))

       move($child)

    return $newObj

 

 

3. MaxineVM对象内存布局

在走读MaxineVM代码前,首先看下对象内存布局(MaxineVM有多种布局方式,此处仅讨论一种)



 

每一个对象都有一个1WordHub指针域,指向一个结构体,描述这个对象的内存布局,同时也用作加速虚方法调用分派(dispatch

 

4. MaxineVM线程栈内存布局

根据上面GC算法伪代码,我们可以对比下真正的GC算法实现。因为根遍历从栈上开始,我们首先要了解下栈内存布局。



 

如图所示,MaxineVM的线程栈是和OS的线程栈是同一个,入栈时栈指针(stack pointer)从高地址(栈底)向低地址(栈顶)增长。栈底一般是libpthread的方法帧,这部分内存没有指向Java堆的指针,无需GC。接着就进入了Java的方法帧,根据JVM规范

Each frame has its own array of local variables (§2.6.1), its own operand stack (§2.6.2), and a reference to the run-time constant pool (§2.5.5) of the class of the current method

那么线程栈中在GC中需要当作根的是每一个方法帧中的本地变量(local variables)、操作数栈(operand stack)。本地变量在方法帧是一片因方法而异的定长内存,即每个Java方法编译后就已经知道需要多少的栈内存来存临时变量了;同样的操作数栈也是在编译器就能知道其最大内存占用。所以对同一个方法来说,Java方法帧的栈内存占用是定长的连续内存。执行过程中栈帧中既有基本数据类型(int|long|char|short|byte|boolean)又有引用数据类型,仅引用数据类型才是需要被垃圾回收的,在MaxineVM中是依赖一个位图来标记栈中的引用指针,即上图中的reference map area。关于线程栈的其余部分,与理解GC算法实现关联不大,此处不再展开。

如此可知,MaxineVM是通过扫描reference map中来找出可达对象的:

com.sun.max.vm.heap.sequential.SequentialHeapRootsScanner

publicvoid run() {

        VmThreadMap.ACTIVE.forAllVmThreadLocals(null, _vmThreadLocalsScanner);

VMConfiguration.hostOrTarget().monitorScheme().scanReferences(_pointerIndexVisitor);

}

privatefinal Pointer.Procedure _vmThreadLocalsScanner = new Pointer.Procedure() {

publicvoidrun(Pointer vmThreadLocals) {

            VmThreadLocal.scanReferences(vmThreadLocals, _pointerIndexVisitor);

        }

    };

逻辑很简单(尚未到根据reference map遍历栈这步),仅仅是使用观察者模式分别遍历所有的活动线程和monitor(即Java中的synchronized关键字锁住的对象)。monitor对象也需要当作根是因为一旦执行完synchronized(obj)后(即字节码monitorenter),线程栈上就没有这个对象的指针了,如下代码中的注释部分,栈上就没有monitor对象的指针了:

synchronized (lockObj()) {

    // do something

    // GC

    // do something

}

 

5. MaxineVM根扫描

接下来着重看对线程栈的GC root扫描过程,即上文代码中的com.sun.max.vm.thread.VmThreadLocal.scanReferences

publicstaticvoidscanReferences [m1]  (Pointer vmThreadLocals, PointerIndexVisitor wordPointerIndexVisitor) {

final Pointer lastJavaCallerStackPointer = LAST_JAVA_CALLER_STACK_POINTER [m2]  .getVariableWord(vmThreadLocals).asPointer();

final Pointer lowestActiveSlot = LOWEST_ACTIVE_STACK_SLOT_ADDRESS.getVariableWord(vmThreadLocals).asPointer();

final Pointer highestSlot = HIGHEST_STACK_SLOT_ADDRESS.getConstantWord(vmThreadLocals).asPointer();

final Pointer lowestSlot = LOWEST_STACK_SLOT_ADDRESS.getConstantWord(vmThreadLocals).asPointer();

final VmThread vmThread = UnsafeLoophole.cast(VM_THREAD.getConstantReference(vmThreadLocals));

StackReferenceMapPreparer.scanReferenceMapRange(vmThreadLocals, lowestSlot, vmThreadLocalsEnd(vmThreadLocals), wordPointerIndexVisitor);

        StackReferenceMapPreparer.scanReferenceMapRange(vmThreadLocals, lowestActiveSlot, highestSlot, wordPointerIndexVisitor); [m3]  

    }

publicstaticvoidscanReferenceMapRange [m4]  (Pointer vmThreadLocals, Pointer lowestSlot, Pointer highestSlot, PointerIndexVisitor wordPointerIndexVisitor) {

final Pointer lowestStackSlot = VmThreadLocal.LOWEST_STACK_SLOT_ADDRESS.getConstantWord(vmThreadLocals).asPointer();

final Pointer referenceMap = VmThreadLocal.STACK_REFERENCE_MAP [m5]  .getConstantWord(vmThreadLocals).asPointer();

finalinthighestRefMapByteIndex = referenceMapByteIndex(lowestStackSlot, highestSlot);

finalintlowestRefMapByteIndex = referenceMapByteIndex(lowestStackSlot, lowestSlot);

 

// Handle the lowest reference map byte separately as it may contain bits

// for slot addresses lower than 'lowestSlot'. These bits must be ignored:

finalintlowestBitIndex = referenceMapBitIndex(lowestStackSlot, lowestSlot);

finalinthighestBitIndex = referenceMapBitIndex(lowestStackSlot, highestSlot);

if (highestRefMapByteIndex == lowestRefMapByteIndex [m6]  ) {

scanReferenceMapByte(lowestRefMapByteIndex, lowestStackSlot, referenceMap, lowestBitIndex % Bytes.WIDTH, highestBitIndex % Bytes.WIDTH, vmThreadLocals, wordPointerIndexVisitor);

        } else {

scanReferenceMapByte(lowestRefMapByteIndex, lowestStackSlot, referenceMap, lowestBitIndex % Bytes.WIDTH, Bytes.WIDTH, vmThreadLocals, wordPointerIndexVisitor);

scanReferenceMapByte(highestRefMapByteIndex, lowestStackSlot, referenceMap, 0, (highestBitIndex % Bytes.WIDTH) + 1, vmThreadLocals, wordPointerIndexVisitor);

 

for (intrefMapByteIndex = lowestRefMapByteIndex + 1; refMapByteIndex<highestRefMapByteIndex; refMapByteIndex++) {

scanReferenceMapByte(refMapByteIndex, lowestStackSlot, referenceMap, 0, Bytes.WIDTH, vmThreadLocals, wordPointerIndexVisitor);

            }

        }

    }

 

privatestaticvoidscanReferenceMapByte [m7]  (intrefMapByteIndex, Pointer lowestStackSlot, Pointer referenceMap, intstartBit, intendBit, Pointer vmThreadLocals, PointerIndexVisitor wordPointerIndexVisitor) {

finalintrefMapByte = referenceMap.getByte(refMapByteIndex);

if (refMapByte != 0) {

finalintbaseIndex = refMapByteIndex * Bytes.WIDTH;

final Pointer slot = lowestStackSlot.plus(baseIndex * Word.size());

for (intbitIndex = startBit; bitIndex<endBit; bitIndex++) {

if (((refMapByte>>>bitIndex) & 1) != 0) {

wordPointerIndexVisitor.visitPointerIndex(slot, bitIndex);

                }

            }

        }

    }

 

发现了栈中的一个引用类型指针后,就需要进行真正的数据拷贝了,即观察者回调wordPointerIndexVisitor_pointerIndexVisitor,也相当于伪代码中的move方法

publicvoid visitPointerIndex(Pointer pointer, intwordIndex) {

final Grip oldGrip = pointer.getGrip(wordIndex);

final Grip newGrip = mapGrip(oldGrip);

if (newGrip != oldGrip) {

pointer.setGrip(wordIndex, newGrip) [m8]  ;

        }

    }

private Grip mapGrip(Grip grip) {

final Pointer fromOrigin = grip.toOrigin();

if (_fromSpace.contains(fromOrigin) [m9]  ) {

final Grip forwardGrip = Layout.readForwardGrip [m10]  (fromOrigin);

if (!forwardGrip.isZero() [m11]  ) {

returnforwardGrip;

            }

final Pointer fromCell = Layout.originToCell(fromOrigin);

final Size size = Layout.size(fromOrigin);

final Pointer toCell = gcAllocate(size) [m12]  ;

            Memory.copyBytes(fromCell, toCell, size);

final Pointer toOrigin = Layout.cellToOrigin(toCell);

final Grip toGrip = Grip.fromOrigin(toOrigin);

            Layout.writeForwardGrip [m13]  (fromOrigin, toGrip);

 

returntoGrip;

        }

returngrip;

    }

 

6. 对经典复制算法的改进

注意到mapGrip方法仅仅复制了指针所指的对象,并没有递归复制引用到的所有子对象。这其实是对基于递归复制的经典复制算法的改进,递归对GC线程的栈空间来说是不可控的(如在对象引用变成链表式的极端情况)。但也不能简单地使用额外堆空间构造一个迭代栈来去递归,因为迭代复制的过程对堆空间的消耗也是负担。MaxineVM的策略是把复制GC算法中的TO空间当成了迭代容器,实际上MaxineVM的复制GC算法伪代码如下

def MaxineVM_copy_gc()

    $free = $to

    for ($obj in $root)

       $newRef = move($obj)

       set_ptr($obj, $newRef)

   

    $scan = $to

    while ($scan < $free)

       $obj = cast_to_obj($scan)

       for ($child in fields($obj))

           if ($child < $from+size($from) && $child >= $from)

           $movedRef = move($child)

           set_ptr($child, $movedRef)

       $scan += sizeof($obj)

   

    swap_ptr($to, $from)

   

def move($obj)

    if ($obj.marked)

       return $obj.forwarding

    $newObj = $free

    $free += sizeof($obj)

    copy($obj, $newObj, sizeof($obj))

    $obj.forwarding = $newObj

    $obj.marked = true

    for ($child in fields($obj))

       move($child)

    return $newObj

move方法中,不再递归调用自身。当复制完根对象后,从TO空间开始迭代,迭代过程中,一边复制对象(已经复制的对象直接返回新地址),一边更新引用关系。其迭代过程可看简图



 

虽然相比经典算法已经有了较大的性能提升,但MaxineVM中的实现仍是非常原始的,如改进后的算法是基于BFS复制对象的,对内存访问友好性不如DFS;再如堆管理非常粗糙,总是有50%的内存空间是不能分配内存的。

7. MaxineVM更新对象间引用

与伪代码相对应的,MaxineVM经过根扫描后,更新对象间引用的代码com.sun.max.vm.heap.sequential.semiSpace.SemiSpaceHeapScheme::moveReachableObjects

privatevoidmoveReachableObjects() {

        Pointer cell = _toSpace.start().asPointer();

while (cell.lessThan(_allocationMark)) {

cell = DebugHeap.checkDebugCellTag [m14]  (cell);

cell = visitCell(cell);

        }

    }

public Pointer visitCell(Pointer cell) {

final Pointer origin = Layout.cellToOrigin(cell [m15]  );

final Grip oldHubGrip = Layout.readHubGrip(origin);

final Grip newHubGrip = mapGrip(oldHubGrip) [m16]  ;

if (newHubGrip != oldHubGrip) {

            Layout.writeHubGrip [m17]  (origin, newHubGrip);

        }

final Hub hub = UnsafeLoophole.cast(newHubGrip.toJava()) [m18]  ;

final SpecificLayout specificLayout = hub.specificLayout() [m19]  ;

if (specificLayout.isTupleLayout [m20]  ()) {

            TupleReferenceMap.visitOriginOffsets(hub, origin, _pointerOffsetGripUpdater);

if (hub.isSpecialReference()) {

                SpecialReferenceManager.discoverSpecialReference [m21]  (Grip.fromOrigin(origin));

            }

returncell.plus(hub.tupleSize());

        }

if (specificLayout.isHybridLayout [m22]  ()) {

            TupleReferenceMap.visitOriginOffsets(hub, origin, _pointerOffsetGripUpdater);

        } elseif (specificLayout.isReferenceArrayLayout()) {

            scanReferenceArray(origin);

        }

returncell.plus(Layout.size(origin));

}

privatefinal PointerOffsetVisitor _pointerOffsetGripUpdater = newPointerOffsetVisitor() {

publicvoidvisitPointerOffset(Pointer pointer, intoffset) {

final Grip oldGrip = pointer.readGrip(offset);

final Grip newGrip = mapGrip(oldGrip);

if (newGrip != oldGrip) {

pointer.writeGrip(offset, newGrip);

            }

        }

    };

 

8. 总结

本文只是选读了MaxineVMGC部分,实用的GC算法实际上也没有那么简单,MaxineVM对这个代码实现的注释是

A simple semi-space scavenger heap, mainly for testing. No, we do NOT share code with other implementations here, even if this means duplication of effort. This code base is supposed to remain stable, as a reliable fallback position. Refactoring of whatever other fancy memory management library must not damage the functionality here.

即这只是个最基础的实现。更多GC算法的知识,推荐阅读

l  《垃圾回收算法手册:自动内存管理的艺术》,理查德·琼斯 (Richard Jones)

l  《垃圾回收的算法与实现》,中村成洋相川光

 


  [m1] 遍历整个线程栈

  [m2] LAST_JAVA_CALLER_STACK_POINTERLOWEST_ACTIVE_STACK_SLOT_ADDRESSHIGHEST_STACK_SLOT_ADDRESSLOWEST_STACK_SLOT_ADDRESS是基于线程栈基址的四个偏移量,对应的位置可查看上文栈内存布局

  [m3] 对每一个线程栈,都分成两段遍历。因为每个线程栈中间都有两个不可读写的内存页(即YellowZoneRedZone

  [m4] 遍历线程栈的其中一段内存

描述  [m5] 栈内引用类指针分布的位图reference map

这是一个闭区间,即[   [m6] lowest, highest ]

  [m7] 遍历位图中的一个字节

  [m8] 把栈上的指针指向移动后的对象

  [m9] 指针值范围在FROM,,说明对象可能需要复制移动(当已经移动过了就不需要了)

  [m10] 读出forwarding指针值,相当于Grip* forwarding = fromOrigin->forwarding

  [m11] 指针值不为NULL,说明对象已经被移动过了

  [m12] TO段分配size大小的空间

  [m13] from对象的forwarding指针域写入to对象的grip地址

  [m14] debug模式下,对象前后会被加上padding。此处是把可能的padding去掉

  [m15] cell变量指向的地址是对象的java部分,此处计算为整个对象的起始地址

  [m16] 检查old对象是否已经被移动到TO空间了,返回移动后的地址

  [m17] 如果oldnew地址不等,说明在mapGrip中发生了复制移动

  [m18] 相当于C++的指针强转

  [m19] 相当于Hub* h; (*h)。如此可以获知对象大小、字段类型及偏移等

  [m20] TupleJavaPOJO/Cstruct,其字段是reference和基本数据类型的集合。与之对应的是Array,其字段只能是基本数据类型,或reference

  [m21] JDK中的java.lang.ref.Reference及其子类,GC中需要特殊处理

  [m22] hybrid表示tuplearray的混合布局,类似与Cstruct {int a; void* b; char[10] c;}

  • 大小: 64.4 KB
  • 大小: 71.2 KB
  • 大小: 69.1 KB
  • 大小: 63.2 KB
  • 大小: 95.8 KB
分享到:
评论

相关推荐

    代码走读记录表模板代码走读记录表模板

    代码走读记录表模板代码走读记录表模板代码走读记录表模板

    代码走读记录

    代码走读记录,又称代码审查记录,包含C++代码走读,JAVA代码走读,C#代码走读

    代码走读检查列表[参考].pdf

    在软件开发过程中,代码走读是一项重要的质量保证活动,它能帮助团队成员理解代码逻辑,发现潜在的问题,以及确保代码遵循既定的设计原则和最佳实践。以下是对代码走读检查列表的一些关键点的详细说明: 1. **设计...

    SPEEDX 代码走读笔记.txt

    ### SPEEDX 代码走读笔记知识点解析 #### LMS算法详解 在文本中提到了LMS(Least Mean Squares)算法的基本公式与参数调整方法。LMS算法是一种用于自适应滤波器的设计方法,主要用于噪声抑制、回声消除等场景。 - ...

    代码走读[总结].pdf

    代码走读是软件开发中的一种重要技术,旨在通过检查代码是否符合编程规范、寻找编译器中的设计陷阱、快速理解源代码、对原有代码的重构等步骤,提高代码的质量和可维护性。 代码走读可以分为四个层次:检查是否符合...

    C++代码走读意见--开发注意事项

    ### C++代码走读意见与开发注意事项 #### 内存管理与安全性 在软件开发过程中,尤其是使用C++这类提供底层内存操作的语言时,代码质量和安全性尤为重要。本篇将基于给定的“C++代码走读意见--开发注意事项”文件中...

    代码走读工具Jupiter实践.pdf

    ### 代码走读工具Jupiter实践 #### 代码复查概览 **代码复查**,亦称作**代码审查**,是一种软件开发中的质量控制措施,旨在通过非代码编写者对已编写的代码进行检查,来识别并修正其中的缺陷或不足之处。这一实践...

    单元测试pclint代码走读重点

    【单元测试PCLint代码走读重点】是一个关于软件质量保证和缺陷预防的主题,主要讨论如何在软件开发过程中,通过代码审查和单元测试工具PCLint来发现并消除潜在的编程错误。潜在缺陷是指那些在常规测试过程中难以察觉...

    代码走读时需要关注的内容分析

    ### 代码走读时需要关注的内容分析 #### 一、准备工作 在进行代码走读之前,准备工作至关重要。这部分主要包括以下几个方面: 1. **设计文档**:确保开发人员已获得一个设计文档来理解代码,该文档应该是最新的...

    DPDKL2fwd代码走读报告(代码流程分析).pdf

    DPDKL2fwd代码走读报告(代码流程分析) DPDK(Data Plane Development Kit)是一种高性能的网络数据包处理平台,能够在通用x86服务器上实现高速网络数据包转发。该平台通过使用hugepage、uio、zero copy、cpu ...

    Storm源码走读笔记

    本文档是关于Storm源码的详细走读笔记,主要分析了Storm的启动场景、Topology提交过程、worker进程中的线程使用情况、消息传递机制以及 TridentTopology的创建和ack机制等多个方面。 首先,文档提到了Storm集群中的...

    FFmpeg开发资料和代码走读报告

    本资料包聚焦于FFmpeg的开发学习,特别是通过一系列的代码走读报告来深入理解其内部工作原理。 首先,我们可以看到一系列的教程源码文件,如`tutorial01.c`到`tutorial08.c`。这些文件很可能是逐步介绍FFmpeg API...

    广播分发代码走读

    ### 广播分发机制详解 #### 一、概述 Android平台中的广播机制是一种非常重要的组件间通信方式,它能够实现跨应用的数据共享与事件通知功能。本文将深入解析广播分发的过程,从源码层面逐步拆解广播的传递路径。...

    单元测试代码走读的一些需要注意的内容

    代码走读是一种审查技术,通过检查代码的各个部分来发现潜在的问题。以下是对这些测试项的详细解释: 1. **J1 下标变量越界**:确保数组或集合的索引不会超出边界,防止引发ArrayIndexOutOfBoundsException。 2. *...

    代码走读与研发自测

    详细介绍了代码走读相关的检查点以及单元测试和集成测试相关的理论。

    代码走读检查列表.pdf

    代码走读,也称为代码审查,是软件开发过程中的一个重要环节,它有助于发现潜在的错误、提高代码质量、促进团队间的知识共享以及确保代码符合既定的设计规范和标准。以下是一份详细的代码走读检查列表,它涵盖了从...

    ALSPS原理以及mtk平台代码走读.pptx

    环境光传感器通过光敏电阻,光敏二极管,光敏三极管,硅光电池等光敏元件可以感知周围光线,stk3x1x – 光敏二极管

    TI AWR1642 代码走读设计文件

    在这个“TI AWR1642 代码走读设计文件”中,我们将探讨其背后的软件设计与实现。 首先,源码是理解任何硬件设备功能的关键,特别是对于AWR1642这样的复杂系统。源码软件包含了驱动程序、应用程序接口(API)、算法...

    二次雷达代码走读文档.docx

    "二次雷达代码走读文档" 二次雷达代码走读文档是关于二次雷达系统的详细代码分析报告,涵盖了二次雷达的准备工作、程序结构、函数检查等方面的知识点。 一、准备工作 二次雷达文件位于 peripherals 目录下,包括...

    Apache Spark源码走读:如何进行代码跟读

    ### Apache Spark源码走读:如何进行代码跟读 #### 概述 本文旨在探讨如何有效地进行Apache Spark源码的阅读与理解。Apache Spark作为一款高性能的分布式计算框架,在大数据处理领域占据着重要地位。其核心由Scala...

Global site tag (gtag.js) - Google Analytics