《深入理解java虚拟机》读书笔记 第三章 垃圾收集器与内存分配策略

18 篇文章 0 订阅
7 篇文章 0 订阅

为什么我们要了解GC和内存分配?

当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,
我们就需要对这些“自动化”的技术实施必要的监控和调节。

对象已死吗

在堆里面存放着Java世界中几乎所有的对象实例垃圾收集器在对堆进行回收前,第一
件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何
途径使用的对象)。

引用计数算法(Reference Counting)

原理:为对象添加一个引用计数器,每当它被引用时,+1;引用失效,就-1;当计数器为0,则认为对象已死。

但是java虚拟中并不使用,缺点:
它无法解决对象间相互循环引用的问题。

可达性分析算法( Reachability Analysis )

原理:通过"GC Roots"的对象作为起点,向下搜索,当一个对象没有引用链与GC roots相连,则说明此对象不可用。

引用链(Reference Chain):搜索所走过的路径
GC Roots对象:
    1. 虚拟机栈(栈帧中的本地变量表)中引用对象
    2. 方法区中类静态属性引用对象
    3. 方法区中常量引用的对象
    4. 本地方法栈中JNI(即一般说的Native方法)引用的对象

这里引用了https://blog.csdn.net/luzhensmart/article/details/81431212的一张图,其中,Object5、Object6、Object7 则无到GC Roots的引用链,可被回收。
在这里插入图片描述

再谈引用

在JDK 1.2之后,Java 对引用的概念进行了扩充,将引用分为强引用( Strong Reference)、
软引用(Soft Reference)、弱引用( Weak Reference)、虚引用(Phantom Reference) 4种,这
4种引用强度依次逐渐减弱。

强引用
指在程序代码之中普遍存在的,类似“Object obj = new Object()” 这类的
引用。
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用
用来描述一些还有用但并非必需的对象。
在系统将要发生内存溢出异常之前,将会把软引用对象列进回收范围之中进行第二次回收。 如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用
用来描述非必须对象。
当发生GC时,无论内存是否足够,弱引用对象都会被回收。
虚引用
最弱的引用关系。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

生存还是死亡

要真正宣告一个对象死亡,至少需要两次标记过程。
第一次:
虚拟机进行可达性分析后,发现对象没有到GC Roots的引用链,标记其,进行下一步。
第二次:
判断其是否需要执行finalize()方法,当对象没有重写finalize()方法,或者已经被调用过,则标记其为要回收的对象。

在文中,作者不推荐重写finalize()方法。

回收方法区

java虚拟机规范中确实说过不要求虚拟机在方法区实现垃圾收集。

永久代的垃圾回收分为两部分:
    1. 废弃常量
        回收方式与java堆中的对象非常类似
    2. 无用的类,同时满足三个条件:
        2.1 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
        2.2 加载该类的ClassLoader已经被回收。
        2.3 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
        
        虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是"可以",而并不是
        和对象一样, 不使用了就必然会回收。是否对类进行回收HotSpot 虚拟机提供了-xnoclassgc参
        数进行控制,还可以使用-vrboseclass以及_XX:+TaceCass oading. -XX:+TracClassUOnL oading查看
        类加载和卸载信息,其中-vertbosc:class 和-XX:+TraceClassSL oading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading 参数需要FastDebug版的虚拟机支持。
        在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类
        频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

垃圾回收算法

标记-清除(Mark-Sweep)

分为两个阶段:
    1. "标记"
    标记出所有需要回收对象
    2. "清除"
    标记完成后统一回收

不足:
    1. "效率问题"
    2. "空间问题"
    产生大量不连续的内存碎片,在分配大内存对象时,可能会因为没有足够的连续内存,而额外触发一次GC。

在这里插入图片描述

复制算法(Copying)

为了解决标记-清除算法的效率问题。

复制算法适合于对新生代的回收,
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都
是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶
指针,按顺序分配内存即可。
不足:
    1. 在对象存活率较高时就需要较多的复制操作,"效率变低"。
    2. "如果不想浪费50%空间",就需要额外的空间进行分配担保

