JVM-读书笔记-第三章

文章目录

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

3.1 概述
3.2对象已死吗?
3.2.1引用计数算法

解决变量是否存活的问题:给对象添加一个引用计数器,每当有引用时,计数器加一;每当引用失效时,计数器减一。

主流的JAVA虚拟机里面没有选用计数算法来管理内存,其中最主要的原因是因为他很难解决对象之间的相互循环引用问题。

1、可达性分析算法

通过一系列成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链(Reference Chain),当一个对象到“GC Roots”没有任何的引用时(用图论的话说就是“GC Roots”到这个对象不可达时),则证明此对象是不可用的,会被回收。

2、“GC Roots”对象包括以下几种
  • 虚拟机栈(栈针中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 方法本地栈中JNI(即一般说的Native方法)引用的方法
3.2.3引用程度区分 page 65

希望能够对引用的对象进行区分,内存够的时候留着,不够的时候删除掉

  • 强引用:只要强引用在,永远不会被垃圾回收
  • 软引用:SoftReference类实现,在系统发生内存溢出异常之前,才会把这些对象列入二次回收范围之内进行回收。如果回收之后还没有足够的内存,则发生内存溢出的异常。
  • 弱引用:WeakReference类实现,被弱引用关联的对象能够存活到下一次垃圾收集发生之前。当垃圾收集工作时,无论内存是否够用都会被回收。
  • 虚引用:PhantomReference类实现,最弱的引用关系,无论是否由虚引用的存在,完全不会对其生存时间构成影响。对一个对象设置虚引用关联的唯一目的就是能在这个对象被收集齐回收时能收到一个系统通知。
3.2.4不可达对象

一个对象的真正宣告回收需要经历两次标记

如果一个对象没有与GC Roots发生引用关系,那么他将会第一次被标记并进行一次筛选。筛选的条件是对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者是已经被虚拟机调用过,虚拟机是这两种情况为“没有必要执行”。

如果被判定有必要执行finalize()方法,虚拟机将它放在F-queue队列中。并由一个虚拟机自动建立的Finalize线程去执行它。为了防止某个对象出现执行缓慢或崩溃而导致其它对象无法执行finalize()方法,所有对象不会执行完成。在finalize()中拯救自己,即跟任何一个引用对象建立关联,那将被取消标记。否则会被标记两次被回收。

3.2.5回收方法区

永久代中的垃回收:废弃常量和无用的类。

3.3垃圾收集算法(笔记见书上)
3.3.1标记清除算法(最基础)
1、过程:标记加清除,前面说过
2、不足:
  • 效率太低:标记和清除效率都不太高
  • 空间问题:执行过后产生不连续的内存空间。当系统需要分配比较大的对象时没有连续的内存空间,导致再进行一次回收过程。
3.3.2复制算法(Eden space)

将总体的内存分成两块儿,先用一块儿地方,满了将这块所有的东西复制到另一块儿上,再把这块儿清理干净。

好处:提高了效率,解决了空间问题。

坏处:每次只用一块儿,空着另一块儿,造成浪费。

空间分配详见 page 70

3.3.3标记整理算法(老年代)

标记过程同上,经历过一次回收后,让存活下来的对象向一端移动,空出来大块内存直接清理掉给之后用。

3.3.4分代收集算法(所有的垃圾收集都采用这个算法)

没有什么新的思想,只是把上述三种算法在不同的区域合理地使用。

即新生区由于回收大量的对象,采用复制算法;

老年代采用标记清理或者标记整理的方法进行对象回收。

3.4 hotspot算法实现
3.4.1枚举根节点
问题一:可达性分析需要对根节点进行枚举,而很多方法仅仅方法区就几百兆,进行枚举显然耗费时间。
问题二:GC停顿,进行可达性分析时必须保证整个执行系统停顿在某个时间节点上,不可以在可达性分析过程中引用关系发生变化。所以在GC进行时,所有的线程必须都停止工作。(号称不会停顿的CMS收集器,在GC时也会停顿。)
HotSpot中使用OopMap的数据结构来达到记录执行上下文和全局引用的位置。虚拟机在执行类加载文件后HotSpot把对象内什么偏移变量上是什么类型的数据计算出来。JIT编译时,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样GC知道哪些地方存放了哪些引用。
3.4.2安全节点
1、问题一:

OopMap帮助HotSpot快速准确的完成枚举。但是引用关系变化导致生成大量的OopMap浪费空间。

问题解决:

只在特定的位置生成OopMap,这些节点叫做安全节点(safepoint)。

2、safepoint选定

既不能太少让GC等待时间太长,也不能太频繁增加系统负荷。

解决

以长时间执行为标准进行选定。因为长时间执行的特征是指令序列复用,(例如方法调用、循环跳转、异常跳转等等)具有这些功能的指令会产生safepoint。

3、GC发生时如何实现中断:

分为两种方式,抢先式中断阻塞式中断

1)抢先式中断,gc发生时把所有线程都中断,没跑到安全节点的,先跑到安全节点再中断。几乎没有虚拟机使用这种方式了。

2)阻塞式中断:gc发生时,不直接对线程操作,而是设置一个标志位,各个线程主动去轮训这个标志位,发现标志位为真的时候自己中断。轮询标志位的位置和安全节点的位置是重合的。

3.4.3 安全区域

线程处于block状态或者是sleep状态时,这时候的线程无法响应中断的请求,也就没办法到安全的地方挂起啦。JVM也不可能等到线程重新被CPU分配时间,对于这种情况需要使用安全区域来进行解决。

安全区域safe region指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,可以看作是扩展了的safepoint。

当线程执行到safe region中的代码时,首先标记自己进入了safe region,这样开始GC时不用管标识自己为safe region的线程了。离开safe region时也要检查自己是否已经完成了根节点的枚举(或者是整个GC过程),完成了那就继续执行,没完成就必须等待直到可以安全离开safe region的信号为止。

3.4.4记忆集(Remember Set)与卡表(card table)

垃圾收集器在新生代中引入记忆集(Remember Set)以避免把整个老年代加进Gcroot的扫描范围。并不是只有新生代和老年代存在跨代引用的问题,所有部分收集器(partial GC)行为的收集器都涉及到。典型的如G1、ZGC、Shenandoah都存在这样的问题。

记忆集是一种用于记录从非收集区域指向手机区域的指针集合的抽象数据结构。

卡表就是记忆集的一种具体实现,它定义了记忆集的精度、与堆内存的映射关系。卡表和记忆集的关系按照java中hashmap和map的关系类比理解。

(书85页,变脏的具体解释)

3.4.5写屏障(-XX:+UserCondCardMark)

如何把卡表变脏,谁来把卡表变脏?

变脏,其他粉黛区域中对象引用了本区域的对象是,对应的卡表变脏。如何维护呢?

hotpot虚拟机使用写屏障(write barrier)技术维护卡表的状态。这个屏障需要同并发中的屏障区分开。

伪共享问题:当多线程修改互相独立的变量时,如果这些变量恰好共享一个缓存行,就会彼此影响而导致性能降低(写回、无效化或者是同步)。

解决:采用先检查标记再标记(if(没别标记)then(标记))。

3.4.6并发的可达性分析

垃圾处理器靠可带那个分析算法判定对象是否存活。这需要一致性保证,即暂停所有线程、停顿时间同java堆容量成正比。对越大标记停顿时间越长。

解决并发扫描一致性的问题:增量更新、原始快照。

cms基于增量更新做并发标记,G1、shenandoah基于原始快照解决。(具体书89页)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值