老马的JVM笔记(二)----垃圾回收与内存分配

2.1 对象的生存

2.1.1 引用计数

新建对象时,给该对象定义一个计数器。每次这个对象被引用,计数器+1;每次引用结束,计数器-1。当该对象的计数器为0时,该对象失效,被清理。

问题在于如果两个对象相互循环引用,会导致无法被GC。没人用这方法。

2.2.2 可达性分析算法

可达性分析为“可达路径分析”。对象中会有一个GC Root作为可达起点,新建的对象会以此连入该“树”中,一旦该节点(对象)失去与GC Root的连接,也就是没有路径可以从GC Root达到该对象,这个对象失效,可以被回收。

GC Root的选择:

1.虚拟机栈(本地变量表)中引用的对象

2.方法区中类静态属性引用的对象

3.方法区中常量引用的对象

4.本地方法栈中JNI引用的对象

反正都是一些非常“扎根”的对象。

2.2.3 引用

1.强引用:最稳固的引用,不会被回收。就是最基本的新建对象,Object obj = new Object();

2.软引用:SoftReference修饰的对象。“还有用但非必需”。这些对象将会在发生内存溢出异常前被回收。相当于强引用不可以被回收,我们就只能回收这些了。

3.弱引用:WeakReference修饰的对象。“非必需”。只要发生GC这些对象就必死。

4.虚引用:PhantomReference修饰的对象。非常弱。虚引用无法获取对象实例。作用是在引用的对象要被GC的时候给出一个通知。

F-Queue队列专门用于存储即将被finalize()的对象们。因为是队列,所以存在队列阻塞的隐患。比如有的finalize()里面出现了死循环,那整个队列都要没完没了地等待。在finalize()里捞一下对象的做法是不可取的。

2.2.4 方法区的回收

方法区其实没什么可回收的。主要两种:废弃常量,无用的类。但这两种东西是非常难以界定的。不好。

无用的类:

1.该类所有实例都已经被回收

2.加载该类的所有ClassLoader都被回收

3.该类的对象在任何地方不被引用, 也无法在任何地方通过反射访问该类

2.3 GC算法

2.3.1 标记-清理算法(mark-sweep)

最基础的算法。标记出要清理的对象,然后全部清理掉。这样就会出现问题:内存碎片。指哪打哪地清理就会导致清理的对象是不连续的,剩余的好对象也都是不连续的。标记过程就是可达性(或者引用计数)。这种算法的效率也非常不高,both标记和清理两个过程。

2.3.2 复制算法(copying)

另外开出来一块区域,区域大小不一定。在垃圾回收时,把标记的要清理的对象摘出来,把存活的对象复制到该区域里,然后把这里的所有对象清理掉。这样,标记的对象全部被清理,存活的对象搬家到新的区域里。好处在于这样内存空间一定是连续的,不会出现碎片问题。问题在于这块区域到底该多大。55开肯定是非常低效率的。考虑到很对对象生存周期都很短,“98%的对象都是朝生夕死的”。所以用于盛装的区域可以很小。8:1是可以接受的。但如果真的活下来的对象非常多怎么办?就要进行“分配担保”。

复制算法的问题在于如果存活对象比例过高,复制成本也会变高,效率变低。

2.3.3 标记-整理算法(mark-compact)

标记整理基于标记清理算法。为了解决内存碎片问题,标记之后将存活的对象挪向一边,把将清理的对象移向另一边,这样存活的对象在清理之后变得连续。

2.3.4 分代收集算法(generational collection)

那么有没有一款算法可以解决以上三种算法的问题呢?就是融合。根据对象的生存周期不同,将内存分为几个部分:新生代,老年代。新生代(局域变量、循环内的临时变量等等)的对象大部分都会很快死去,那就用复制算法,因为存活率非常低。老年代(缓存对象、数据库连接对象、单例对象)因为都是存活率高的对象,复制算法效率,额外空间成本高,就是用标记清理或标记整理算法。这种算法是现在很主流的算法。新生代和老年代对象都在Java堆里,永久代在方法区里。

2.4 算法实现

在GC时,为了保证程序的一致性,需要将程序停顿一下,否则还会有新的将被回收的对象的出现。这个停顿叫“Stop the world”。在枚举根节点(GC Root)的时候,必须停顿。

停顿不可以随时停顿,就需要一个时机来安全停顿,这个时间点叫安全点(safepoint)。在方法调用、循环跳转、异常跳转等时机时,可以记录safepoint。

如何保证所有线程都在safepoint停下来?

1.抢先式中断(preemptive suspension):强行中断所有线程,如果你没到safepoint,那你就快跑几步到安全点。

