美文网首页java技术交流社区
太顶了!阿里架构师深入讲解垃圾回收算法,强烈建议做开发的都看看。

太顶了!阿里架构师深入讲解垃圾回收算法,强烈建议做开发的都看看。

作者: 程序员伟杰 | 来源:发表于2021-05-12 21:15 被阅读0次

    〇、补充-数组长度

    补讲数组长度:

    1、如果不压缩,则在arrayOopDesc中声明的非静态字段之后分配。

    此时存klass指针 + 数组长度要用 12字节
    eg: 11111 11111

    2、如果压缩,它将占用oopDesc中_klass字段的后半部分

    此时存klass指针 + 数组长度要用 8字节。因为压缩后klass指针只占用4个自己,还剩4个空的字节,用来存储数组长度,而不是去再申请一个8个字节的长度,然后将压缩至4字节。
    eg:1111 [多出来的四个字节存数组长度:1010]

    这里是如何生省出来4个字节的呢?
    因为存储类型和数组长度的用的是联合体,联合体大小就是8个字节,如果压缩开启后只用4个字节,剩下的4个字节就浪费了,所以用来存储数组长度。因为数组长度刚好是int,占用四个字节。

    我们来看存储数组长度的联合体:

    union_metadata {
        Klass*      _klass;  // 占用8B
        narrowKlass _compressed_klass; // 占用4B
    } _metadata;
    
    

    整个联合体占用8字节

    • JVM 四大模块: 1、类加载 2、内存模型 3、执行引擎 4、垃圾回收器
    • 其中,垃圾回收器是基于垃圾回收算法实现的。
    • 垃圾判断算法
    • 垃圾回收算法

    标记清楚算法:标记的是存活对象还是等待回收的对象?
    个人整理了一些资料,有需要的朋友可以直接点击领取。

    Java基础知识合集

    百本Java架构师核心书籍

    对标阿里P7的java架构师学习路线图

    结合金三银四整理的2021年最新面试题

    垃圾判断算法

    引用计数法

    和可重入锁有点像,进入锁之后这个数+1,释放锁之后-1.

    在对象中添加一个属性用于标记对象被引用的次数,每多一个其他对象引用,计数+1,当引用失效时,计数-1,如果计数=0,表示没有其他对象引用,就可以被回收。
    这个算法无法解决循环依赖的问题。


    可达性分析

    通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系链向下搜索,如果某个对象无法被搜索到,则说明该对象无引用执行,可回收。相反,则对象处于存活状态,不可回收。
    JVM中的实现是找到存活对象,未打标记的就是无用对象,GC时会回收。


    哪些对象可以作为GC Root呢:
    • 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
    • VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
    • JNI handles,包括global handles和local handles
    • (看情况)所有当前被加载的Java类
    • (看情况)Java类的引用类型静态变量
    • (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
    • (看情况)String常量池(StringTable)里的引用

    复制算法:给存活对象打标记,回收没有打标记的对象;

    内存分配算法

    虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

    在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump thePointer)。如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(FreeList)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

    指针碰撞(open jdk基于此实现,无限CAS,自旋锁)

    取到指针地址,然后加上对象的大小。
    然后判断相加后的值是否小于总的内存大小。
    因为分配内存是并发的,所以使用了CAS去比较,如果失败,goto retry,继续分配。
    CAS失败了,就代表该块内存已经被使用了。


    空闲列表

    0~10
    
    0-5:    m_available_table   当前可以用内存
    6-10:   m_idle_table 空闲列表
    
    list<Memorycell> m_available_table; // 所有可使用的内存
    list<Memorycell> m_used_table;      // 所有被使用的内存
    list<Memorycell> m_idle_table;      // 空闲内存
    list<Memorycell> m_transer_table;   // 待交换内存
    
    

    内存池及相关算法

    先搞明白角色

    • OS 堆
    • Memory Pool
    • Memory Chunk
    • Memory Cell

    Memory Poo l内存池

    理解成一个管理员,管理的是内存块,不直接持有内存

    内存池中使用一个列表维护着内存块:


    能做的事情:
    向OS申请内存:malloc、calloc
    释放内存:没有垃圾回收器,需要手动释放
    其他:打印chunk信息

    Memory Chunk 内存块

    直接持有内存
    管理内存块

    将分配到的内存分块处理(Memory Cell, 内存细胞)
    每个Cell占8字节

    Memory Cell

    真实在使用的内存
    每个cell占的字节大小,由开发者决定。JVM中是8字节

    每一个角色的分析

    OS堆:操作系统也是有内存模型的
    堆(OS堆)

    静态区域
    代码区域 可读可写可执行

    Memory Pool

    MemoryChunk *new_chunk(uint_size);  // 像操作系统要内存
        1、根据你要的内存以及对齐(8B)计算出向OS要多大的内存;
            如果要了79B,那么根据8字节对齐计算后,会向OS申请80B内存。
        
        2、向OS申请内存   
        
        3、根据不同的垃圾回收算法,填充不同的list
            标记清除、标记整理,这两种算法回收的是整个chunk. 这两种算法只需要两个list就能玩转
                list<Memorycell> m_available_table; // 所有可使用的内存
                list<Memorycell> m_used_table;      // 所有被使用的内存
            
            分代+复制算法:五五分,一半在用,一般空闲。四个list都要用]
                From 区 => TO 区
                0-10    正在用(gc) -》 我这边没有被回收的对象放在哪里 (From)-<(To)
                11-20   空闲 (To)->(From)
    
                list<Memorycell> m_available_table; // 所有可使用的内存    0-10 cell
                list<Memorycell> m_used_table;      // 所有被使用的内存   
                list<Memorycell> m_idle_table;      // 空闲内存         11-20 cell  
                list<Memorycell> m_transer_table;   // 待交换内存
            
    void print_chunks();                // 调试用的
    
    void free_chunks();                 // 释放内存,用C或者C++申请的内存需要手动释放
    
    

    内存算法怎么写,取决于你要支持的垃圾回收算法

    垃圾回收算法

    1、将内存55分;

    标记-清除算法

    遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。

    面向整个堆,会产生碎片

    如果需要分配大对象,需要连续的内存
    而你的内存是碎片化的,分配不到内存。所以出现了标记整理算法,但是标记整理算法比较耗费CPU。

    我们知道,Eden区的对象通常都是招生夕灭,对象通常在第一次gc就会被回收,生命周期很短,会产生大量的内存碎片。

    这个时候不是没有内存,而是内存不是连续的,导致无法分配对象。

    清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。

    标记-整理算法

    内存碎片合并算法
    老年代基本都是基于这个算法实现的,因为老年代本身的回收对象的概率比较低。

    能整理内存
    合并碎片的时候需要STW

    合并碎片的时候,面临两种空间:
    1、这个空间的对象已经被释放
    2、这个空间的对象未被释放(复杂):合并内存、数据移动、指针移动

    分代+复制算法

    存在的目的:解决标记-整理算法,合并碎片消耗性能过高的问题、以及gc停止用户线程过长的问题
    1、将内存五五分(JVM中是8:1:1),一半用(0-10, From,),一半空闲(11-20, To);
    2、发生gc的时候,切换角色:
    原先的From区的内存处理:

    标记的对象清理
    存活的对象需要移动到新的内存区域(11-20)
    数据整理
    指针整理

    总结:

    不管是现在的9种垃圾回收器还会未来新的垃圾回收器,他们的底层都是这三种回收算法。

    指针移动后,老的指针怎么办?

    移动以后,老的地址不变,非常复杂。需要借助另外的数据结构,指针是动态计算出来的。下面是计算公式:


    chunk->get_data()获取到chunk的首地址,

    总结

    内存池
        JVM不需要我们手动释放内存,有垃圾回收器自动回收;
    
    垃圾回收算法
        标记-清除算法
            标记阶段:遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象
            清除阶段:将没有标记的对象全部清除掉。
        
        标记-整理算法
            标记存活的对象
            清除没有被标记的对象
            
            标记和清理这两个阶段基本和标记-清理算法是一样的
    
            内存整理:
        
        分代-复制算法
            因为整理算法性能太低,而且新生代的特点是招生夕灭
                
    
    
    

    最后

    各位大佬都看到这里了给个赞呗

    相关文章

      网友评论

        本文标题:太顶了!阿里架构师深入讲解垃圾回收算法,强烈建议做开发的都看看。

        本文链接:https://www.haomeiwen.com/subject/oplzdltx.html