JVM原理:JVM垃圾回收算法(通俗易懂)

简介: JVM原理:JVM垃圾回收算法(通俗易懂)

目录

前言

正文

垃圾标记算法

引用类型

强引用

软引用

弱引用

虚引用

引用计数法

循环引用问题

根可达性分析法

虚拟机栈(栈帧的局部变量表)中的引用

方法区中类静态属性引用

方法区中常量引用

本地方法栈(Native方法)引用

垃圾回收算法

标记清除算法

复制算法

复制算法和标记清除算法如何选择?

标记整理算法

分代收集算法

总结

前言

为什么程序跑久了有时会变卡,如果你看过笔者的 《JVM运行时内存模型》那么应该知道用户线程在跑时会将new出来的对象放到虚拟机堆中,当堆满了之后会发生垃圾回收,而垃圾回收过程中会STW,也就是停止用户线程运行,这种在程序层面看来就是没反应,所以如果出现这种问题,我们就要想办法让STW的时间尽量的变短;


这分为几种情况,垃圾回收时间长或者垃圾回收的频率高;

垃圾回收时间长的情况?

可能是因为GC要扫描的区域比较大有,清理时间较长,或者说我们选择的垃圾回收器不适合当下情况

GC频率过高

可能有部分对象发生内存泄漏,导致无法回收,占用堆空间,此时的堆空间变小,满了就触发GC,频率自然高。也可能是年轻代和老年代空间分配不合理,空间过小导致频繁GC;

正文

认清原理,并接受原理,才能有效地做事情,所以我们要知道有哪些垃圾回收算法、哪些垃圾回收器,了解其特性,才可以帮忙我们进行程序优化。

垃圾标记算法

程序运行时,堆中存储了一堆对象,那垃圾回收器怎么知道哪些需要回收,不会发生误回收的情况呢?当一个实例对象有被引用时可以认为它是个活跃的状态,不进行回收。所以根据这个特性,可以添加一个标记专门记录被引用的次数,当次数不为0的就不进行垃圾回收,这种方式就是引用计数法。还可以在根节点挨个遍历,遍历到的对象进行标记,那些没遍历到的则被当作垃圾处理,这里的根节点可以简单理解为main方法里面开始的都是根,这种方式就是根可达性分析法;目前使用最多的是根可达性分析法;

引用类型

强引用

强引用我们平常用得最多,强引用在内存不足时,垃圾回收器不会进行回收,即使抛出OOM,因为该实例对象被引用,证明不是垃圾数据。

如:

Person person=new Person();

软引用

软引用在内存不足时(证明老年代满了,触发FULL GC),垃圾回收器会进行回收,所以垃圾标记算法在扫描标记时,判断如果引用为软引用,则把它当垃圾处理;


如:

Person person=new Person();
SoftReference<Person> sPerson=new SoftReference<>(new Persion());

弱引用

弱引用不管内存够不够时,这里内存够触发GC则证明年轻代满了触发YGC,内存不够则证明是老年代满了触发FULL GC,只要进行垃圾回收就会被当垃圾清除;


如:

Person person=new Person();
WeakReference<Person> sPerson=new WeakReference<>(new Persion());

虚引用

虚引用在垃圾回收时被清理,一般配合队列使用,在垃圾回收时会将其加入队列,此时队列可以拿到虚引用,但是虚引用指向的强引用Person已经被回收了,调用phantomReference.get()方法时获取不到实例对象;

 ReferenceQueue<Person> QUEUE = new RegferenceQueue<>();
 Person person=new Person();
 PhantomReference phantomReference = new PhantomReference(person, QUEUE);

引用计数法

我们在引用指向对象时就给该对象引用次数加1,在指引指向别的对象或变量销毁时将其-1,在垃圾回收器进行回收时,判断对象的引用标记数是否为0,如果为0则认为它是垃圾数据,对其进行回收;

优点:算法简单,判断搞笑

缺点:无法解决循环引用问题

循环引用问题

public class MyObject {
    public Object ref = null;
    public static void main(String[] args) {
        Person person1 = new Person();
        Person person2 = new Person();
        person1.friend = person2;
        person2.friend = person1;
        person1 = null;
        person2 = null;
    }
}

5c3d1a19691d40d146a7818fe9c0bad.png

虽然person1对象和person2对象已经没有任何引用指向它们了,但是此时它们的引用标记数依旧不为0,垃圾回收器认为它们不是垃圾,导致空间无法得到释放;

根可达性分析法

根可达性法以GC ROOTS节点为起点,开始往下扫描,遍历属性引用关系来判断对象是否为可回收的垃圾对象;如果把ROOTS比喻成江河的源头,那么那些未跟源头相通的河或者内湖就可以理解为垃圾对象,三色标记法是比较常用的根可达性分析法;


那么,GC Roots 是什么呢?

虚拟机栈(栈帧的局部变量表)中的引用

public class Demo {
    public static void main(String[] args) {
        // 创建实例A
        // a1:就是虚拟机栈(栈帧的局部变量表)中引用
        A a1 = new A();
        // 当 a1 不再指向实例A的时候,实例A 将不可达
        a1 = null;
    }
}


