G1GC的算法与实现之实现篇笔记


《深入Java虚拟机:JVM G1GC的算法与实现》-实现篇笔记

在上一篇文章中我们记录了G1GC的算法包括内存结构标记位图SATB本地队列转移专用记忆集合等关键组成部分,接下我们会对HotSpotVM的具体实现方法进行分析;

HotSpot的代码结构

HotSpot的源码位于src/hotspot下,如下所示

文件夹 说明
cpu 依赖CPU的代码
os 依赖操作系统的代码
os_cpu 依赖操作系统和CPU的代码
share 通用代码

在share下又划分为以下文件结构

文件夹 说明
ci C1编译器
classfile Java类文件的定义
gc GC部分
interpreter Java解释器
oops 对象结构的定义
runtime VM运行时所需库

参考openjdk

HotSpot内部的大部分代码都是继承与以下两个类中的一个:

  • CheapObj类
  • AllStatic类

下面对这两个类进行分析:

  • CheapObj类

CheapObj类是一个由C的堆内存空间来管理的类,CheapObj类的子类实例都会被分配到C的堆内存上;

  • AllStatic类

AllStatic类是一个"仅带有静态信息"的特殊类,继承AllStatic的类不需要创建实例;

由于HotSpotVM需要运行于各种操作系统之上.因此,开发者为HotSpotVM设计了一种巧妙的结构(接口),使得它能够通过统一的接口来处理各种操作系统的API;

  • os.hpp
class os: AllStatic {
  friend class VMStructs;
  friend class JVMCIVMStructs;
  friend class MallocTracker;
//省略
}

os类中定义的成员函数在HotSpotVM中都有对应的各种操作系统实现:

  1. os/posix/vm/os_posix.cpp
  2. os/linux/vm/os_linux.cpp
  3. os/windows/vm/os_windows.cpp
  4. os/solaris/vm/os_solaris.cpp
    在构建OpenJDK时,hotspot会从以上文件中,选择与当前系统对应的文件进行编译和链接;

当VM调用os.hpp时,对于操作系统下的实现类就会执行具体方法;

  • os_windows.cpp
#ifdef _DEBUG
#include <crtdbg.h>
#endif

#include <windows.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/timeb.h>
#include <objidl.h>
#include <shlobj.h>

//省略....
  • os_windows.cpp
#ifdef _DEBUG
#include <crtdbg.h>
#endif

// put OS-includes here
# include <sys/types.h>
# include <sys/mman.h>
# include <sys/stat.h>
# include <sys/select.h>
# include <pthread.h>
# include <signal.h>
# include <endian.h>
# include <errno.h>
# include <dlfcn.h>

//省略....

在不同的实现下引入了不同的.h文件;

堆结构

堆结构大体上可以划分为两个部分:

  1. 程序员选择的GC算法所使用的内存空间
  2. 常驻内存空间

VM堆划分

常驻内存空间通常是用于分配类型信息或方法信息等永久存在的对象,该空间几乎不会随着GC算法的变化而变化;

在 JDK 8 及之后的版本中,永久代已经被移除,被一个称为元空间(Metaspace)的区域所取代。因此,可以说 JDK 8 及之后的版本中没有永久代这一概念。不过,元空间和永久代的作用类似,都是用于存放类信息等元数据的区域,只不过它们的实现方式和内存模型不同。 元空间和永久代最大的不同在于,元空间使用本地内存(native memory)存储元数据,而不是像永久代一样使用虚拟机内存(Java heap)来存储。这样的好处是可以避免永久代出现的内存溢出问题,因为元空间的大小可以根据需要动态调整,并且可以使用操作系统的内存分配器来管理内存。另外,元空间与永久代相比还有一些其他的不同点,例如元空间可以通过命令行参数来配置大小、元空间的垃圾回收机制与永久代不同等等。但是,从常驻内存空间的角度来说,可以认为元空间已经取代了永久代,成为了 Java 虚拟机中存放类信息等元数据的常驻内存区域。

在JVM中是通过Universe:initialize_heap来实现创建堆内存的功能,如下所示,会根据create_heap()initialize()方法选择不同的实现;

创建堆内存选择执行类

下面详细看一下G1CollectedHeap,其中有三个重要的成员变量:

  1. _hrs:通过数组维护所有的HeapRegion
  2. _young_list:新生代HeapRegion的链表
  3. _free_region_list:空闲HeapRegion的链表

