java虚拟机成神之路 - 垃圾收集器与内存分配策略:

如何确定对象是否死亡:

第一个为引用计数算法,对象被引用一次时计数就+1,引用失效时计数就减一,计数值为0时该对象已经死亡不能被使用。但是当两个不可能再被引用的对象互相引用时,就发生问题。现在主流的虚拟机几乎不适用引用计数算法来管理内存。

第二个为可达性分析算法:从根节点GC Roots进行向下搜索,与之关联的都是存活对象,没搜索到的都是不可能再被使用的对象。GCRoots不可达的会被判定为可回收对象,当第二次

引用:强引用 软引用(内存不够时,才会将这些对象回收) 弱引用 虚引用(这个对象被收集器回收时收到一个系统通知)

生存还是死亡?

可达性分析算法中判定为不可达的对象也不是非死不可。再被判定为不可达对象后,还有一次筛选机会,如果对象有必要执行finalize()方法,就会将对象放入F-Queue队列中,如果对象能与GCRoots引用链上任意对象关联,那么它就可以存活,完成自救。(finalize方法只能被执行一次)慰。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、 更及时,所以笔者建议大家完全可以忘掉Java语言里面的这个方法

回收方法区:废弃的常量和不会再被使用的类型。如常量池中的字符串系统中没用过。类型被回收的条件:该类的所有实例都被回收,该类的类加载器被回收,其他类不会通过反射引用该类。

垃圾收集算法

强分代假说:大多数对象都是马上要死亡的

弱分代假说:熬过越多次垃圾回收的对象就越难以消亡

跨代引用假说:存在少数对象在新生代和老年代中相互引用

垃圾收集器的设计原则:按堆划分不同区域,要死亡的对象放一块难以消亡的集中放一块

跨代引用被消除几乎要等到新生代年龄增长直到进入老年代。为了不为少数的跨代引用扫描整个老年代。老年代新建了一个记忆集的数据结构,把老年代分成若干块,标识出哪一块会存在跨代引用,扫描时只需要扫描记忆集就可以。此后在进行跨代扫描时,只有包含了跨代引用的小内存里的对象才会被加入到GC Roots进行扫描。

部分收集:

  • 新生代收集

  • 老年代收集 (CMS才有单独的老年代收集)

  • 混合收集(G1才有)

整堆收集:收集整个Java堆和方法区的垃圾收集

标记清除算法:把可回收的对象进行标记,回收已被标记的或者把存活的对象进行标记回收存活的对象。效率低。空间碎片化,大的对象来了不好存储。

标记复制算法

将内存分为大小相等的两块,复制存活的对象到另外一块,然后清理这块空间。内存缩小为了原来的一般,浪费了空间。如果大多数对象都是存活的对象,运行效率就太高了。

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代,IBM公司曾有一项专门研 究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此 并不需要按照1∶1的比例来划分新生代的内存空间。

Appel式回收将新生代分为Eden区和两块Survivor区,大小比例是8:1,每次分配内存时都将对象分配在一个Eden区和一个Survivor区,回收时将存活对象复制到另一个survivor区,然后一次性清理掉eden区和一个survivor区,当survivor存放不下去的时候会存放在老年代

标记整理算法:对可回收的对象进行标记,然后将存活对象都向空间的一端移动,然后直接清理掉边界以外的对象。

Hotspot的算法实现细节:

根节点枚举:我们以可达性分析算法中从GC Roots集合找引⽤链这个操作作为介绍虚拟机⾼效实现的 第⼀个例⼦。 在HotSpot的解决⽅案⾥,是使⽤⼀组称为OopMap的数据结构来达到这 个⽬的。⼀旦类加载动作完成的时候, HotSpot就会把对象内什么偏移量上是什么类型 的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈⾥和寄存器⾥哪些位置 是引⽤。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正⼀个不漏地从⽅ 法区等GC Roots开始查找。

安全点:Hotspot不会为每条指令生成一个OopMap而是在特定位置记录这些信息,这些位置被称为安全点。用户执行时,只有在到达安全点的位置才能够进行垃圾回收

如何在垃圾回收时让所有线程都跑到安全点?

抢先试中断:会先暂停所有线程的执行,没有跑到安全点上的线程会让它重新恢复,跑到安全点上再进行中断。

主动式中断:当垃圾收集需要中断线程的时候,会设定标志位,在线程执行过程中,如果标志位为真,则跑到最近的安全点上主动中断挂起。

记忆集与卡表:为了解决对象跨域引用所带来的的问题,使用记忆集来解决,记忆集中存放非收集区域指向收集区域的指针集合的抽象数据结构。收集器只需要通过记忆集判断出非收集区域是否存在指向收集区域的指针。

卡精度:被称为卡表,每个记录精确到一块内存区域,该区域内有对象含有快带指针。最为常用的方式去实现记忆集。

写屏障:写屏障可以看作在虚拟机层⾯对 “引⽤类型字段赋值”这个动作的AOP切⾯,在引⽤对象赋值时会产⽣⼀个环形(Around)通知

并发可行性分析:

经典垃圾收集器:

Serial收集器:单线程的收集器,在收集时只会使用一个线程或一个处理器,并且暂停掉其它所有线程,直到它收集完成

