第3章 垃圾收集器与内存分配策略

目录

垃圾收集器与内存分配策略

对象已死?

分代收集理论

垃圾收集算法

标记-清除算法

标记-复制算法

标记-整理算法

Appel式回收算法

HotSpot算法实现细节

经典垃圾收集器

Serial收集器

ParNew收集器

Parallel Scavenge收集器

Serial Old收集器

Parallel Old收集器

CMS收集器

Garbage First收集器

使用命令查看当前JDK版本默认垃圾收集器


垃圾收集器与内存分配策略

对象已死?

垃圾标记算法

1.引用计数算法

C++智能指针、Python

2.可达性分析算法

Java
GC Roots的根对象作为起始节点,通过引用链到某个对象不可达时,证明此对象不可能再被使用。
GC Root对象用一句话可以描述为栈中引用的对象。

强引用:

通常所见的引用

软引用:

描述一些还有用,但非必须的对象。
系统将要发生内存溢出异常前,对软引用对象进行二次回收,仍然没有足够内存才会抛出异常。

弱引用:

只能生存到下一次垃圾收集发生为止。无论内存是否足够。

虚引用:

无法通过虚引用获得对象实例,为对象设置虚引用的目的只是为了在对象被回收时收到系统通知。

回收一个对象必须经过至少两次标记过程:

1.可达性分析后没有与GC Roots相连接的引用链,那会被第一次标记。

2.根据finalize()是否有必要执行(finalize()方法只能被执行一次,第二次将判定为没必要执行),将对象放入F-Queue中,由虚拟机建立的Finalizer线程去执行他们的finalize()方法。只要重新与引用链上的对象建立关联,则不会被收集,否则就要被回收。

不要使用finalize(),任何事可以由try-finally来做。


回收方法区

-Xnoclassgc

在大量使用反射、动态代理、CGLib等字节码框架的场景,需要jvm具备类型卸载的能力,保证不会对方法区造成过大的内存压力。

分代收集理论

Java由于使用追踪式垃圾收集算法,减小扫描无用对象的范围,通常垃圾收集齐会使用分代收集策略。收集器将Java堆划分出不同的区域,根据对象年龄分配到不同区域中。

建立在三条分代假说上:

  1. 弱分代假说,大多对象用完即丢。因此GC主要收集新生代。
  2. 强分代假说,熬过多次GC的对象就越需要,而不用丢弃。
  3. 跨带引用假说,跨带引用相对于同代引用占极少数。跨带引用的对象由于老年代难以消亡,使得跨带引用对象在新生代中晋升为老年代后,跨带引用也被消除。因此不用为了少量的跨代引用扫描整个老年代。只需要在新生代建立全局记忆集,这个记忆集标识出老年代哪块内存存在跨带引用。相比扫描整个老年代,使用记忆集则需要维护对象的引用更新情况。

垃圾收集算法

分为两种:

  • 引用计数式垃圾收集(直接垃圾收集)
  • 追踪式垃圾收集(间接垃圾收集) Java

标记-清除算法

标记过程即为上述垃圾标记算法,清除被标记的对象,反之亦可。

缺点:

  1. 执行效率不稳定
  2. 产生大量不连续内存碎片

标记-复制算法

内存分成两部分,标记清除后剩余的对象,复制到另一个部分,使得解决内存碎片的问题。

缺点:

  1. 对象复制成本高
  2. 可用内存缩小为原来的一半

商用Java虚拟机采用这个算法回收新生代。新生代中的对象有98%熬不过第一轮的收集,因此可以不必按照1:1的比例来划分新生代。1989年,Andrew Appel提出了更优化的半区复制算法,即**"Appel"式**,将新生代划分成Eden和两个Survivor区,默认比例8:1:1。(这就是为什么新生代需要划分为这三种区域的原因)

标记-整理算法

将所有存活的对象往内存另一侧移动,然后直接清理掉边界以外的内存。

缺点:

对于老年代对象来说,有大量对象存活区域,移动对象必须暂停用户应用线程(应该是碎片整理都需要"Stop The Wrold",包括标记-复制算法)。因为,需要移动对象并且更新所有引用这些对象的对象。

从JDK 12的ZGC技术实现了整理过程与用户线程并发执行。

Appel式回收算法

一种比标记-复制算法更优的半区复制分代策略,把内存分为Eden区和两块Survivor区,默认比例是8:1:1。


HotSpot算法实现细节

根节点枚举(即列出所有GC Roots,栈上对象引用等)和整理内存碎片一样会面临"Stop The Wrold"。但是现在JVM可达性分析查找引用链和用户线程可以并发。

在HotSpot解决方案里,使用一组称为OopMap的数据结构来存放着对象引用,而不需要一个不漏的检查完所有引用位置。

安全点

要求必须执行到安全点才能够暂停,进行垃圾收集。安全点得选择基本是以 `是否具有让程序长时间执行的特征`,
如方法调用,循环跳转,异常跳转。

