java虚拟机随手笔记(2)垃圾收集器与内存分配策略

JIT:JVM机试编译,just in time complication

TLAB:本地线程分配缓冲,Thread Local Allocation Buffer

回收类型

1.何时回收对象?即如何判断对象已死:有两种方式,但是java用的是第二种。即判断对象的可达性。java会将所有的对象连接到一个引用链上,一般是个树,从树的根节点GC roots开始搜索,如果能搜索到,说明没死,搜索不到说明死了。GC Roots并不是只有1个,需要存储在GC Root Set里面,然后被垃圾收集器锁维护

第一种就是很多面试官问,然后同学们回答的:采用计数器法,一个引用+1,减少一个引用-1.最后引用为0就回收。但无法避免相互循环引用的情况。比如A持有B的引用,B持有A的引用。但是A,B没有其他对象持有了,这个时候也是需要回收的。

2.引用的类型:强引用(不会被回收),软引用(可能会被回收),弱引用(下一次一定会被回收(除非特殊情况)),虚引用(最弱的引用)

3.生存还是死亡:对象被回收之前会进行两次标记,在这两次标记之间还有机会拯救自己。第一次标记是被发现没有GC root相连,这个时候要判断对象是否覆盖finalize方法,或者被调用过。如果没有覆盖也没有被调用过finalize方法,那么对象会被放置到一个F-Queue队列之中,并稍后会由虚拟机自动建立的finalize方法进行回收。当然在执行finalize方法之前还会被进行一次小规模的标记,如果在这之前能够简历新的引用,还有机会存活,这就是存活的机会。

如果finalize被执行过一次了,那么这个对象会直接被回收掉。如果finalize被重写了,那么垃圾回收器会调用它自身的finalize方法。这个finalize方法也只会被执行一次。

不推荐使用finalize方法, 可以使用try-finally方法啊,因为finalize方法优先级很低,需要等待一段时间才能执行。finally及时多了。

4.回收方法区:HotSpot的虚拟机的方法区又叫永久代,这部分现在在java8中已经取消了。但是还是了解一下。回收废弃常量和无用的类。回收废弃常量和回收java对中的对象非常类似。

判定一个类为无用的类的条件:该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例;加载该类的ClassLoader已经被回收(类加载其);该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

5.标记-清除垃圾回收算法:最基础的收集算法,首先标记出所有的需要回收的对象,标记完成后统一回收所有被标记对象。不足:效率不高,内存会产生大量内存碎片。

6.复制算法:将内存按容量划分成大小相等的两块,每次只使用其中一块。当一块用完了,就将还活着的对象复制到另外一块上面,然后将原来的那一小块内存全部清理掉。不足:内存缩小了一半啊亲!,这个主要运用于年轻带(新生代)

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代(又叫年轻带,英文New,Young)中的对象98%朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将EdenSurvivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。 HotSpot虚拟机默认EdenSurvivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%80%+10%),只有10%的内存会被浪费。 当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代,又叫终生代,英文Tenured、Eden)进行分配担保(Handle Promotion)。


7。标记-整理算法:在老年代,提出了标记整理算法,来避免复制算法的缺点。标记过程和标记-清除算法一样,但是整理算法是让或者的对象向一端移动,然后直接清理掉端边界意外的内存(就是死掉的内存))

8.分代收集算法:将java堆分为新生代和老年代,每个代采用不同的收集算法以适应不同代的特性。年轻代每次都有很多对象死去,所以就用复制算法。而老年代死去的对象并不多,所以就可以用标记-清除算法或者标记-整理算法

9.HotSpot的算法实现