它依然是Hotspot虚拟机运行在客户端模式下默认的新生代收集器,它的优点是简单高效,在单线程收集器中是效率最高的。在近年来流行的微服务应用中,分配给虚拟机管理的内存不大,一般在几十兆至一两百兆之间,垃圾收集是暂停线程的时间只会控制在几十毫秒之间,用户完全可以接受。

ParNew收集器:实质上是Serial收集器的多线程并行版本,允许垃圾收集线程的多线程并行。CMS收集器的出现首次实现了用户线程和垃圾收集线程的同时工作,JDK5中使用CMS来收集老年代的时候,新生代只能选择parNew或者Serial。自从G1收集器的出现替代点了CMS+ParNew的组合。

并行:多个垃圾收集器线程之间可以一起工作

并发:垃圾收集器线程和用户线程之间可以一起工作,垃圾收集器的线程占用了一部分资源,应用程序段的处理的吞吐量受到影响。

Parallel Scavenge收集器:使用标记复制算法实现的收集器,是一款新生代收集器,与CMS等收集器不同的是,CMS收集器是为了在垃圾收集时缩短用户线程的停顿时间,而Parallel Scavenge收集器的目的是提高吞吐量。吞吐量是代码运行时间除以代码运行时间+垃圾收集时间

Serial Old收集器:是Serial的老年代版本,采用标记整理算法,是一个单线程收集器

parallel Old收集器:是parallel Scavenge的老年代版本,可以多线程并发收集,采用标记整理算法。可以与Parallel Scavenge搭配使用。

CMS收集器:以缩短用户停顿时间为目标,长应用在浏览浏览器的网站时,提升服务器的响应速度。CMS收集器是基于标记清楚算法来实现的。共有四个阶段分为初始标记(标记能GC Roots能关联到的对象)、并发标记(从GC Roots的直接关联对象开始遍历整个对象图的过程)、重新标记(修改哪些在用户线程运行的过程中标记发生改变的那部分对象)、并发清楚(清除掉已经死亡的对象)

优点是并发收集、低停顿

缺点是在并发阶段,会占据一部分线程导致收集器的吞吐量减少,影响用户体验。CMS默认的线程启动数是(处理器核心处理数+3)/4,如果处理器核心数太少,会导致处理器负载很高。

CMS无法处理浮动垃圾,在CMS的并发标记和处理标记结束了之后,用户程序是还在继续运行的,这期间就会产生垃圾对象,第一次无法收集它们,必须等到第二次收集时再处理。

CMS是基于标记清楚算法的,产生碎片化的空间,当有大对象进来时会无法为其分配空间

Garbage First收集器:G1收集器的出现取代了Parallel Old+Parallel Scavenge的组合,替代了CMS的组合。之前的收集器不是针对新生代进行回收就是老年代进行回收再或是Full GC,而G1是根据在哪块内存中进行回收的效益最高,这是G1收集器的Mixed GC模式。G1将Java堆划分成多个Region,不再支持固定大小及固定数量的分代区域的划分。每一个Region都可以是新生代的空间或者老年代的空间。大的对象可以直接用Jumongous区域进行存储,通过G1HeapRegionSize进行设置。每次回收的内存空间都必须是Region大小的整数倍。

通过使用记忆集来解决跨Region引用对象的问题

初始标记:只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象

并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图找出要回收的对象。可与用户程序并发执行

最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留的少量的STAB记录

筛选回收:更新Region统计数据,把要回收的Region部分复制到空的Region中,清楚当前Region部分

G1收集器希望做到的是在延迟可控的情况下做到高吞吐量。G1整体上看是标记整理算法,局部上看又是标记清除算法。G1带来了许多创新,如指定最大停顿时间,使用Region区域进行内存布局、按最大收益进行垃圾回收这些都是G1收集器的创新之处。

选择合适的垃圾收集器

垃圾收集器的作用其实不仅仅是进行垃圾收集,更重要的是进行内存的管理、分配等行为。

衡量垃圾收集器的三项最重要的指标是:内存占用、吞吐量和延迟

内存分配与回收策略:

对象是在堆上进行分配的,新生对象一般分配在新生代中,少数超出标准大小的对象直接分配在老年代中。关于对象的创建和存储细节这个要依据不同的其选择的垃圾回收器来决定。

对象优先在Eden分配

新生对象优先在Eden区进行分配,Eden区和Survivor的大小比是8:1,HotSpot有两个Survivor。当Eden和一个Survivor没有多余的空间再存储要进来的对象时,就会发生一次Minor GC,要进来的对象大小大于Survivor区的大小,就会将利用分配担保机制对象直接存储在老年代。

大对象直接进入老年代:

HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的是避免对象在Eden区和两个Survivor区来回复制,产生大量的内存复制操作

长期存活的对象进入老年代

虚拟机给每个对象都定义了一个年龄计数器,每进行一次Minor GC年龄计数器就+1,当达到15岁时就会被晋升到老年代中(默认为15)。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置

动态对象判定年龄

虚拟机中并不是新生代中的对象年龄达到一定大小才能进入老年代,只要相同年龄的对象内存大于Survivor区的一般就会直接进入老年代中。

空间分配担保

在发生Minor GC之前先检查虚拟机新生代中所有对象的总和是否小于老年代最大可用的连续空间,如果小于那么这次Minor GC是安全的,如果不小于就会检查老年代的最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则安全,如果小于那么表明是有风险的,会进行一次Full GC,这样停顿时间就会很长0

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值