当垃圾收集发生时,让所有线程跑到最近的安全点停下来,有两种方案:

1.抢占式中断(几乎没有虚拟机实现)

2.主动式中断

记忆集与卡表

卡表是记忆集的一种具体实现。
其实并不只新生代存在卡表,只要是跨区域的引用(例如Eden引用Survivor区的对象)都会记录在(Survivor)卡表中,每个区域有有一个卡表。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构(即跨代或者说跨区域的引用)。

作用是,使用记忆集来缩减GC Roots扫描范围,用以避免把整个老年代加进GC Roots扫描范围。

卡表可以只是一个字节数组(HotSpot就是这样),其每个元素都标识着一块内存块,卡页,卡页包含多个多个对象,
只要卡页内有一个对象存在着跨代指针,就将卡表元素的值标识为1,称为变脏。
当垃圾收集发生时,筛选出卡表变脏的元素,就能得到卡页对应的内存块中的对象,把他们加入GC Roots中扫描。

写屏障

解决卡表如何维护的问题。

当有其他分代区域的对象引用了本区域对象时,其对应的卡表元素就应该变脏。
变脏时间点发生在引用类型字段赋值的那一刻。

在HotSpot里是通过写屏障技术维护卡表状态的。写屏障可以看作在虚拟机层面对引用类型字段赋值动作的AOP切面。
人话就是,每次只要对引用进行更新,就会在引用对象赋值时产生一个环形切面(写前屏障和写后屏障),对卡表进行更新。

每次只要对引用进行更新,就会产生额外的开销。但对比扫描整个老年代的代价要低得多。

经典垃圾收集器

七种用于不同分代的收集器,连线表示可以搭配使用。

Serial收集器

新生代,标记-复制算法

最基础,历史最悠久。

单线程收集器,,单线程高效。

迄今为止,依然是HotSpot在客户端模式下的默认新生代收集器。

ParNew收集器

新生代,标记-复制算法

实质上是Serial的多线程版本,除了使用多线程收集垃圾外,其余行为与Serial收集器完全一致。

ParNew是激活CMS后的默认新生代收集器,使用命令来制定或禁用

-XX:+/-UseParNewGC

使用参数来限制垃圾收集的线程数

-XX:ParallelGCThreads

Parallel Scavenge收集器

新生代,标记-复制算法

吞吐量优先收集器

关注一个可控的吞吐量,运行用户代码与处理器总耗时(运行用户代码时间+垃圾收集时间)的比值。

控制最大垃圾收集停顿时间:

-XX:MaxGCPauseMillis
G1上也能使用

设置吞吐量大小(垃圾收集占总时间的比例):

-XX:GCTimeRatio
应该设置为大于0小于100的整数。

自适应策略参数:

-XX:+UseAdaptiveSizePolicy
设置这个参数后就不需要这是新生代大小比例了-XX:SurvivorRatio

Serial Old收集器

老年代,标记-整理算法

Parallel Old收集器

老年代,标记-整理算法

CMS收集器

老年代,标记-清除算法

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的收集器,适合用于B/S系统的服务端上。

并发低停顿收集器。

缺点:

  1. 垃圾收集线程值占用不超过25%的处理器资源。但处理器核心数量不足4个时,CMS对程序的影响就变得很大。

  2. 无法处理浮动垃圾,导致Full GC

    浮动垃圾,垃圾对象出现在标记过程后,CMS无法在当次收集处理掉的对象。

  3. 标记-清除算法带来的内存碎片问题

Garbage First收集器

G1收集器

新生代,老年代,标记-整理算法,两个Region之间使用标记-复制算法。

JDK 9发布之日,G1宣告取代Parallel Scavenge+Parallel Old组合,成为服务端默认的垃圾收集器,CMS被声明为不推荐使用的收集器。

G1不在坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域Region。每个Region都维护自己的记忆集,双向的卡表结构。

缺点:

  • 内用较大内存
  • 写前屏障造成额外负担

使用命令查看当前JDK版本默认垃圾收集器

java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=267137920 -XX:MaxHeapSize=4274206720 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_65"
Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.65-b01, mixed mode)

其中-XX:+UseParallelGC表示,Parallel Scavenge + Serial Old组合,Serial Old运行在服务器下不是最好的选择。

可以添加:

-XX:+UseParallelOldGC,开启Parallel Scavenge + Parallel Old组合。

-XX:+UseG1GC,开启G1收集器。


命令:

-verbose:gc,当回收时在输出设备显示信息。

-XX:+PrintGCDetails,当发生垃圾收集行为是打印内存回收日志,并在推出时输出当前内存各区域分配情况。

-XX:SurvivorRatio=8,默认比例即为8:1:1。

-Xms20M -Xmx20M,堆大小固定为20M。

-Xmn10M,分配给新生代的大小。


参考:《深入理解Java虚拟机》 第3版 周志明

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值