在这里插入图片描述

标记-整理算法(Mark-Compact)

根据“老年代”的特点提出。
与标记-清除算法不同的是,标记后让存活对象向一端移动,然后清理无用的空间。

image

分代收集算法(Generational Collection)

当前商业虚拟机GC都采用此算法。
根据对象存活周期不同,将内存分块。

java堆分为新生代和老年代,这样根据各个年代的特点采用最合适的GC算法。

新生代:
    每次垃圾收集都发现有大批对象死去,只有少量存活,就采用"复制算法"。
老年代:
    对象存活率高,没有额外空间进行分配担保,就使用"标记-清理"或者"标记-整理"。

HotSpot的算法实现

枚举根结点

可达性分析中,可作为GC Roots的节点非常庞大,如果要逐个检查,这必然消耗很多时间。在HotSpot中,则是使用"OopMap"的数据结构来实现。在类加载完成时,HotSpot就将对象数据计算出来,也会在"安全点"记录下栈和寄存器中那些位置是引用。这样GC在扫描时就可以直接取得执行信息。

同时,其对执行时间的敏感还表现在GC停顿上。在分析执行时,需要保持"一致性"(对象引用关系不变化)。要达到一致性,GC必须停顿(Sun称之为“Stop The World”)。即使是号称(几乎)不会停顿的CMS,枚举时也需要停顿。

在这里插入图片描述

安全点(SafePoint)

安全点的选定基本上是以"是否具有让程序长时间执行的特征"为标准进行选定。如:方法调用、循环跳转、异常跳转等。

另一个问题,如何在GC发生时,让所有线程(不包括执行JNI调用的线程)都运行到最近的安全点上再停顿?
解决方案:
    1. 抢先式中断(Preemptive Suspension)
        几乎没有虚拟机使用。GC发生时,首先中断所有线程,如果发现有线程不在安全点上,则恢复,让其运行到安全点。
    2. 主动式中断(Voluntary Suspension)
        当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

安全区域(SafeRegion)

安全区域可以看成是安全点的拓展点。

安全点在程序没有分配CPU时间时,如:线程处于Sleep、Blocked状态,这时线程无法响应jvm的中断请求。对于这种情况,就需要安全区域来解决。

在线程执行到Safe Region中的代码时,首先标识自己已经进人了Safe Region,那样,当在
这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离
开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完
成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

垃圾收集器

首先了解并行与并发的区别。

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾回收线程同时执行,用户程序在继续运行。
垃圾收集器是内存回收的具体实现。
JDK1.7 Update 14提供了商用的"G1"收集器。

下图中,存在连线的垃圾收集器,说明可以搭配使用

在这里插入图片描述

Serial收集器

单线程收集器。
依然是虚拟机在Client模式下的默认新生代收集器。
1. 只使用一个CPU或一条线程完成GC工作
2. 在执行垃圾收集时,"必须暂停其他工作线程"。

优点:
    简单高效(与其他收集器的单线程比)

在这里插入图片描述

ParNew收集器

其实就是Serial收集器的多线程版本。除了使用多条线程之外,其余与Serial相同。
它是许多运行在Server模式下的虚拟机的首选新生代收集器。

在这里插入图片描述

Parallel Scavenge收集器

特点
它的关注点与其他不同,其他收集器关注点事尽量缩短GC时用户线程的停顿时间,而Parallel Scavenge目的是达到一个可控制的吞吐量。

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

特点:
自适应调节策略:
-XX:+UseAdaptiveSizePolicy 
开启此参数后,就无需手动调节更多参数,只需要把基础内存数据设置好,然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio
(更关注吞吐量)参数给虚拟机设立一个优化目标。

Parallel Old收集器

Parallel Scavenge 的老年代版本。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel
Old收集器。

在这里插入图片描述

CMS收集器(Concurrent Mark Sweep)

一种以获取最短回收停顿时间为目标的收集器。
大多B/S应用(重视响应速度的服务)使用。
CMS是一种标记-清除算法实现。其过程分为4步骤:
    1. 初始标记(CMS initial mark)
    2. 并发标记(CMS concurrent mark)
    3. 重新标记(CMS remark)
    4. 并发清除(CMS concurrent sweep)
