第四篇:JVM中堆结构、垃圾回收GC、垃圾回收器

JVM中的堆区主要存储的是实例化对象,随着对象的创建以及使用完毕,堆区的内存是经常波动的,所以堆区是垃圾回收GC的主要地方。

1、对象死了吗?

1.1、引用计数算法
对象中存在一个引用数变量,当一个对象被引用时,引用数+1,当引用数为0的时候,说明该对象没用了,就可以回收了。
引用计数算法存在一个缺陷:
互相引用,当两个对象互相引用的时候,两者的引用数都不会等于0,两者都需要等待对方被回收后,引用数才会变为0。二者相互等待。两个对象永远不会回收。

1.2、根搜索算法:

根搜索算法是JVM采用的算法。
在这里插入图片描述
如上图所示,从GC Root开始遍历图,遍历到的对象都是存活对象,没有遍历到的就是可回收的对象。遍历到的对象会被标记,如果对象没被标记的话,就说明可以被回收!

GCRoots是什么呢?
1、虚拟机栈中的局部变量表中的引用的对象
2、JNI本地方法栈中的局部变量表中的引用的对象。
3、类中的静态引用对象(static)
4、类中的常量引用对象(final)

强引用、软引用、弱引用、虚引用

强引用:
程序中普遍使用的类型,通过new或者getInstance()等创建的。
User u = new User();

一个对象中只要被强引用引用,哪怕只有一个,这个对象就属于强可达。

强可达对象永远不会被回收,即使发生OOM也不会回收

软引用:
通过SoftReference userSoftReference = new SoftReference(u)来创建,u是一个强引用类型,通过强引用类型来创建软引用,通过userSoftReference.get()来获取引用的对象。

一个对象中不存在强引用,存在弱引用,这个对象属于软可达。

当内存紧张的时候,软可达对象进行回收。将软引用对象加入到ReferenceQueue中,对队列中的软引用对象进行回收!

软引用适用于缓存对象,在内存紧张的时候对其进行回收,防止OOM,同时不影响系统的运行

弱引用:
通过WeakReference userWeakReference = new WeakReference(u)来创建,u是一个强引用类型,通过userWeakReference.get()来获取引用的对象。

一个对象中不存在强引用和软引用,存在弱引用,这个对象属于弱可达。

当进行垃圾回收的时候,即使内存很充足也会对弱可达对象进行回收。

弱引用也适用于缓存对象

虚引用:
通过PhantomReference userPhantomReference = new PhantomReference(u,ReferenceQueue)来创建,虚引用几乎不能获取其引用的对象,也就是通过userPhantomReference.get()获取的对象为null。
一个对象中只存在虚引用,这个对象属于虚可达。

虚引用监督对象回收
1、虚引用创建的时候必须指定一个queue
2、自定义类继承PhantomReference,在自定义类中自定义变量来保存对应的User信息,对PhantomReference对象进行标记。PhantomReference本身也是一个对象啊,只不过PhantomReference对象中包含了User对象的引用。
3、当虚引用对应的对象被回收的时候,会将PhantomReference对象加入到queue中,我们可以开一个线程来监视读取queue来查看哪些对象被回收了,进行业务处理。

4、软引用和弱引用也可以这样做来监督对象回收,做法是一样的。

下面是具体的例子:


public class ReferenceTest {

    public static void main(String[] args) throws InterruptedException {
        People people = new People();
        ReferenceQueue queue = new ReferenceQueue();
        PeopleReference reference = new PeopleReference(people,queue);
        people = null;


        new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        PeopleReference people1 = null;
                        if((people1 = (PeopleReference)queue.remove()) != null){
                            //用来处理业务逻辑
                            System.out.println("对象回收了:"+people1.name);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        System.gc();
    }
}

class People{
    public String name = "jaychou";
}

class PeopleReference extends PhantomReference<People>{