2.主动式中断(voluntary suspension):程序中设立一个break的flag。每个线程没事查询一下这个flag是否为真,如果是了,那就把自己停下来。

safepoint可以解决一些“跑着”的线程,但有些线程在sleep,block就无法做到。这是需要查看代码是否在安全空间(safe region)。进入这块代码,用一个flag标识一下,如果jvm发起GC,就可以GC了。在重新跑起来时,查看一下GC有没有完成,完成了就可以继续跑了。

2.5 垃圾收集器

2.5.1 Serial收集器

最原始的收集器。用一个线程专门GC,且在GC时其他线程都要停下来等他。说到这就已经觉得很废物了。但在单线程收集器里,他简单而高效。

2.5.2 ParNew收集器

Par指的是Parallel。ParNew指的是Serial的多线程版本。Serial不是因为只有一根线程所以Stop the world太长吗,那我就多给你几根一起GC。并行的Serial。ParNew为新生代收集器(不是说他新,就是他收集新生代)。

2.5.3 Parallel Scavenge收集器

新生代收集器。同样是并行收集器,Parallel Scavenger关注系统的吞吐量(throughput),即一段时间里,我干活的时间比上(干活+GC)的时间要足够高。这样在用户体验上来看,大部分时间在干活。Tradeoff在于平衡“干一会儿歇一会儿”和“使劲干使劲歇”。

2.5.4 Serial Old收集器和Parallel Old收集器

Serial Old收集器老年代版Serial收集器,一样单线程。使用标记-整理算法(新生代就都用复制算法了)。

Parallel Old收集器为Parallel Scavenge收集器的老年代版。与Parallel Scavenge收集器互相配合,一起关注吞吐量。

2.5.5 CMS收集器

CMS(Concurrent Mark Sweep)收集器,关注极大缩短停顿时间。

过程:

1.初始标记(CMS initial mark):Stop the world。标记一下GC Root能直接关联到的对象。非常快。

2.并发标记(CMS concurrent mark):GC root Tracing。

3.重新标记(CMS remark):Stop the world。更新并发标记时导致的标记变动。

4.并发清除(CMS concurrent sweep):清理。

耗时长的部分,24,都是在并发运作,所以无需停顿。停顿的部分耗时又很快。可以保证用户的流畅体验。

缺点:

1.CMS收集器对CPU资源非常敏感。毕竟要占用CPU来清理内存,还要并发。所以在CPU不够多的时候,会使程序效率变低。解决方法是“增量式并发收集器”(Incremental concurrent mark sweep/ i-CMS):GC线程与用户线程交互运行,时间边长一些但不会变卡顿。但这东西现在已经不让用了。

2.CMS收集器无法处理浮动垃圾。浮动垃圾就是并发时用户产生的新垃圾。为了应对并发时的新垃圾,需要留出足够内存。在并发途中的新垃圾叫浮动垃圾。这些垃圾要在下一次GC才能解决掉。

3.CMS收集器会出现内存碎片问题。CMS基于标记清理。只有在full GC的时候才能整理内存碎片。

2.5.6 G1收集器

G1(Garbage-First)收集器面向服务端应用,最先进的垃圾收集器(现在呢?)。

特点(综合了很多收集器的优点):

1.并行与并发

2.分代收集

3.空间整合

4.可预测停顿

不同于前几款收集器,G1模糊了新生代老年代的物理隔离,转而将Java堆分成很多region,每次优先收集价值最大的region。价值由回收可得的空间与所需时间来界定。这样能保证收集效率尽可能高。G1中,虚拟机为每一个region建立remembered set来维护region之间的对象引用,避免全堆扫描。

收集过程:

1.初始标记(initial marking)

2.并发标记(concurrent marking)

3.最终标记(final marking)

4.筛选回收(live data counting and evacuation)

2.6 内存分配

对象优先分配在Eden space(新生代区)。当新生代区满了,先来一次minor GC。如果还不行,只能通过分配担保机制把对象放入老年代中。

大对象指需要大量连续内存的对象(长字符串或者大数组)是不好的,特别是速死的大对象,要避免。这种对象会被放入老年代。

如何让新生代进入老年代?变老。当一个对象活过一次GC,年龄会+1,一旦年龄到了某一阈值,会进入老年代(比如15)。这个阈值可以动态变化。

在新生代minor GC的时候,由于使用复制算法,一旦survivor空间不足,会通过空间分配担保移入老年代,这时就需要在GC前先比较一次新生代对象内存是否小于老年代可用内存的和。用新生代对象平均内存进行比较也可以,来决定是否需要full GC来为老年代腾出更多空间。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章