垃圾收集器与内存分配
垃圾回收机制
垃圾收集就是我们熟知的GC(Carbage Collection),为了达到更高的并发量,我们需要对这些自动化技术实施监控与调节。
新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。
Gc触发时机
https://www.cnblogs.com/williamjie/p/9516367.html
对象已死
堆里几乎存有所有对象实例。垃圾回收对象前,首先需要判断对象是否还存活,这里有两种方法,引用计数算法和根搜索算法(可达性分析)。
推荐对象分配的地方的博客
https://blog.csdn.net/zhaohong_bo/article/details/89419480
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它是加一,引用失效则减一。任何时刻引用计数都为零的对象,不可能再被使用。
优点:简单高效
缺点:很难解决循环引用问题
java你要使用这种方式管理内存。
根搜索算法
根搜索算法基本思路:通过一系列名为"GC Roots”的对象为起始点,从这些节点向下搜索,经过路线就是引用链,当一个对象和GC Roots之间没有引用链,即不可达时,证明对象是不可用的。
可作为GC Roots的对象:
- 栈帧中本地变量表的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用对象
- 本地方法栈中JNI(Native方法)引用的对象
- Thread - 活着的线程
引用的分类
JDK1.2之前,引用就是reference类型的数据中储存着一块内存的起始地址,就是引用。只有是或者不是,非常狭隘。
JDK1.2之后,对引用的概念进行扩充,出现了我们熟悉的四种引用,强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom),强度依次递减。
- 强引用:普遍存在的,类似 Object obj = new Object()这类引用,只要强引用还在,垃圾回收器就不可能回收它
- 软引用:描述一些还有用,但非必须的类。系统将要内存溢出之前,把这些对象列入回收范围进行二次回收。JDK1.2后,提供 SoftReference类实现软引用。
- 弱引用:描述非必须对象。下一次GC到来时,回收。hanlder的内存泄露的解决,ThreadLoacl的实现中,都有使用。JDK 1.2后,提供了WeakReference实现弱引用
- 虚引用:最弱的一种引用,不会对一个对象的生存时间产生影响,无法通过虚引用来取得一个对象实例。采用虚引用,唯一目的就是希望对象被回收时取得一个系统通知。JDK1.2后,提供PhantomReference来实现虚引用。
对象存活判断
在可达性分析中,某个对象不可达,不是就立即死亡,而是在缓刑阶段。宣告一个对象死亡,至少要经过两次标记过程:在可达性分析中,对象不可达,会被第一次标记并进行一次筛选,筛选条件是对象是否有必要执行finalize()方法。
没有必要执行的两种情况:
(1)对象没有覆盖finalize()方法
(2)finalize()方法已经被虚拟机调用了
如果对象被判定有必要执行finalize()方法,那么它会被放置在一个 名字为 F-Queue的队列中,然后有一条虚拟机自动建立的,低优先级的Finalizer线程去执行队列里的对象的finalize() 方法,但是不承诺会等到它方法执行结束。
不承诺等到对象finalize()方法执行完的原因:
如果一个对象的finalize方法执行缓慢或者发生死循环,会导致内存回收系统崩溃。
finalize()方法是对象不被回收的最后一次机会:方法执行后,会对FQ队列里的对象进行一次标记-重新与引用链上的任何一个对象建立关联即可从“即将回收的集合”中移除。
注意:一个对象的finalize()方法只会被系统调用一次,对于下次GC,不会被调用
finalize方法被说适合用来关闭外部资源,但是使用Try-finally或者其他方式会更好,尽量避免使用它。
回收方法区
方法区,在HotSpot虚拟机里也叫永久代。方法区的回收效率远低于堆的回收效率。规范里可以不要求虚拟机实现方法区的垃圾回收。
方法区的垃圾收集主要包括 废弃常量 和 无用的类信息 两部分。回收无用常量的方式和回收堆里的对象的方法十分类似,通过引用判断。
而判断无用类,则条件更苛刻一点。
- 堆里的所有实例都被回收
- 该类的类加载器被回收
- 该类的对象没有任何地方引用(应该包括四种引用类型),无法在任何地方通过反射访问该类方法
满足这三个条件**,则可以进行回收,但不是必然**。虚拟机还会提供一些参数进行控制,或者查看类的加载和卸载信息。
垃圾回收算法
标记-清除算法
最基本的收集算法。分为两个阶段,标记和回收。首先先标记出所有将要回收的对象,标记完成后统一回收掉所有标记的对象。
主要缺点:
标记和清除两个过程效率不高。
会产生内存碎片。
复制算法
将可用内存分为大小相等的两块,每次只使用一块。当这一块内存用完了,就将活着的对象复制到另一块上面,然后把已经使用过的那块内存一次清理。
实现简单,运行高效。只是浪费空间。
这种算法被运用到回收新生代。新生代被划分为eden区和两块Survivor区。
每次使用eden区和一块survivor区,另外一块survivor区供复制使用。
eden区survivor区比例 8:1(一共占新生代的90%)。当survivor区不够用时,就会使用其他内存区域(老年代)进行分配与担保。
标记-整理算法
复制收集算法需要进行较多的复制操作,效率会变低,而且浪费50%的内存空间,而且需要有额外的分配担保区域来应对100%对象存活的情况。所以老年代一般不采用这种算法。
老年代的特点是对象存活时间相对较长,针对这种特点,有人提出一种“标记-整理算法”。先标记,所有存活对象再向一端移动,然后清理边界以外的内存。
分代收集算法
现代商业虚拟机都采用分代收集算法。根据对象存活周期不同将内存分为几块,一般是新生代和老年代。根据各年代的特点,采用最适合的收集算法。
新生代每次收集时都有大批对象死去,采用复制算法。老年代对象存活率高,采用标记-整理或者标记-清除算法。
垃圾收集器
对于内存回收,收集算法是方法论,垃圾收集器是具体实现。
java虚拟机规范中对垃圾收集器应该如何实现没有做如何规定,因此不同厂商,不同版本的虚拟机提供的垃圾收集器可能不同。一般会提供参数,让用户根据自己应用的特点和要求组合出各年代所使用的收集器。
例如Sun HotSpot虚拟机包含的所有收集器,连线表示可以搭配使用。
没有完美的收集器,选择合适的最重要。
Serial收集器
Serial收集器是最基本,历史最悠久的收集器,在JDK1.3之前,新生代收集的唯一选择。
它是单线程收集器,每次收集,都会停止其他所有线程(Stop the World),直到收集结束。
Stop the World 使得用户体验不好,但是简单高效(与其他收集器的单线程相比),对于单核环境,没有线程交互开销。
ParNew收集器
ParNew收集器是Serial收集器的多线程版本,仍然会 Stop the World,j即并行,没有做到并发。
它是许多运行在 Server模式下的虚拟机 新生代收集器的首选。
因为除了Serial ,只有它能够和CMS收集器搭配使用。
并行(Parallel):多条回收线程并行工作,用户线程停止等待。
并发(Concurrent):用户线程和回收线程同时执行(并行或者交替执行)。
Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器。它使用复制算法而且并行。
看上去和Parallel New一样。但是 Parallel Scavenge收集器的关注点与其他收集器关注点不同。CMS收集器等关注的是,垃圾收集时尽可能减少用户线程的停顿时间。Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
吞吐量:CPU用于运行用户代码的时间与CPU总耗时的比值。
那么停顿时间与吞吐量怎么选择呢?
停顿时间越短越适合需要与用户交互的程序,以良好的响应速度提升用户体验。
高吞吐量则可以最高效率利用CPU,尽快完成程序运算速度,适合后台运算,不需要太多交互的任务。
Parallel Scavenge收集器提供 -XX:MaxGCPauseMillis参数控制停顿时间,
-XX:GCTimeRatio参数控制吞吐量大小。其他参数可以查阅资料。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,用来回收老年代的。单线程,使用 标记-整理算法。
它被Clinet模式下的虚拟机使用。
在Server模式下,在JDK1.4及之前,与Parallel Scavenge收集器搭配使用。
以及作为CMS收集器的预备方案,在并发收集遇到 Concurrent Mode Failure时使用。
Parallel Old收集器
Parallel Scavenge收集器的老年代的版本,也是用来回收老年代。
多线程,使用标记-整理算法。
这个收集器是JDK1.6之后才提供的。
由于这个收集器的出现,“吞吐量优先”才名副其实。Parallel Scavege +Parallel Old 的组合。在Parallel Old 收集器出现之前,Parallel Scavege收集器位置比较尴尬,不能与CMS收集器搭配使用,而其他老年代收集器会影响Parallel Scavege 收集器的 吞吐量。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短停顿时间为目标的收集器,重视服务的响应速度,所以网站,B/S系统服务端等这类场景,是适合CMS的。
CMS是基于:“标记-清除”算法实现的。实现过程相对复杂一些。
包括
- 初始标记:标记一下GC Roots能关联到的对象,速度很快
- 并发标记:GC Roots Tracing过程
- 重新标记:修正并发标记期间,因为用户线程继续运作导致的标记变动
- 并发清除
初始标记和重新标记两个过程仍需要暂停所有用户线程(Stop the World)。
整个过程 并发标
记和并发清除的耗时最长。
该收集器的优点:低停顿,并发收集。
缺点:
(1)会占用一部分线程,导致应用变慢,吞吐量降低。
默认回收线程数 (CPU数+3)/4,所以CPU数量地域4个时,对用户程序影响大。
(2)无法处理浮动垃圾,可能出现 Comcurrent Mode Failure。在并发收集阶段,用户线程还在运行,产生的垃圾只有下次GC才能被回收。因为在一些阶段用户线程和并发线程是并行的,需要预留内存给用户线程。而CMS运行期间,这个预留内存不足时,就导致 Concurrent Mode Failure ,启动预备方案,Parallel Old 重新回收老年代,导致停顿时间变长。这个只能调整参数,减少这种情况的出现。
(3)由于采用 标记-清除算法,会尝试大量内存碎片。没有新生代没有足够连续内存时,就会考虑老年代的内存空间,但是如果还是无法找到足够大的内存空间,就会提前出发 Full GC。当然,CMS通过设置相应参数提供空间整理功能(无法并发),以及多少次FullGC后,进行空间整理的参数。
G1收集器
G1收集器是垃圾收集器理论的进一步发展的产物。
与CMS收集器相比:
(1)采用 标记-整理算法,不会产生内存碎片。
(2)可以非常精确的控制停顿时间,使用户可以设置一次M毫秒的时间内,消耗在垃圾回收的时间不超过 N毫秒。
G1收集器可以实现基本不牺牲吞吐量的情况下,完成低停顿的垃圾回收。因为它极力回避全区域的垃圾收集。之前的收集器的回收范围都是整个新生代或者老年代。
G1收集器将整个java堆(包括新生代,老年代)分为多个大小固定的独立区域(Region),跟踪这些区域的垃圾堆积程度,后台维护一个优先列表,每次根据允许的回收时间,优先回收垃圾最多得区域(Carbage First)。这样的优先级,保证了G1收集器在有限时间内的最高收集效率。
内存分配与回收策略
自动内存管理解决两个问题:对象内存分配 和 回收对象内存。
下面是普遍几条分配规则。
- 对象优先在Eden区分配
- 大对象直接进入老年代:大对象指的是需要大量连续内存的java对象,例如很长的字符串或者数组。这样可以避免在Eden和Survivor之间发生大量拷贝。
- 长期存活对象将进入老年代:虚拟机给每个对象定义一个年龄计数器。对象在Eden区出生,经过一次minorGC后存活,且能被Survivor取容纳,就被移动到survivor区,年龄设为1.在survivor区每熬过一次minorGC,年龄就加一,增加到一定程度(默认15,可以自己设置),就进入老年代。
- 动态年龄判定:对象进入老年代,不一定受年龄限制。Survivor空间中,相同年龄的所有对象大小的综合大于Survivor空间一半,年龄大于或等于该年龄的对象就直接进入老年代。
- 空间分配担保:发生MinorGC时,虚拟机会检测之前每次进入老年代的平均大小是否大于老年代的剩余空间大小,如果大于,直接进行Full GC。如果小于,则查看 HandlePromotionFailure参数,是否允许担保失败。如果允许,则只会进行Minor GC,否则进行Full GC。新生代代采用复制收集算法,其中一个Survivor取用作轮换备份,如果 Minor GC后有大量对象存活,就需要老年代进行分配担保,那么Survivor区没法容纳的对象,直接进入老年代。取平均值是动态概率手段,如果某次Minor 存活对象激增,那么平均值是小于老年代剩余空间大小的,就会引发担保失败,失败后重新 full GC。虽然担保失败后步骤更多,但一般HandlePromotionFailure开关是打开的,减少了频繁的 full GC(Full GC 比 Minor慢很多)。