管理各个区域是通过HeapRegion类来实现的,在G1CollectedHeap中为了快速找到每一个HeapRegion,因此用HeapRegionSeq(Heap Region Sequence)是用于表示堆区域(Heap Region)的序列或集合的地址,_hrs就是指向HeapRegionSeq的指针;

G1GC堆的结构

分配器

内存分配的流程

Vm堆空间申请 -> VM堆空间分配 -> 对象的分配
下图展示这个过程

内存分配的流程

需要注意的是在Linux上,用于实现内存申请和分配的是mmap(),在Linux中没有申请内存空间的概念,调用mmap()后就会分配内存空间,不过并不是立即分配物理空间,在这这中间还有一层虚拟内存;只有在分配到的内存空间被访问时才会实际发生物理内存分配;

对象分配的流程

对象分配的流程

TLAB

TLAB(Thread Local Allocation Buffer,线程本地分配缓冲区)是对象分配的要点之一;
VM是所有线程共享的内存空间,因此当需要在VM堆上分配对象时,必须锁定整个堆,以防止其他线程同时分配对象;
但是为了让不同线程工作于不同的CPU核心上时需要分配对象时不用等待VM堆上的锁释放,因此引入了TLAB的概念,解决思路就是让各个线程拥有自己的专用对象分配缓冲区,从而减少锁定次数;
当一个线程第一次分配对象时,它会从VM堆中得到一定大小的内存空间,然后作为它自己的缓冲区保存下来,当这个线程需要分配对象时,优先从这块专用区域进行分配;

对象结构

  • oopDesc类

oopDesc类是所有GC目标对象的抽象基类,继承自oopDesc的类实例都是GC的目标对象;

oopDesc类

在第第56行代码中的_mark变量是对象头,_mark中不仅保存了标记-清除算法的标记,还保存了对象所需的其他各种信息;在oopDesc中有一个指向自己类的指针,在代码的
第57行_metadata.在大部分情况下,这个联合体中保存的是_klass变量的值,kass保存的是指向对象类的指针.

  • klass
    klass继承于oopDesc,用来表示类型信息,Klass的实例是作为klassOop的一部分创建出来的;

HotSpot的线程管理

在Windws和Linux中都有用于调用操作系统线程的库,在Windows上我们使用的是Windows Api调用线程,在Linux上我们使用的是POSIX线程标准的Pthreads库来调用线程;

在HotSpotVm内可以使用相同的方式调用不同操作系统得益于设计出来’线程抽象层’,最重要的就是Thread;

Thread类

Thread类的继承关系

在Thread类中通过定义虚函数run(),子类进行实现,其中JavaThread表示的就是Java语言级别运行的线程,NameThread类是支持线程命名的,我们可以通过为对NameThread实例设置一个唯一名字;

线程的什么周期

  1. 创建Thread类的实例
  2. 创建线程(调用os:create_thread)
  3. 开始线程处理(调用os:start_thread)
  4. 结束线程处理
  5. 释放Thread类实例

一个windows下创建线程的例子:


#include <Windows.h>
#include <stdio.h>

DWORD WINAPI ThreadFunc(LPVOID);

int main()
{
    HANDLE hThread;
    DWORD threadId;

    hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId); // 创建线程
    printf("我是主线程, pid = %d\n", GetCurrentThreadId());      // 输出主线程pid
    Sleep(2000);
}

DWORD WINAPI ThreadFunc(LPVOID p)
{
    printf("我是子线程, pid = %d\n", GetCurrentThreadId()); // 输出子线程pid
    return 0;
}

线程的互斥处理

  • 什么是互斥处理?

如果线程共享内存空间,那么就会出现多个线程同时在一个地址上进行读写的情况.有些数据会被其他线程所改变,并且这样的改变是意料之外的;对于这样被修改的对象,被称为"临界区".对于"临界区"的处理必须要按照原子操作;

  • 如何实现互斥处理?
  1. 使用互斥量
    互斥量可以是全局锁/标识/互斥原语等,个人理解互斥量主要是两个特点:全局可见和唯一持有

  2. 监视器
    线程之间通过监视器来完成互斥和协助,在jvm中监视器是由Object中的monitor对象头来进行实现,

互斥方式可以避免线程使用共享数据时被其它线程干扰,而协作方式则帮助多个线程共同完成同一个目标


  TOC