[JVM] jvm自动内存管理 垃圾清除的基础逻辑依据(九)

编程语言 编程语言 8347 人阅读 | 0 人回复

根节点

对于对象的标记,现在主流的虚拟机基本都是使用可达性分析,来确认一个对象是否已经死亡的。

可达性算法的基础是需要确定GCRoots,只要确定了根节点,也就是起始点,才能够进行可达性分析。

如果这个范围过大,囊括了一些不必要的对象,那么最终结果虽然没问题,但是耗费了大量的时间进行枚举、可达性分析判断,这是无意义的。

如果这个范围不足,缺少了一些需要的对象,那么最终很可能把本来需要的对象清理了,这是致命的。

我们的目的是期望:刚刚好,不多也不少

针对于实现上来说,就是需要高效、快速、准确地列举出来GCRoots。

image.png

STW

应用是时刻在运行的,Jvm的内存空间中,存在根节点集合的对象引用关系还在不断变化的情况,也就是时刻进行着对象的创建、改变的。

那么如何能够准确地获取到你想要的那些初始节点呢?

答案就是STW,Stop The World ,也就是瞬间冻结一切

在这一瞬间产生的快照中,进行标记处理,因为在这个标记获取的过程中,引用关系是并没有发生变化的,因为所有都被冻结了

JVM内部数据表示模型简介

C++虚函数以及JVM内部表示

继续之前,插一段相关知识点,简单提一下。

1.虚函数的简介 由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。

用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。

这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。

虚函数主要通过V-Table虚函数表来实现,该表主要包含一个类的虚函数的地址表,可解决继承、覆盖的问题。

当我们使用一个父类的指针去操作一个子类时,虚函数表就像一个地图一样,可指明实际所应该调用的函数。(每一个virtual函数的class都有一个相应的vtbl,当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。)

防止多重派生时,使用指针调用同名函数时已基类函数(父类)为准。

2.虚函数的核心概念 某基类中声明为virtual并在一个或多个派生类中重新定义的成员函数叫做虚函数。

3.虚函数的核心作用 实现动态联编,在函数运行阶段动态的选择合适的成员函数。 在定义了虚函数后,可实现在派生类中对虚函数进行重写,从而实现统一的接口和不同的执行过程。

#include<iostream>  
using namespace std;  

class A  
{  
public:  
    void foo()  
    {  
        printf("1\n");  
    }  
    virtual void fun()  
    {  
        printf("2\n");  
    }  
};  
class B : public A  
{  
public:  
    //隐藏:派生类的函数屏蔽了与其同名的基类函数
    void foo()  
    {  
        printf("3\n");  
    }  
   //多态、覆盖
    void fun()  
    {  
        printf("4\n");  
    }  
};  
int main(void)  
{  
    A a;  
    B b;  
    A *p = &a;  
    p->foo();  //输出1
    p->fun();  //输出2
    p = &b;  
    p->foo();  //取决于指针类型,输出1
    p->fun();  //取决于对象类型,输出4,体现了多态
    return 0;  
}

image.png

oop-klass模型

hotspot虚拟机内部用opp-klass模型来表示一个Java类。

Hotspot的底层实现使用C++实现的,C++也是一门面向对象的语言,很自然的底层也会有与java对象对应的c++对象。

而这个模型就是为了这个理论基础服务的。

通过oop+klass的形式,一分为二,避免了每个oop中都保存一份虚函数表。

oop普通对象指针用来表示类的实例数据,这些数据保存在了指针所存放的内存首地址后那片区域,生成一张数据视图,里面保存了类的实例对象在程序运行期间各个属性的值。

klass保存了Java类的各种变量和方法,例如类变量、成员变量、成员方法和构造函数等。

它会生成两张表:

一张是元数据表,保存了该类的全部数据结构信息

一张是虚函数表

虚函数是C++里的一个特性,用关键字“virtual”来修饰一个方法,JVM内部就是使用C/C++实现的,所以里面有很多的C++方法,每一个Java类最后都会被JVM转换成内部的C++类。

因为Java语言没有“virtual”这个关键字,也就是没有虚函数,那么Java实现多态的方式是什么?

答案是Java类将所有的函数都当作“虚函数”来处理,不用在前面添加“virtual”之类的关键字修饰,类中的所有方法都可以直接被子类覆盖。

所以JVM的klass模型为每一个类都生成虚函数列表,用以维护Java类和内部C++类的对应关系。

image.png

如上图所示,oop与klass的关系,有点类似对象与实例的关系。

hotspot部分的代码可以直接查看源码

oop

是普通对象指针,存放Java类的实际数据,JVM内部定义了许多___oopDesc*类型的指针,这些指针都由GC(garbage collection垃圾回收)管理。

