文章目录
目录
1. 自动垃圾回收
在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现 内存泄漏。我们称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收。
Java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。通过垃 圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。其他 很多现代语言比如C#、Python、Go都拥有自己的垃圾回收器。
自动根据对象是否使用由虚拟机来回收对象
优点:降低程序员实现难度、降低对象回收bug的可能性
缺点:程序员无法控制内存回收的及时性
之所以要去了解垃圾收集和内存分配, 是因为当需要排查各种内存溢出,内存泄漏等问题时,当垃圾收集成为系统达到高可用瓶颈时,我们就需要对这些"自动化" 的技术实施必要的监控和调节。
1.1 垃圾回收区域
运行时数据区域中的程序计数器,本地方法栈,虚拟机栈随着线程的销毁而被直接回收,不需要进行控制,垃圾回收本质上就是对方法区和堆内存进行垃圾回收。
2. 方法区回收
方法区的垃圾回收主要回收两部分:废弃的常量和不再使用的类型。
判断一个常量是否废弃是相对来说比较简单的,而要判断一个类型是否属于 “不再使用类型” 就比较复杂了。 需要满足下面三个条件。
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
- 加载该类的类加载器已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用
3. 堆回收
3.1 对象已死?
在堆中存放着Java世界中几乎所有的对象实例, 垃圾回收器在对堆进行回收前,第一件事情就是要确定哪些对象还 “存活” , 那些对象 “死去”。
3.1.1 引用计数算法
引用计数是指:通过在对象中添加一个引用计数器, 每当有一个地方引用它时,计数器加1,当引用失效时,计数器减1。
这个算法有一个严重的弊端:存在循环引用问题。
public class TestReferenceCounting {
public static TestReferenceCounting instance;
public static void main(String[] args) {
TestReferenceCounting obj1 = new TestReferenceCounting();
TestReferenceCounting obj2 = new TestReferenceCounting();
obj1.instance = obj2;
obj2.instance = obj1;
}
}
因此Java虚拟机没有采用引用计数算法来判断对象的存活情况。
3.1.2 可达性分析算法
当前市面上大部分语言的内存管理子系统,都是通过可达性分析算法来判断对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots” 的根节点作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程中走过的路程称之为 “引用链” ,如果某个对象没在这个引用链上,那么就说这个对象不可达。
在Java技术体系中,固定可以作为GC Roots的对象如下:
- 线程Thread对象。
- 系统类加载器加载的java.lang.Class对象,引用类中的静态变量。
- 监视器对象,用来保存同步锁synchronized关键字持有的对象。
- 本地方法调用时使用的全局对象。
3.1.3 再谈引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达引用链,这些都和引用离不开关系。
在JDK1.2 版本之前,Java里面的对象只有引用和被引用两种状态,如果我们想要想要描述一类对象:在内存充足的情况下留在内存中,内存不足的情况被回收掉,显然是无法实现的。
在JDK1.2版本之后, Java对引用概念进行了扩充,将引用分为强引用(Strongly Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)。
强引用
最传统的 “引用”。
软引用
软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。
在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。
使用场景 - 缓存
弱引用
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。 在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。 弱引用对象本身也可以使用引用队列进行回收。
虚引用
虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回 收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道 直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
3.2 垃圾收集算法
从如何判断对象是否消亡的角度出发, 垃圾回收算法可以分为 “引用计数垃圾收集” 和“追踪式垃圾收集” 两大类。 这两类也被称为 “直接垃圾回收” 和 “间接垃圾回收” 。由于Java虚拟机并没有使用引用计数算法来判断对象是否存活, 那我们便将关注点转到追踪式垃圾收集上。
3.2.1 分代收集理论
分代收集名为理论,实际上是一套符合大多数程序运行实际情况的经验法则, 它建立在两个分代假说之上。
1) 弱分代假说:绝大多数对象都是朝生夕死的。
2)强分代假说:熬过越多次垃圾回收的对象越不容易消亡。
根据这一理论,收集器应该将Java堆划分出不同的区域,然后按照年龄将对象分配到不同的区域中
进行存储。Java也。
Java堆通常被划分为是这么做的以下几个区域:
年轻代(Young Generation):
- 这是新创建对象的主要存储区域。年轻代又可以细分为三个部分:
- Eden区:新对象首先在Eden区分配内存。
- Survivor区:Eden区的垃圾收集后,存活的对象会被移动到Survivor区。Survivor区又分为两个部分,通常称为S0和S1,交替使用。
老年代(Old Generation):
- 当对象在年轻代中经历了多次垃圾收集仍然存活时,它们会被移动到老年代。老年代的垃圾收集频率较低,通常采用标记-清除或标记-整理算法。
永久代(Permanent Generation)(在Java 8及之前版本):
- 存放类的元数据、常量池等信息。永久代的大小是固定的,可能会导致内存溢出。在Java 8及之后的版本中,永久代被元空间(Metaspace)取代,元空间使用本地内存而不是堆内存。
3.2.2 垃圾回收算法的评价标准
1.吞吐量:吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 / (执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高。
2.最大暂停时间:最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。比如如下的图中,黄色部分的STW就是最 大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时 受到的影响就越短。
3.堆使用效率: 不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算 法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
3.2.3 标记-清除算法
标记清除算法的核心思想分为两个阶段:
1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出 所有存活对象。
2.清除阶段,从内存中删除没有被标记也就是非存活对象。
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:
1.碎片化问题 由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一 个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
2.分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才 能获得合适的内存空间。
3.2.4 标记-复制算法
复制算法的核心思想是: 1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。 2.在垃圾回收GC阶段,将From中存活对象复制到To空间。 3.将两块空间的From和To名字互换。
优缺点
3.2.5 标记-整理算法
标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。 核心思想分为两个阶段: 1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出 所有存活对象。 2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
优缺点
3.2.6 分代垃圾回收
现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收 算法(Generational GC)。 分代垃圾回收将整个内存区域划分为年轻代和老年代。
IBM公司曾有一项专门研究对新生代 "朝生夕灭" 的特点做了更量化的诠释。新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1:1的比例来划分新生代的内存空间。分代垃圾回收就是这么做的。把新生代划分为一块较大的Eden空间和两块较小的Surivivor空间, 每次分配内存只使用Eden和其中一块Surivivor。 发生垃圾收集时,将Eden和Surivivor中仍然存活的对象一次性复制到另一块Surivivor空间上,然后直接清理掉Eden和已经使用过的那块Surivivor空间。
如果Surivivor空间不足以容纳一次Minor GC(新生代收集),就需要依赖其他内存区域(大多数是老年代)进行分配担保。
3.3 垃圾回收器
如果说收集算法是内存回收的方法论, 那垃圾收集就是内存回收的实践者。《Java虚拟机规范》中对垃圾收集器应该怎样实现没有做出任何规定,因此不同的厂商,不同版本的虚拟机所包含的垃圾回收器可能会有很大差异。
HotSpot虚拟机的垃圾回收器图示。
3.3.1 Serial收集器
Serial是最基础,历史最久远的垃圾收集器,在JDK1.3之前是HotSpot虚拟机新生代收集器的唯一选择。
Serial是是一种单线程串行回收年轻 代的垃圾回收器。它的单线程的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是在强调它在进行垃圾回收时,必须暂停所有其他的工作线程,直到它收集结束。
3.3.2 Serial Old收集器
Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。
3.3.3 ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾回收之外,其余的行为包括Serial收集器可用的控制参数,收集算法,Stop The World, 对象分配规则,回收策略等都与Serial收集器完全一致。
3.3.4 CMS收集器
在JDK5时,HotSpot推出了一款在强交互应用中几乎可以称之为划时代的垃圾收集器 - CMS收集器。
这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程和用户线程(基本上)同时工作。
CMS执行步骤:
1.初始标记,用极短的时间标记出GC Roots能直接关联到的对象。
2.并发标记, 标记所有的对象,用户线程不需要暂停。
3.重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
4.并发清理,清理死亡的对象,用户线程不需要暂停。
3.3.5 Parallel Scavenge收集器
Parallel Scavenge是JDK8默认的年轻代垃圾回收器, 多线程并行回收,关注的是系统的吞吐量。具备自动 调整堆内存大小的特点。
3.3.6 Parallel Old收集器
Parallel Old是为Parallel Scavenge收集器 设计的老年代版本,利用多线程并发收集。
3.3.7 G1垃圾回收器
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。
Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间 ,但是会减少年轻代可用空间的大小。
CMS关注暂停时间,但是吞吐量方面会下降。
而G1设计目标就是将上述两种垃圾回收器的优点融合:
1.支持巨大的堆空间回收,并有较高的吞吐量。
2.支持多CPU并行垃圾回收。
3.允许用户设置最大暂停时间。
内存结构
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、 Old区。Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定(其 中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
回收方式
G1垃圾回收有两种方式: 1、年轻代回收(Young GC) 2、混合回收(Mixed GC)。
年轻代回收(Young GC)
回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数 -XX:MaxGCPauseMillis=n(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地 保证暂停时间。
执行流程:
1、新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行 Young GC。
2、标记出Eden和Survivor区域中的存活对象,
3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的 参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。 比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。
4、后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
5、当某个存活对象的年龄到达阈值(默认15),将被放入老年代。
6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是 4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。
混合回收
7、多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时 (-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和 部分老年代的对象以及大对象区。采用复制算法来完成。
混合回收分为:初始标记(initial mark)、并发标记(concurrent mark)、最终标记(remark或者Finalize Marking)、并发清理(cleanup)
G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbage first)名称的由来。
最后清理阶段使用复制算法,不会产生内存碎片。
FULL GC
如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法, 此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。
总结
以上就是这篇博客的主要内容了,大家多多理解,下一篇博客见!