其中,初始标记和重新标记扔需要"Stop The World"。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都能与用户线程一起工作,所以在总体上,CMS收集器是与用户线程并发执行的

缺点:
    1. 对CPU资源敏感。默认启用(CPU数量+3)/4个线程。当CPU在4个以下时,对用户程序影响很大。
    2. 无法收集浮动式垃圾(Floating Garbage),可能出现"Concurrent Mode Failure",而导致另一次Full GC。
    3. 由于它是一种"标记-清除"算法实现,会产生大量空间碎片。无法对大内存对象进行分配而进行Full GC。为了解决这个问题,CMS提供了"++UseCMSCompactAtFullCollection"默认开启的开关参数。在Full GC时,同时整理内存碎片。解决内存碎片问题的同时,引入了新的问题:
        停顿时间变长。同时,CMS提供"-XX:CMSFullGCsBeforeCompaction" 参数用于设置多少次不压缩Full GC后,执行一次整理内存碎片的GC。(默认为0,即每次都进行整理)
    
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。
    

G1收集器(Garbage First)

是当今收集器技术发展的最前沿成果之一。在JDK7u4中正式商用。

特点:
    1. 并行和并发。充分运用多CPU、多核的优势,来缩短"Stop-The-World"时间
    2. 分代收集。G1可以不需要其他收集器配合,但它还是以不同的方式来对待新生代和老年代。
    3. 空间整合。从整体上看是基于"标记-整理"的。这种特性有利于程序长时间运行时,因无法给大对象分配连续空间,而提前进行GC。
    4. "使用G1时,Java堆的内存布局与之前的收集器不同。G1将java堆分成多个等大小的内存区域(Region),虽然保留了新生代和老年代,但是它们并不是物理隔离的了。"
    5." G1收集器之所以能建立可预测的停顿时间模型,是因为它避免在java堆中进行全区域的垃圾收集。它在后台以每个Region中的价值大小(回收所获得的空间大小以及回收所需时间)维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这也是Garbage-First名称的来由。 这种优先级的区域回收方式,保证在有限时间内尽可能高的收集效率。"
G1如何避免在收集垃圾时,进行全堆扫描?
    为什么会发生全堆扫描:
        Region不是孤立的。一个对象在Region A中,它可能被java堆中任意对象引用。在做可达性分析时,还需要扫描整个java堆?
    G1如何避免上述情况?
        G1每个Region都有对应的"Remembered Set"。当对象被其他Region引用时,记录。在发生GC时,在GC Roots枚举范围加入Remembered Set,即可保证不全堆扫描,也不会有遗漏
执行步骤:
    初始标记(Initial Marking)
    并发标记(Concurrent Marking)
    最终标记(Final Marking)
    筛选回收(Live Data Counting and Evacuation)
    

在这里插入图片描述

内存分配与回收策略

自动内存管理归结为自动化解决了两个问题:
    1. 给对象分配内存
    2. 回收分配给对象的内存
对象的内存分配:
    往大方面讲,就是在堆上分配。对象主要分配在新生代的Eden区域。

普遍的内存分配规则:

先介绍三种GC,(结合书本和Minor GC、Major GC和Full GC之间的区别
Minor GC:"当JVM无法为一个新的对象分配空间(Eden区满)时会触发 Minor GC",(书中将Minor GC称为新生代GC),也称为Young GC

Major GC/Full GC 书中称为老年代GC。
触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

1. 对象优先在Eden分配
当JVM无法为一个新的对象分配空间(Eden区满)时会触发 Minor GC
2. 大对象直接进入老年代
写程序时应该避免创建大量"短命大对象"。大对象容易导致提前触发GC。
3. 长期存活的对象将进入老年代
分代收集的思想实现:
    虚拟机给每个对象定义年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
4. 动态对象年龄判定
    为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进人老年代,无须等到MaxTenuringThreshold中要求的年龄。
5. 空间分配担保
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值