    String name;
    public PeopleReference(People referent, ReferenceQueue<? super People> q) {
        super(referent, q);
        this.name = referent.name;
    }
}

具体的原理可以参照这篇文章Java Reference核心原理分析

不可达:
当一个对象对应的所有引用都设置为null时,对象就变成了不可达。
不可达对象会被回收。

2、常用的GC算法

2.1、标记-清除算法
根据1.2的根搜索算法,JVM对存活对象做了标记,所以在内存回收的时候将没有标记的对象清除即可。

优点:方便,不需要移动对象
缺点:会存在内存碎片

2.2、复制算法
在这里插入图片描述

将内存一分为二,其中一半内存存放对象,而另一半不存储数据。当触发GC时,将内存1中的对象依次复制到内存2中,清空内存1,下一轮GC中将内存2转存到内存1中。

优点:不存在内存碎片
缺点:需要额外复制对象,会有开销、并且会移动对象,还需要额外更改对应的指针。

当存活的对象少时,复制算法就比较优秀。

2.3、标记-压缩算法
标记-压缩算法是标记-清除的改版,就是在清除对象后,会对内存空间进行碎片整理,将对象按低地址依次排列。
缺点:需要移动对象,更改指针。

2、堆区结构

在这里插入图片描述
堆分为新生代和老年代:
1、新生代:
从名字就可以看出来,新生代存放的是新生成的对象。新生代的GC称为MinorGC。
新生代又分为:
a、Eden
新创建的对象存放在Eden区
b、Survivor From
存放上一次MinorGC中幸存对象
c、Survivor To
空的内存区域

Eden、Survivor From、Survivor To 的空间大小比为8:1:1.
为什么要设立Eden?
如果只有Survivor From 和Survivor To的话,会造成空间的浪费。

MinorGC算法过程:
MinorGC采用的是复制算法,因为新生代中的对象存活率不高。
1、对Eden区和Survivor From区进行扫描,将其中存活的对象复制到Survivor To区,在复制的过程中,将对象的年龄+1,当对象的年龄大于15的时候,将对象转移到老年代中。
2、这时Eden和Survivor From区就清空了,并且调换Survivor From 和 Survivor To区,所以存活的对象都转移到了Survivor From区,也就是上一轮中的Survivor To区。

2、老年代:
老年代对应的GC叫做Major GC。老年代存放的是存活时间比较长的对象。对象比较稳定,所以Major GC不会太频繁。

Major GC的过程
Major GC采用的是标记-压缩算法
1、首先利用根搜索算法,将存活的对象进行标记
2、对没有标记的对象进行回收
3、整理内存碎片

Full GC 是指同时执行minorGC 和 major GC。

为什么新生代使用复制算法、老年代使用标记-整理算法:
因为新生代的大多数对象的生命周期都很短,如果采用标记-整理算法的话,需要执行大量的清理操作,效率低。
老年代的对象都很稳定,如果采用复制算法的话,需要大量的复制对象,效率低,标记-清除的效率更高!

3、GC时机

1、Eden区空间不足以存放新对象的时候,触发Minor GC
2、老年代空间无法存储从新生代升到老年代的对象的时候,触发Major GC
3、当调用system.gc()的时候,会触发FullGC

4、垃圾回收器

JVM中提供了多个垃圾回收器,各个回收器都有其适用的场景,没有哪一个垃圾回收器是完美的。我们需要根据具体的业务场景来选择合适的垃圾回收器。

Stop-world指的是在GC过程中,造成的程序线程暂停的现象,此时系统被停顿,也被简称为STW,垃圾回收器的目标就是减少STW和提高系统的吞吐量

为什么需要停止主程序呢?
如果标记和清除和主线程一块执行的话,主线程会不断的产生垃圾,导致GC的效果差。不会产生错标的情况,因为一个对象没有了引用,是无法被再次引用的。

1、Serial收集器
**Serial收集器对新生代和老年代都可以回收。**Serial收集器是JVM最古老和最基本的收集器了。Serial就是串行化的意思。也就是说Serial收集器是单线程的。并且当单线程回收垃圾的时候,其他工作线程都需要暂停,这会造成STW,造成停顿。

新生代垃圾回收算法:复制算法
老年代垃圾回收算法:标记-清除算法 Serial Old收集器

好处:简单高效,适用于单核处理器。因为如果多线程的话,会有额外的切换开销。
缺点:会造成"stop-world",也就是其他工作线程会暂停,导致程序的不可用,是独占式的。

2、Parnew
Parnew垃圾回收器其实就是Serial回收器的多线程版本。只针对新生代GC,GC算法为复制算法。通常配合老年代CMS垃圾回收器使用

为什么会出现Parnew呢?
因为随着计算机的发展,计算机的处理器有了多核的版本,如果在继续使用单线程的话,比如8核的话,其他7个核就在空闲,使用不上了。所以才出现了多线程版本来适应多核版本。

Parnew回收器在回收垃圾的时候,其他工作线程仍然是不可用的,也会造成STW,仍然是独占式的,造成停顿。

但是如果是单核CPU或者并行能力不行的系统中,Parnew回收器的效果不如Serial回收器。

3、Parallel
**Parallel对新生代和老年代都可以回收。**Parallel收集器也是多线程的收集器。与Parnew不同的是,Parallel设置了最大垃圾清理时间,垃圾清理时间不能超过规定时间。Parallel收集器仍然是独占式的,清理垃圾的时候会将工作线程暂停STW,造成停顿。

新生代回收算法:复制算法 Parallel Scavenge
老年代回收算法:标记-清除算法 Parallel Old

4、CMS
CMS的全称是Concurrent Mark Sweep,中文叫做并发标记清除
CMS是一个老年代垃圾收集器。可以与Serial和Parnew垃圾回收器搭配使用。常常跟Parnew收集器搭配使用。采用的GC算法是标记-清除算法。
在这里插入图片描述

CMS的卡表技术
JVM将堆分为了新生代和老年代,新生代和老年代的GC是分开的,新生代的GC很频繁,并且采用可达性分析来标记存活对象。如果只遍历存在新生代的GCRoots会存在这种情况,会将一些新生代对象误标,因为有可能老年代的对象引用了该新生代对象。

所以想要准确的标记新生代的对象,需要扫描整个老年代,这样的效率很低下,所以引入了卡表。

JVM将老年代分为了若干个卡页,每个卡页的大小为2的幂次方,然后用卡表来记录对应的卡页的状态,如果一个老年代中的对象引用了新生代中的对象,就将老年代对象对应的卡页对应的卡表项标记为dirty,当新生代进行GC的时候,不需要扫描整个老年代,而是只需要扫描标记为dirty的卡页就可以了。

卡表采用下标的方式来管理,通过对象的地址除以卡页的大小就得到对应的卡表下标了。

由于新生代的对象的存活时间非常短,CMS对老年代进行GC的时候,会扫描整个堆,并没有在新生代中维护卡表

4.1、初始标记:
这是CMS中两次Stop-World的第一次,造成停顿。主要标记老年代中的与GcRoots关联的对象。
在这里插入图片描述
在这里插入图片描述
CMS采用三色标记的方法,GcRoots关联的对象会被标记为灰色。

4.2、并发标记:
从初始标记中对象开始做可达性分析,并发标记还在存活的对象。在并发标记的时候,主程序是可以运行的。

在这里插入图片描述

4.3、重新标记

重新标记是stop-world的,不允许主程序同时运行,造成停顿

1、在并发标记的过程中,主线程会产生新的垃圾,可能会存在漏标的情况,CMS会记录对应的对象,然后重新将其标记为白色。
2、在并发标记的过程中,可能会有对象从新生代移动到老年代,或者有新对象直接存入老年代,CMS也会记录对应的对象

在这里插入图片描述

4.4、并发清理

将 白色的对象进行清理。此时主程序线程可以同时工作,同时也会产生垃圾,这种垃圾叫做”浮动垃圾“。

CMS会导致碎片。

CMS已经被逐渐抛弃了,取而代之的是G1收集器。

5、G1收集器

G1的全称是Gabbage First。也就是垃圾优先。G1收集器虽然也是将堆分代,分成老年代和新生代。但是和之前不同的是,堆被分为若干个Region。每一个Region可以根据需要来充当Eden、Survivor、Old,十分灵活,对不同类型的Region采用不同的收集策略,达到最好的收集效果。

一个Region对应着一小块连续的内存。Region采用的回收算法是复制算法,是没有碎片的

在这里插入图片描述

G1之所以能够称为现在主流的垃圾回收器,与其Region堆内存布局密切相关。
一个Region对应着有它的价值,与其回收所消耗的时间和回收获得的空间大小有关。
按照Region的价值对其进行排序,优先回收价值大的Region,保证了G1能够在有限的时间内获得最大的收集效果,提高了G1的收集效率。

Remember Set
因为G1是按照region进行回收的,对于新生代中的一个要回收的region,需要确定region里面的对象是否被老年代引用,G1不采用对老年代整个扫描的方式,而是采用Remember Set的方式,Remember Set是一个类似于哈希表的结构,Key是引用了该region的region的起始地址,Value是引用了该region的对象的卡页集合。这样的话,不需要对整个老年代进行扫描,而是通过remember set中记录的卡页集合来确定region里的存活对象。

新生代GC:
当eden区无法存放对象的时候,会触发新生代的GC,会对新生代进行GC roots 扫描以及 Remeber Set来确定对象是否存活,接着将s1和eden的对象拷贝到s2中。

新生代GC的过程是STW的。

随着老年代的内存占用提高,当超过老年代占用率超过阈值后,会触发以下的混合GC
5.1、初始标记
标记GCRoots的直接对象。

5.2、并行标记
对根扫描中的对象进行可达性分析,对所有可达性对象进行标记。
在并行标记的过程中,程序线程还可以运行,也会产生新的对象

Region中有有两个地址prevTAMS和nextTAMS。地址之间存放的是新增的存活对象,nextTAMS随着对象的增加而升高。

5.3、重新标记
这个会造成stop-world,程序停顿。这个标记速度很快,
对于新增的对象,当对Region进行清理的时候,会将prevTAMS和nextTAMS之间的对象进行复制,不会对其进行回收。

对于并发标记中的改变的节点,G1采用的是原始快照式的,可以保证节点不被误标记为白色。

5.4、清理准备
这个会造成Stop-world,停顿。会对所有Region垃圾信息进行统计,垃圾较多的Region会被优先清除,将其Region标记为G。

5.5、清理
会造成stop-world。
在满足回收时间的条件下,挑选回收价值最大的region进行回收,将其存活对象进行复制,然后清空对应的region。

如果在回收的过程中,找不到空闲的region来存放活对象,就会触发一次Serial GC。

G1因为采用复制算法,所以不会产生碎片。

G1中除了并行标记,其他阶段都是停顿的。

CMS追求的是并发清理,低停顿,而G1追求的是在满足用户要求的延迟的情况下,追求更高的清理效果。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值