前言
对于Java来说,垃圾回收大部分人都把这个技术作为其伴生的产物,但是实际上GC技术的历史远比Java久远。并且对于我们的程序来说,垃圾回收机制的存在是十分有必要的,在通常情况下,垃圾收集对性能的影响一般有以下几个一般有以下几点:
- 内存泄露
- 程序暂停
- 程序吞吐量下降
- 响应时间慢
垃圾收集的一些基本概念
- Concurrent Collector: 收集的同时可运行其他的工作进程。
- Parallel Collector: 使用多CPU进行垃圾收集。
- Stop-the-word(STW): 收集时必须暂停其他所有的工作进程。
- Sticky-reference-count: 对于使用“引用计数”(reference count)算法的GC,如果对象的计数器溢出,则起不到标记某个对象是垃圾的作用了,这种错误称为sticky-reference-count problem,通常可以增加计数器的bit数来减少出现这个问题的几率,但是那样会占用更多空间。一般如果GC算法能迅速清理完对象,也不容易出现这个问题。
- Mutator:mutate的中文是变异,在GC中即是指一种JVM程序,专门更新对象的状态的,也就是让对象“变异”成为另一种类型,比如变为垃圾。
- On-the-fly:用来描述某个GC的类型:on-the-fly reference count garbage collector,此GC不用标记而是通过引用计数来识别垃圾。
- Generational gc:这是一种相对于传统的“标记-清理”技术来说,比较先进的gc,特点是把对象分成不同的generation,即分成几代人,有年轻的,有年老的。这类gc主要是利用计算机程序的一个特点,即“越年轻的对象越容易死亡”,也就是存活的越久的对象越有机会存活下去。
吞吐量与响应时间
牵扯到垃圾收集,还需要搞清楚吞吐量与响应时间的含义
- 吞吐量是对单位时间内完成的工作量的量度。如:每分钟的 Web 服务器请求数量。
- 响应时间是提交请求和返回该请求的响应之间使用的时间。如:访问Web页面花费的时间。
吞吐量与访问时间的关系很复杂,有时可能以响应时间为代价而得到较高的吞吐量,而有时候又要以吞吐量为代价得到较好的响应时间。而在其他情况下,一个单独的更改可能对两者都有提高。通常,平均响应时间越短,系统吞吐量越大;平均响应时间越长,系统吞吐量越小; 但是,系统吞吐量越大,未必平均响应时间越短;因为在某些情况(例如,不增加任何硬件配置)吞吐量的增大,有时会把平均响应时间作为牺牲,来换取一段时间处理更多的请求。
针对于Java的垃圾回收来说,不同的垃圾回收器会不同程度地影响这两个指标。例如:并行的垃圾收集器,其保证的是吞吐量,会在一定程度上牺牲响应时间。而并发的收集器,则主要保证的是请求的响应时间。
GC的回收流程
- 找出堆中活着的对象
- 释放死对象占用的资源
- 定期调整活对象的位置
GC算法
- Mark-Sweep 标记-清除
- Mark-Sweep-Compact 标记-整理
- Copying Collector 复制算法
- Mark-标记从”GC roots”开始扫描(这里的roots包括线程栈、静态常量等),给能够沿着roots到达的对象标记为”live”,最终所有能够到达的对象都被标记为”live”,而无法到达的对象则为”dead”。效率和存活对象的数量是线性相关的。
- Sweep-清除扫描堆,定位到所有”dead”对象,并清理掉。效率和堆的大小是线性相关的。
- Compact-压缩对于对象的清除,会产生一些内存碎片,这时候就需要对这些内存进行压缩、整理。包括:relocate(将存货的对象移动到一起,从而释放出连续的可用内存)、remap(收集所有的对象引用指向新的对象地址)。效率和存活对象的数量是线性相关的。
- Copy-复制将内存分为”from”和”to”两个区域,垃圾回收时,将from区域的存活对象整体复制到to区域中。效率和存活对象的数量是线性相关的。
其中,Copy对比Mark-sweep
- 内存消耗:copy需要两倍的最大live set内存;mark-sweep则只需要一倍。
- 效率上:copy与live set成线性相关,效率高;mark-sweep则与堆大小线性相关,效率较低。
分代收集
分代收集是目前比较先进的垃圾回收方案。有以下几个相关理论:
- 分代假设:大部分对象的寿命很短,“朝生夕死”,重点放在对年青代对象的收集,而且年青代通常只占整个空间的一小部分。
- 把年青代里活的很长的对象移动到老年代。
- 只有当老年代满了才去收集。
- 收集效率明显比不分代高。
HotSpot虚拟机的分代收集,分为一个Eden区、两个Survivor去以及Old Generation/Tenured区,其中Eden以及Survivor共同组成New Generatiton/Young space。通常将对New Generation进行的回收称为Minor GC;对Old Generation进行的回收称为Major GC,但由于Major GC除并发GC外均需对整个堆以及Permanent Generation进行扫描和回收,因此又称为Full GC。
分代收集中典型的垃圾收集算法组合描述如下:
- 年青代通常使用Copy算法收集,会stop the world
- 老年代收集一般采用Mark-sweep-compact, 有可能会stop the world,也可以是concurrent或者部分concurrent。
那么何时进行Minor GC、何时进行Major GC? 一般的过程如下:
- 对象在Eden Space完成内存分配
- 当Eden Space满了,再创建对象,会因为申请不到空间,触发Minor GC,进行New(Eden + S0 或 Eden S1) Generation进行垃圾回收
- Minor GC时,Eden Space不能被回收的对象被放入到空的Survivor(S0或S1,Eden肯定会被清空),另一个Survivor里不能被GC回收的对象也会被放入这个Survivor,始终保证一个Survivor是空的
- 在Step3时,如果发现Survivor区满了,则这些对象被copy到old区,或者Survivor并没有满,但是有些对象已经足够Old,也被放入Old Space。
- 当Old Space被放满之后,进行Full GC
但这个具体还要看JVM是采用的哪种GC方案。总而言之,内存回收和垃圾收集是比较复杂的原理,很多时候是影响系统性能、并发能力的主要的因素之一,深入理解其运作,对编写的程序有一点影响力。