Java~学习垃圾回收算法(GC)的基本原理(标记-回收)

简单介绍GC

  • GC主要是用来按照以对象为单位的方式回收堆区的内存, 堆区的每一个对象都持有了一些内存,释放对应的对象,也就是回收了对应的内存
  • 可以很大程度的避免内存泄漏
    Java~垃圾回收机制知识总结(GC)

GC有需要回收方法区的内存, 但是方法区空间小,数据失去作用的概率低
栈区是不用GC回收的,栈上的内存释放是有明确的(线程结束,栈上的内存就全部释放了,某个栈帧销毁(对应的方法执行完),也会导致对应的局部变量释放)
程序计数器只是保存了一个地址 不需要回收

  • 举个简单的例子来说就好比我借用公共设施,在我不用的时候就应该还回去,但是我忘还了,这是就会有一个管理人员过来把我用过的设施还到借用处,GC就起到这样的一个作用

回收对象的基本思路

  • 简单来说就是先标记,判断这个对象是否需要回收(也就是判断这个对象的生死), 然后把死了的对象回收回去

如何标记判断生死

引用计数法
  • 每个对象都专门分配一个计数器变量,有新的引用指向该对象,引用计数器就+1, 用旧的引用指向别的对象了或者指向null了, 计数器就-1 直到当引用为0表示这个对象没有引用了 于是就可以判定这个对象可以被回收了
  • 一个致命缺点:无法解决循环引用问题
    比如如下代码
/**
 * Created with IntelliJ IDEA.
 * Description: If you don't work hard, you will a loser.
 * User: Listen-Y.
 * Date: 2020-08-03
 * Time: 15:04
 */

class People {
    public String name;
    public People brother;
}
public class Test {

    public static void main(String[] args) {
        People people = new People();
        People people1 = new People();
        
        people.brother = people1;
        people1.brother = people;
        
        people = null;
        people1 = null;
    }
    
}

在这里插入图片描述

  • 分析得 people和people1的引用计数器显然不为0 按理说不应该被回收 但是 我们发现这俩个对象已经用不成了 因为想找到对象people 就得找到对象people的引用,但是这个引用在对象people1中, 所以就得需要找到people1的引用,可是people1的引用又在people中 所以要想解决这个问题又得需要引入一些其他成本 于是Java就抛弃了这个引用计数算法 而是使用可达性分析算法
可达性分析
  • 在代码所有对象之间,其实暗地里都有着一定的关联,这样的关联关系错综复杂,可以类似构成了一个有向图
  • 于是我们可以遍历这个对象的关系图,如果某对象可以被遍历找到.就认为不是垃圾(可达的) 如果遍历不到就说明是不可达的
    从哪开始遍历?
  1. 针对每个线程的每个栈帧的局部变量表(线程有很多,每个线程的栈帧也有很多,每个栈帧中又会有多个变量)
  2. 常量池中的引用对象
  3. 方法区中静态变量的引用对象

遍历的起点不是一个, 而是很多, 把每个起点都要进行往下遍历这些起点统称为GCRoot

回收方法区类对象的规则
  1. 该类的所有实例都已经被回收了
  2. 加载该类的ClassLoader也已经被回收了
  3. 该类的对象没有在代码中被使用了(包括各种静态成员,包括反射等)
    总而言之就是通过可达性分析知道同时满足这个三个条件,就认为该类对象是可以被回收的
各种引用的特点
  • 本质上引用就是一个低配指针, 初心就是为了找到对象 经过我们上面发现 这个引用有一个副作用就是还可以决定对象的生死 但是一个就立马决定生死有点不妥 就有了很多引用
  1. 强引用: 平时用的引用, 既能找到对象 也可以决定对象的生死
  2. 软引用: 可以找到对象,也可以一定程度决定对象的生死(就是可以保对象一时, 不让他立马死掉,这个最终让对象活多久 看内存的时间大小决定)
  3. 弱引用: 能找到对象, 但是不能决定对象的生死
  4. 虚引用: 不能找到对象 也不能决定对象的生死, 只是当对象死了的时候, 做一些善后的事

如何回收

标记-清除

标记清除的本质想法就是直接释放内存
在这里插入图片描述

如上图所示就会造成内存碎片 就导致整体的内存空间剩余很多 但是不连续 无法创建大的连续内存

  • 优点: 简单高效
  • 缺点: 造成内存碎片化
标记-复制

在这里插入图片描述

  • 优点: 能够解决内存碎片化问题 保存内存回收之后 并不存在碎片化
  • 缺点: 需要额外的一块内存区域 而且如果对象比较多的时候,移动起来就比较抵效
标记-整理

在这里插入图片描述
类似顺序表的删除操作 去掉中间一个的时候就把后面的往前移动整理

  • 优点: 不像复制那样需要额外的一大块区域 有没有内存碎片
  • 缺点: 搬运效率相对较低, 也不适合频繁操作
分代回收(重点)
  • 按照对象的年龄(该使用对象活过GC的几轮次的记录) 把堆区内存分为新生代(伊甸区 生存区1 生存区2) 和 老年代
    在这里插入图片描述
  • 这就有了对象的一生:
  1. 对象诞生于新生代的伊甸区, 新对象的内存就是新生代中的内存
  2. 第一轮GC扫描伊甸区之后,会把大量的对象干掉(绝大多数的对象都是在这时候死的) 一些少数没死的对象 就会被拷贝到生存区(优先拷贝到生存区1中 使用的是标记复制算法)
  3. 进入生存区的对象 , 也会被GC进行扫描 (可达性分析) 如果发现他还不是垃圾就会被拷贝到生存区2中 反之不可达的话 就会被认为是垃圾然后回收他

为啥搞俩个生存区 就是为了更好的施展复制算法 由于生存区的对象存活概率也很低 施展复制算法的成本不是很高

  1. 对象在生存区经历若干次GC的的扫描之后 还没有被回收 是就说明这个对象存活时间会比较久 就会被放到老年代中
  2. 老年代中的对象也会进行GC的扫描 但是由于老年代中的对象存活时间较久 所以扫描的时间间隔会比在新生代中长很多

大对象直接进入老年代

所谓的大对象是指,需要大量连续空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(上述代码中的byte[] 数组就是典型的大对象)。大对象对虚拟机的内存分配是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来放置大对象。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的在于避免伊甸区区以及两个生存区区之间发生大量的内存复制(新生代采用复制算法收集内存)

猜你喜欢

转载自blog.csdn.net/Shangxingya/article/details/107763103
今日推荐