java相比于C++来说,两个显著的特征就是动态内存分配和垃圾回收
博客小白,文章为原创,里面一些个人的观点可能不太准确或严谨,若有问题欢迎各路大神批评指正
本文从以下几点总结GC知识点:
1. 对象的生死
2. 在哪里回收什么
3. 何时回收
4. 怎么回收
首先还是上图,内存模型:
一. 对象的生死
学习java不可避免的是每天都要和对象打交道, 一个对象的创建由我们自己控制,但是对象的死亡绝大多数时候不用我们控制
我们知道新实例化一个对象一般都会同时创建一个引用指向它,引用对象存放在虚拟机栈中,对象在堆中
其中引用也分为四种:
1. 强引用,在该引用存在期间对象永远不会被回收,我们最常用的new出来的对象就属于强引用
2. 软引用,有用但非必须,如果系统资源紧张可能就会回收掉
3. 弱引用,非必须的对象,下一次垃圾回收一定会被回收掉
4. 虚引用,引用很弱,无法用它来操作对象
我们绝大部分时候的引用都是强引用,所以此处关于四种引用就不一一展开了;
那么有个问题就很重要了,收集器是如何判断对象是否已经“死了”,而可以被其回收呢?
在java的发展历史上主要有这两种方法:
1. 引用计数法
每个对象内都会有一个引用计数器,有一个地方引用它,计数器+1,引用失效,计数器-1;
当计数器为0时表示对象已死亡
有一个很严重的问题:如果两个对象是相互引用怎么办,这时如果两个对象都没有其他引用,
这两个对象其实是已经死亡的,
而计数器此时的值都为1,gc无法回收,所以目前的jdk不会用这个计数法
2. 可达性分析算法
找寻特定点(GC Roots)作为根节点(一般来说不止存在一个根节点的),从这些节点的引用关系向下搜索
会组成一条链子一样的东西,这个东西我们称为引用链。如果某一个对象不存在任何一条引用链上就表示这个
对象已经死亡了
关于对象的生死还有一个关键的方法,就是Object类中的finalize方法
为了避免朋友们这个见得少读不出来此处附上音标。。。[ˈfaɪnəlaɪz]
在GC的可达性分析时,如果发现一个对象不再引用链中,不是直接把对象视作可回收对象,
而是先看看对象有没有覆盖finalize方法;
一般覆盖此方法里面实现的逻辑都是重新把对象放到引用链中,只有当gc以为对象已经死亡时,
才会调用该方法 可以说覆盖finalize方法是对象‘最后的救赎’;
finalize方法只会被调用一次,gc再一次在引用链中找不到它就不会再调用了
而且需要注意的是,gc只保证会执行finalize方法,但是并不会保证执行完成
一般日常开发中finalize方法能不用尽量不用,不然很容易脱离掌控
二. 在哪里回收什么
GC回收什么?自然是回收对象;
至于在哪里回收,请看文章开始的内存模型图,线程隔离区,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,所以java GC主要是针对堆和方法区
三. 何时回收
GC可以分为minor GC和full GC,我习惯称之为小回收和大回收
1. 调用System.gc()时会触发full gc
关于这个方法有一点很容易误解,我们从源码的注释可看出:
/**
* Runs the garbage collector.
* Calling this method suggests that the Java virtual machine expend
* effort toward recycling unused objects in order to make the memory
* they currently occupy available for quick reuse. When control
* returns from the method call, the virtual machine has made
* its best effort to recycle all discarded objects.
* ...
*/
public native void gc();
gc()方法是一个native方法,不是调用了就一定会回收,而是建议虚拟机在此处进行一次垃圾回收
2. eden区满了触发minor gc
关于eden区和survivor区在上一篇博文中已经介绍过,这里就不多赘述了
小小的引申一点,以sun公司的hotspot虚拟机为例,eden区和两个survivor区的内存默认大小比例为8:1:1,
至于为什么是这样的一个比例是因为,我们绝大部分新建的对象都是‘朝生夕死’,也就是遇到第一次回收就被回收掉了,ibm做了一个研究,一个正常项目运行过程,超过90%的对象活不过第一次gc,所以根据很多的实验及经验才确定了这么一个比较合理且均衡的内存比例;
3. 老年代或方法区空间不足触发full gc
关于老年代空间不足一般有几种情况:
- 对象活过15次gc会放入老年代,对象要求连续空间太大也会直接放入老年代,当老年代在这种正常的增加对象时空间不足了自然会触发full gc;
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
四. 如何回收
前面介绍了哪些对象需要回收,这里就详细介绍下虚拟机是如何回收这些目标对象的
GC常用的有四种算法
1. 标记-清除
该方法比较基础,给每个对象一个标记,记录其活着还是死亡。标记阶段对每个对象更新标记,清理阶段直接把标记已死亡的对象清理掉;
优点:标记阶段只需要找到对象一个引用就说明是活着的,效率高,不需要移动对象位置。
缺点:由于不移动对象位置,会产生打断的碎片空间,也就是不连续的空间,而java新建对象较经常的会用到连续空间:
如数组,ArrayList等,会造成较空间浪费,也正是因为这一点,标记-清楚算法较少被采用
2. 标记-整理
该方法是在标记清除算法上的升级,标记阶段一样,但是在清理阶段,会把所有存活的对象放置到另一块区域,再对原区域进行全部清理
优点:不会产生不连续空间
缺点:如果存活的对象较多,gc效率会比较低
3. 复制
把内存平均分为AB两部分,每次只使用其中一部分,当A满了,把存活的对象移动到B, 对A进行全部清理,以此往复;
优点:不会产生连续空间,实现也比较简单
缺点:每次只能利用到一半的空间,造成资源浪费
4. 分代收集算法
我们一般所说的eden和survivor分区就是分代收集算法,hotspot虚拟机采用的就是分代收集算法;
五. 垃圾收集器
说到垃圾收集器首先要知道一个概念,STW即stop the world,也就是GC停顿,
GC停顿指在垃圾回收的过程中,要停止所有的用户线程。为了防止GC过程中对象的引用还在发生变化。换句话说就是在你打扫房间的时候,不允许别人继续扔垃圾了。实际上的stw时间非常短,用户基本不会感受到;
收集器如上图所示,三种新生代收集器,三种老生代收集器,
知识点拓展:
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
1、Serial收集器:
新生代收集器,采用复制算法进行垃圾收集。最古老的收集器,Serial表示串行的意思,也就是说该收集器是一个单线程的收集器。它在进行垃圾收集是必须暂停其他所有的用户线程,GC停顿感很强。但是单线程收集效率很高,它不用考虑线程交互,专心收集垃圾。
2、ParNew:
新生代收集器,采用复制算法进行垃圾收集。该收集器可以多线程进行收集垃圾,相当于Serial收集器的多线程版本。
3、Parallel Scavenge收集器:
新生代收集器,采用复制算法进行垃圾收集。它也是可以进行多线程收集垃圾,但是它多了一个独特的能力,引入了一个吞吐量(吞吐量=用户代码执行时间/(用户代码执行时间+GC所用时间))的概念,被称为吞吐量优先收集器。它可以自动调节内存分配。
4、Serial Old收集器
老年代收集器,采用标记整理算法进行垃圾收集。和Serial收集器一样是一个单线程收集器。
5、Parallel Old收集器
老年代收集器,采用标记整理算法进行垃圾收集。是一个多线程垃圾收集器。
6、CMS收集器(Concurrent Mark Sweep):
老年代收集器,采用标记清除算法进行垃圾收集。在前面提到过,它的GC停顿时间非常短,它主要有4个步骤进行垃圾收集:①初始标记;②并发标记;③重新标记;④并发清除。这里我再描述一下每个状态的具体情况。在初始标记的时候会产生GC停顿,它是单线程标记。在并发标记的过程中不会产生GC停顿,它可以与用户操作线程进行并发,并不影响用户操作,它主要是寻找引用链(在谈论生死的博客中有提到)的根节点。在重新标记的时候会产生GC停顿,它可以并行操作,主要是修正前面的标记过程中又变化的对象引用。再并发清理阶段,可以与用户操作线程并发处理,不产生GC停顿。仔细看我上面的描述,你会发现有并发和并行两种概念,切不可混为一谈。并发是GC线程与用户线程同时操作,而并行的意思是各个GC线程可以同时操作。并发不存在GC停顿,而并行存在GC停顿。
7、G1收集器
据说是迄今为止最牛逼的收集器。它的GC停顿时间也非常短,主要有4个步骤:①初始标记;②并发标记;③最终标记;④筛选回收。在初始标记中会产生GC停顿,单线程进行标记。在并发标记中不会产生GC停顿,与用户线程并发操作。在最终标记中,GC线程并行处理,会产生GC停顿。修正前面标记过程中导致对象应用又变化的部分。在筛选回收会产生GC停顿,并行处理。
参考博客:
https://blog.csdn.net/u012403290/article/details/66971189