可达性分析从GC Roots找引用链这个操作为例,可作为GC Roots的节点主要是在全局性的引用(例如常量或类的静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有几百兆,如果要逐个检查这里面的引用,那必然是很慢的。

而且如果是多线程执行,那么在进行可达性检查的时候,为了确保一致性,还必须加锁,这样会极地的降低JVM的效率。

HotSpot是使用准确式GC和OopMap的数据结构来快速完成GC Root枚举的,具体怎么做的百度吧。。。反正这里面很大一块知识点。

垃圾收集器



上图介绍了7种用于不同分代的收集器,两个收集器间存在连线,说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代还是老年代。

10.Serial收集器:最早的收集器,不仅仅是单线程执行,而且进行垃圾收集时必须暂停其它所有工作现场,直到它工作结束。缺点很明显,暂停工作非常的不好。优势是效率很高!
11.ParNew收集器:Serial收集器的多线程版本,可以多线程收集,其它和Serial收集一样。
12.Parallel Scavenge收集器:这是一个新生代收集器,使用复制算法,也是并行的多线程收集器。这样看来和ParNew收集器没啥区别嘛?其实,Parallel Scavenge收集器的特点是它关注点与其他收集器不同。CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器关注的目标则是达成一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间和CPU总消耗时间的比值。 即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%
13.Serial Old收集器:是Serial收集器的老年版本,同样是一个单线程收集器,使用标记-整理算法。
14.Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程的标记-整理算法。
15.CMS收集器:Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器。
CMS收集器的运行过程分为以下四个步骤:
初始标记(CMS initial mark)
并发标记(CMS  concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中,初始标记和重新标记这两个步骤仍然需要停止其他所有线程,即Stop The World。初始标记仅仅只是标记一下GC Roots能够关联到的对象,速度很快。并发标记阶段式进行GC Rooots Tracing的过程。而的重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变化的那一部分对象标记记录。

并发标记和并发清理时间最长,由于这两块是和用户线程并行执行,所以没有什么停顿。而初始标记和重新标记时间都很短,所以停顿也很短。于是这是一款优秀的收集器。

CMS还不完美,它有以下3个明显的缺点:(1)对CPU资源非常敏感,因为面向并发的设计对cpu资源比较敏感。解决--提供了一种称为“增量式并发收集器”,就是在并发标记、清理到时候让GC线程,用户线程交替运行,尽量减少GC线程的独占资源时间。现在已不提倡使用。
(2)CMS无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS在并发清理阶段用户线程还在运行着,伴随着程序执行而产生的垃圾就叫做浮动垃圾,这部分垃圾是不能被这次并发清理清理掉的,只能等到下一次CG时再清理掉。这时候新产生的垃圾可能会填满老年代空间,那么会触发“Concurrent Mode Failure”,这个时候就要临时启用“Serial Old”收集器进行老年代的垃圾收集,就会停留很长时间。因此CMS收集器应该在老年代空间没满时就启动垃圾收集,JDK1.6将这个值设置为92%。
(3)CMS是基于“标记-清除”算法的,可能会留下大量的空间碎片。CMS会设置一段时间进行一次内存整理。

16.G1收集器(Garbage First):是收集器的最新成果,是一款面向服务器端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替代CMS收集器。
与其他GC收集器相比,G1收集器具备如下特点:并行与并发;分代收集;空间整合;可预测的停顿。
G1对空间整合采用的方式是将堆内存整体划分为若干个大小相等的Region。
G1收集器之所以能够简历可预测的停顿时间模型,是一位它可以有计划的避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的手机时间,优先回收价值最大的Region(这也就是Garbage First的由来)。
一个问题

但是这也有问题,就是一个对象在一个Region中,他不可能仅仅只和这个Region内部的对象发生引用关系,而是和任何一个Region内部的对象都可能发生引用关系,那么如何去用可达性判定确定对象是否存活呢?G1收集器中,Region之间的对象引用以及其他收集器中的新生代老生代之间的对象引用,虚拟机都是使用Remembered_Set来避免全堆扫描的。G1中的每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,都会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中,如果是,便通过CardTable把相关的引用信息记录到被引用对象所属的Region的Remembered_Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

运作步骤

如果不计算维护Remembered Set的操作,G1收集器运作可大致划分为以下几个步骤

初始标记:该阶段仅仅是标记一下GC Roots所能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,该阶段单独执行,耗时很短

并发标记:并发标记阶段从GC Root开始进行可达性分析,找出存活的对象,耗时较长,并发执行。

最终标记:修正并发期间用户线程同时运行产生的变动,单独执行。虚拟机将这段变化记录到Rmembered Set Logs里面。最终标记阶段需要将Rmembered Set Logs的数据合并到Remembered Set中,这阶段可以并行进行(无用户进程)。

筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来指定回收计划。


垃圾回收器相关常用参数


内存分配与回收策略

17.对象优先在Eden分配。大多数情况下,对象优先在新生代Eden区中分配,如果没有空间分配时,将发起一次Minor GC(新生代GC)。举个例子,假设你分配20m堆内存空间,10m给新生代,10m给老年代。其中新生代中8m给Eden区,2m给另外的两个Survivor区(每个1m)。这个时候你要连续放入四个对象,大小分别是2m,2m,2m,4m。放完前三个到Eden区之后,第三个无法放入Eden,这个时候会触发Minor GC。由于这时候Eden去没有垃圾可以回收(一开始的3个2m都还活着),而这三个也没办法放入Survivor区内(大小不足),所以只好通过分配担保机制提前转移到老年代去。于是结果及时Eden区占用4m,survivor空闲,老年代占用6m。
老年代GC(Major GC/Full GC):只发生过在老年代的GC,出现了Major GC,经常会便随至少已多次的Minor GC(并非绝对)

18.大对象直接进入老年代:所谓的大对象是指连续内存空间的java对象。最典型的的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息,经常出现大对象容易导致内存空间不足,也就是说内存空间还有不少说不定就会触发垃圾回收。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。

19.长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经历过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设定为1.对象在Survivor区中,每经过一次GC,年龄就加1,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。

20.动态对象年龄判定:并不是一定要满足上述年龄才会进入老年代。如果在Survivor区域内的相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需达到MaxTenuringThreshold要求的年龄。

21.空间分配担保在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。 如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC ,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC  '
jdk6u24之后,HandlePromotionFailure设置将不再起作用。规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则就要进行Full GC


总结一下就是:新生的对象只要没超过大对象阈值,都会进入Eden区域。接下来可能发生以下情况(只要Minor GC发生,Eden区存活的对象都要进入Survivor.如果Survivor空间不足,会触发空间分配担保进入老年代。如果满足动态对象年龄判定,则满足的进入老年代;如果年龄到达,也会进入老年代):
只要发生某个对象引用删除操作(a=null),此时会发生Minor GC,回收这个对象,同时其他对象会进入Survivor(如果能容纳,就进去,如果不能容纳,通过空间分配担保进入老年区;如果满足动态对象年龄判定,则满足的进入老年代);
如果新进入Eden的对象空间不足,触发Minor GC,删除无用的对象,这时,其他对象都进入Survivorr(如果能容纳,就进去,如果不能容纳,通过空间分配担保进入老年区;如果满足动态对象年龄判定,则满足的进入老年代);
等等。
上述过程中,在发生MinorGC之前,都要进行检查。检查老年代最大可用连续空间是否大于新生代对象总空间。如果大于则执行Minor GC,如果小于则先进行Full GC进行空间收集(jdk6u24之后的内容)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值