//https://github.com/openjdk/jdk/blob/master/src/hotspot/share/oops/oop.hpp
//所有oops共同基类
typedef class   oopDesc*                    oop;
//表示Java类型实例
typedef class   instanceOopDesc*            instanceOop;
//表示Java方法
typedef class   methodOopDesc*              methodOop;
//表示Java方法中的只读信息
typedef class   constMethodOopDesc*         constMethodOop;
//性能统计数据结构
typedef class   methodDataOopDesc*          methodDataOop;
//oops数组
typedef class   arrayOopDesc*               arrayOop;
//引用类型数组
typedef class   objArrayOopDesc*            objArrayOop;
//基本类型数组
typedef class   typeArrayOopDesc*           typeArrayOop;
//.class文件中的常量池
typedef class   constantPoolOopDesc*        constantPoolOop;
//常量池缓存
typedef class   constantPoolCacheOopDesc*   constantPoolCacheOop;
//指向klass实例
typedef class   klassOopDesc*               klassOop;
//对象头
typedef class   markOopDesc*                markOop;
typedef class   compliedICholderOopDesc*    compliedICHolderOop;

每一个类型代表JVM内部一个特定的对象模型,也就是说在Java程序运行时,每创建一个新的对象,都会有对应的OopDesc对象生成。

 /* src/hotspot/share/oops/oop.hpp */
 class oopDesc {
  private:
   // 对象头
   volatile markWord _mark;
   //元数据
   union _metadata {
    //对应的class对象
     Klass*      _klass;
     narrowKlass _compressed_klass; // 压缩版本
   } _metadata;
   // 紧接着存放Java对象的实例字段
   // 紧接着存放Java类的静态字段
    //(注意只有java.lang.Class类的实例才有这部分内容,也就是Klass中的_java_mirror字段)
 }

 /* src/hotspot/share/oops/instanceOop.hpp */
 class instanceOopDesc : public oopDesc {}

klass


//所有klass共同基类
class   Klass;
//JVM内部与Java类对等的结构
class   InstanceKlass;
//描述java.lang.Class实例
class   InstanceMirrorKlass;
//描述java.lang.ref.Reference子类
class   InstanceRefKlass;
//Java类方法
class   methodKlass;
//Java类方法对应的字节码指令
class   constMethodKlass;
class   methodDataKlass;
//klass链路的末端
class   klassKlass;
class   instanceKlass;
//标识Java数组
class   arrayKlass;
//标识引用类型数组
class   objArrayKlass;
//标识基本类型数组
class   typeArrayKlass;
//.class文件中的常量池
class   constantPoolKlass;
//常量池缓存
class   constantPoolCacheKlass;
class   compliedICHolderKlass;

klass作为java类的对等模型,描述了java类的元信息,所以也存在整个体系,创建了什么样的类,就会有什么类型与之对应。

/* src/hotspot/share/oops/oopsHierarchy.hpp */
 class     Klass;
 class     InstanceKlass;              // 普通Java类,有其生成的oop对象存储了类中的实例字段
 class     InstanceMirrorKlass;      // java.lang.Class类,除了Class类自身的实例字段,由其生成的oop还存储了对应Java类中的静态字段
 class     InstanceClassLoaderKlass; // 类加载器类
 class     InstanceRefKlass;         // 引用类
 class     ArrayKlass;
 class     ObjArrayKlass;
 class     TypeArrayKlass;
/* src/hotspot/share/oops/klass.hpp */
 class Klass {
   jint        _layout_helper; // 包含oop header和实例字段的长度,创建对应的oop时需要
   int         _vtable_len;    // 实例方法长度
   OopHandle   _java_mirror;   // 类对象的Class类实例
   Klass*      _super;         // 父类

   klassVtable vtable() const; // 虚函数表
 }

 /* src/hotspot/share/oops/instanceKlass.hpp */
 class InstanceKlass: public Klass {
   int             _nonstatic_field_size;  // 实例字段的长度
   int             _static_field_size;     // 静态字段的长度,生产mirror时用到
   int             _itable_len;            // 实现的接口个数
   Array<u2>*      _fields;                // 实例和静态字段信息,包括偏移值
   Array<Method*>* _methods;               // 实例和静态方法信息,有解释执行和编译执行的入口
     Array<Method*>* _default_methods;       // 类所实现的接口中的默认实现方法

   // 根据类型分配InstanceKlass或者他的子类
   static InstanceKlass* allocate_instance_klass(const ClassFileParser& parser, TRAPS);
   // 生成类的实例对象oop,包含实例字段在内
   instanceOop allocate_instance(TRAPS);
   bool link_class_impl(TRAPS);  // 方法链接,包括方法链接(确定解释器入口和编译器入口),接口列表的初始化
   klassItable itable() const;   // 接口和其方法表
   // 紧接着存放类的实例方法列表(虚函数表,vtable)
   // 紧接着存放类的接口和接口方法列表(itable)
   // ...(还有其他信息,省略)
 }

 /* src/hotspot/share/oops/instanceMirrorKlass.hpp */
 class InstanceMirrorKlass: public InstanceKlass {
   static int _offset_of_static_fields;  // 静态字段在oop对象中的偏移量
   // 生产类对应的java.lang.Class类的实例oop对象
   // 相比InstanceKlass的allocate_instance,会把普通类的静态字段考虑进来
   instanceOop allocate_instance(Klass* k, TRAPS);
 }

handle

https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/handles.hpp

class Handle {
 private:
  //指向oop
  oop* _handle;