当开启一个新线程时,会创建线程私有的虚拟机栈,栈帧中维护着局部变量表,线程执行方法中的变量会记录到局部变量表里面,所以可以把局部变量表的变量理解为GC Roots,往下遍历其引用链,在引用链上的都是可用对象;

方法区中类静态属性引用

public class Demo {
    // 创建实例A
    // a2:方法区中类静态属性引用
    public static A a2 = new A();
    public static void main(String[] args) {
        // 当 a2 不再指向实例A的时候,实例A 将不可达
        a2 = null;
    }
}

当属性被static修饰时,其变量会放到方法区中的静态变量区中,静态变量区的变量指向的引用可以看作GC ROOTS;

方法区中常量引用

public class Demo {
    // 创建实例A
    // a3:方法区中常量引用
    public static final A a3 = new A();
    public static void main(String[] args) {
        // 当 a3 不再指向实例A的时候,实例A 将不可达
        a3 = null;
    }
}


当使用final修饰后,其变量会存储到方法区中的常量池中,所以方法区中常量引用可以看作GC ROOTS;

本地方法栈(Native方法)引用

本地方法栈和虚拟机栈类型,也会维护局部变量表,当本地方法区引用了堆中的对象时,可以把其当作GC ROOTS;

垃圾回收算法

标记清除算法

标记清除法分为了标记和清除两个步骤,首先采用可达性分析法进行标记后,对被标记为垃圾的对象进行回收;

黑色为已经扫描过的,有被引用的对象,灰色代表待扫描对象,白色代表待垃圾回收对象;当垃圾回收完成后,此时的空间就变得很碎片化,如果此时需要存入一个对象,该对象占用3个格子的空间,那么第一行的两格空间就没法被利用到;所以碎片化太严重的情况下,空间利用率低,GC的频率就会变高;

复制算法

复制算法会开辟两块内存空间,当标记完成之后,将存活对象拷贝到另外一个空间中,并将当前空间数据清除。这种方式可以解决内存碎片化问题,而且效率高。但意味着我需要更多的内存空间,比如1G的空间,由于需要复制就得拆分成两块500M的空间,所以也是很耗费内容的;

复制算法和标记清除算法如何选择?

一般如果程序对象一般占用空间较小的话,可以采用标记清除算法,这样空间利用率会比复制算法高;如果对象是比较大的话,选择复制算法的空间利用率会比较高,算法没有好坏,只有适不适合;

标记整理算法

标记整理算法在进行标记之后,将存活对象移动到一块,剩下的全部清理掉。这样就有碎片化带来的问题,也不像复制算法那样浪费空间;

分代收集算法

由于在垃圾回收时,会STW暂停用户线程,导致程序没响应,如果停顿时间过长,会导致服务宕机;所以堆空间过大会导致垃圾回收时间过长,所以将堆进行分代管理,分为年轻代和老年代;

当new对象时,会尝试将数据存储到Eden区,如果Eden区满了之后会触发YGC,年轻代采用复制算法,第一次将存活对象复制到survivor0区,第二次会将survivor0和Eden区存活的对象复制到survivor1区,就这样反复在survivor0和survivor1间复制。


当年轻代的存活对象存活时间较长时,导致年轻代剩余空间变小,YGC就会变得很频繁;针对这种情况,给每个存活对象设置存活年龄,每次YGC后未被回收将年龄+1,当年龄到达阈值时(默认15),将其迁移到年轻代中;


当老年代空间满了之后会触发FULL GC,此时会对年轻代和老年代进行垃圾回收,老年代的回收算法不同的垃圾回收器采用的机制不同;

总结

垃圾回收算法有多种,每种针对的场景不一样,没法说其好坏,只有综合业务场景,设备信息等因素挑选出适合程序的算法,现市面上垃圾回收器有多种就是针对不同场景而定制的。


目录
相关文章
|
28天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
41 3
|
7天前
|
算法 容器
令牌桶算法原理及实现,图文详解
本文介绍令牌桶算法,一种常用的限流策略,通过恒定速率放入令牌,控制高并发场景下的流量,确保系统稳定运行。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
令牌桶算法原理及实现,图文详解
|
17天前
|
负载均衡 算法 应用服务中间件
5大负载均衡算法及原理,图解易懂!
本文详细介绍负载均衡的5大核心算法:轮询、加权轮询、随机、最少连接和源地址散列,帮助你深入理解分布式架构中的关键技术。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
5大负载均衡算法及原理,图解易懂!
|
23天前
|
算法 数据库 索引
HyperLogLog算法的原理是什么
【10月更文挑战第19天】HyperLogLog算法的原理是什么
37 1
|
27天前
|
算法
PID算法原理分析
【10月更文挑战第12天】PID控制方法从提出至今已有百余年历史,其由于结构简单、易于实现、鲁棒性好、可靠性高等特点,在机电、冶金、机械、化工等行业中应用广泛。
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
5天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
3天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
7 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
56 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!