 protected:
  oop     obj() const                            { return _handle == NULL ? (oop)NULL : *_handle; }
  oop     non_null_obj() const                   { assert(_handle != NULL, "resolving NULL handle"); return *_handle; 
  .....  
}

handle 拥有指向oop的指针,也就是通过handle 可以获取到对象实例oop,根据oop又可以获取到java类的元数据信息。

Handle(句柄)只是对oop的间接引用。

从hotspot的名词解释中,有说明它的作用,简单说VM内通过handle对oop进行访问,这样当GC对对象内存位置变更时,handle是不变的, 改变的是内部的指向。

C/C++代码通常通过句柄间接引用oop,以使GC更容易找到和管理其根集。

https://openjdk.org/groups/hotspot/docs/HotSpotGlossary.html

handle

A memory word containing an oop. The word is known to the GC, as a root reference. C/C++ code generally refers to oops indirectly via handles, to enable the GC to find and manage its root set more easily. Whenever C/C++ code blocks in a safepoint, the GC may change any oop stored in a handle. Handles are either 'local' (thread-specific, subject to a stack discipline though not necessarily on the thread stack) or global (long-lived and explicitly deallocated). There are a number of handle implementations throughout the VM, and the GC knows about them all.

OopMap

主流Java虚拟机使用的都是准确式垃圾收集,也就是有办法直接得到哪些地方存放着对象引用的。

怎么知道的呢?

答案是通过OopMap,前提是oop-klass模型基础,以及handles。

ordinary object pointer map 可以翻译为普通对象指针地图

他是oop+map的组合,直接当做是oop的map就好了,通过这个map映射了使用到的引用,而不是需要全量遍历。

可以参考:https://www.crazybytex.com/thread-217-1-1.html

image.png

根据栈和寄存器中的一些信息,生成oopmap

根据oopmap可以确定栈里和寄存器里哪些位置是引用

通过“空间换时间” 牺牲掉了oopmap占用的空间,换取不全量扫描栈和寄存器来感知那些位置是引用。

安全点

既然oopmap好用,记录了某时刻的状态,是不是总是都去创建维护oopmap呢?

显然是不可以的,也是不能的,性能、空间开销浪费。

因为会产生oopmap引用关系变动的指令有很多,无法每个指令都进行oopmap的创建。

所以有了安全点的概念

安全点就是选取出来一些、oopmap 引用关系可能发生变化的位置,在这个时候进行map的生成。

也就是并非在代码指令流的任意位置都能够停顿下来开始垃圾收集, 而是强制要求必须执行到达安全点后才 能够暂停。

如果安全点太少,垃圾收集器需要等很久才能到下一个安全点;

如果安全点太多,增加了系统运行的性能开销;

安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的, 因为每条指令执行的时间都非常短暂, 程序不太可能因为指令流长度太长这样的原因而长时间执行。

“长时间执行”的最明显特征就是指令序列的复用, 例如方法调用、 循环跳转、 异常跳转等都属于指令序列复用, 所以只有具有这些功能的指令才会产生安全点。

如何停在安全点

image.png

抢占式:

在垃圾收集发生时, 系统首先把所有用户线程全部中断, 如果发现有用户线程中断的地方不在安全点上, 就恢复这条线程执行, 让它一会再重新中断, 直到跑到安全点上。

主动式:

不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。

现在主流都是主动式的中断。

比如,方法调用结束后,设置安全点,也在方法调用结束后,进行轮询,安全点和轮询标志的地方是重合的,自然而然的就可以在最近的安全点停下。

HotSpot使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度。

安全区域

对于正在运行的部分,通过安全点即可,但是对于类似sleep的线程或者blocked的线程,线程无法响应虚拟机的中断请求, 不能再走到安全的地方去中断挂起自己, 虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。

但是在这些状态下的线程,他们的引用关系其实并不会发生变化,所以这时称之为进入了安全区域。

在安全区域内进行垃圾回收,也是安全的。

只要进入安全区域就可以进行垃圾回收

出来的时候看下是否枚举根节点结束即可。

image.png

小结

不管是什么垃圾收集算法还是垃圾收集器,都有标记的过程,标记就需要有确认的过程,本文主要介绍的标记中需要了解到的一些理论基础,后续部分随着垃圾收集器的说明展开。

common_log.png 转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-216-1-1.html

关注下面的标签,发现更多相似文章
    黄小斜学Java

    疯狂的字节X

  • 目前专注于分享Java领域干货,公众号同步更新。原创以及收集整理,把最好的留下。
    包括但不限于JVM、计算机科学、算法、数据库、分布式、Spring全家桶、微服务、高并发、Docker容器、ELK、大数据等相关知识,一起进步,一起成长。
热门推荐
海康摄像头接入 wvp-GB28181-pro平台测试验
[md]### 简介 开箱即用的28181协议视频平台 `https://github.c
[CXX1300] CMake '3.18.1' was not
[md][CXX1300] CMake '3.18.1' was not found in SDK, PATH, or
[若依]微服务springcloud版新建增添加一个
[md]若依框架是一个比较出名的后台管理系统,有多个不同版本。