文章分为三个部分,分别是:垃圾回收算法、分区回收、垃圾回收器。
1.方法区:保存已加载的类源信息、运行常量和字符串常量。
2.虚拟机栈:存放线程的运行方法。
3.本地方法栈:存放本地方法。
4.程序技术器:记录线程执行的字节码行号。
5.堆:存放对象数据,分为新生代和老年代。
垃圾回收一般指对对象的回收,详细讲一下堆。
由于初代标记复制算法对内存造成了一半的浪费,而且新生代的回收率接近百分之九十八,所以只需要留出百分之十的内存用于存储回收完毕后剩余的对象即可。
新生代将内存分为一个Eden(占比百分之八十),两个survive(分别占比百分之十),每次垃圾回收一个Eden和一个Survive区,将回收完毕之后剩余的对象复制到另一个Survive中。
由于老年代每次垃圾回收之后剩余的对象较多,而复制算法每次势必要复制大量的对象到另一个内存块内,是很不划算的。
老年代采用标记清除和标记整理算法。
由于标记清除算法会导致内存碎片化,无法存储大量连续的内容,所以需要和标记整理算法并行使用,当内存碎片化严重,则采用标记整理算法。
前置概念:
新生代:经历回收次数较少的对象,每次回收存活的对象较少,一般为百分之九十八。
老年代:经历回收次数较多的对象,每次回收存活的对象较多。
由于无法判断对象之间是否存在引用关系,这种算法被淘汰
标记所有要回收的对象,而后进行回收。
无法回收细小的碎片,导致内存区域碎片化,无法存储大文件。
将一半内存中回收之后剩余的对象复制到另一半内存中。
会造成内存的浪费。浪费的量为复制到的量。
由于新生代的对象绝大部分都会被回收,所以内存划分不按照一半一半分,而是分为一块占比80的Eden区和两块占比10的survive区。
发生垃圾收集时:将Eden和一块Servive中仍然存活的对象一次性复制到另一块Survive上,而后直接清理Eden和Survive中的对象。,这样内存的“浪费”比例只有百分之十,而没有百分之五十那么夸张。
如果发生极端情况,剩余的对象超过了百分之十,则通过老年代的区域进行临时储存。
让所有存活的对象都向内存的一端进行移动,而后直接清理边界之外的所有对象。
老年代一定会有大量的对象无法被回收,所以进行复制也是一项很繁重的任务,而且会导致stw,使得整个程序临时暂停。
腾出的区域是连续的大块内存区域。
老年代交替使用标记清除算法和标记整理算法,当内存碎片严重时,再使用标记整理算法。
标记清除算法的吞吐量更小但是回收速度更快,标记复制的吞吐量更大但是速度更慢。
jvm中的更关注吞吐量的ParallelScavenge收集器是基于标记整理算法的,而关注速度的CMS收集器则是基于标记清除算法的。
前置概念:
GCRoot:垃圾回收器作为遍历起点的引用,可以理解为遍历起始的根对象,这些对象一般认为是一定存活的。
STW:stop the world,世界停止,指停止用户进程,专门进行垃圾回收。
吞吐量:单位时间内回收的垃圾量,可能一次暂停数十秒来进行垃圾回收。
服务区间:
新生代
采用算法:
复制算法。
收集器特点:
单线程,内存占用小,依然是默认情况下的客户端垃圾回收器,没有线程之间的通信消耗。
服务区间:
新生代
收集器特点:
Serila的多线程版本,多了线程之间的通信消耗,一般和CMS配合使用。
服务区间:
新生代
特点:
不关注STW时间,只关注吞吐量的大小
服务区间:
老年代
采用算法:
标记整理算法
特点:
jdk5之前和Parallel Scavenge搭配使用,是服务器模式下CMS失效的后备预案。
老年代
JDK6之后
和Parallel Scavenge搭配使用,实现新生代和老年代的高吞吐量。
老年代
标记清除算法
1.初始标记:STW,标记GCRoot,检查关联的跟对象
2.并发标记:从GCRoot中遍历整个对象图,找到被关联引用的对象。
3.重新标记:STW,标记在并发标记时新产生的关联变换。
4.并发清除:和用户线程并发,不会产生Stw
对于高处理器核心数的处理器友好,一旦处理器的核心数下降,将会极大地占用cpu资源,当cpu的资源不超过四个时,将分出一半的资源去执行垃圾回收。为了解决这个问题,jdk尝试推出了和用户线程交替运行的icms,但是会拉长垃圾回收的时间,实际效果不佳,用户实际感觉速度下降了许多,jdk7之后,被完全抛弃。
浮动垃圾:垃圾回收过程中新产生的垃圾,下一次的垃圾回收才能够被回收,导致内存必须腾出一部分的空间来储存这些浮动垃圾,最开始jdk5当老年代的占用率达到了百分之六十八便开始垃圾回收。jdk6将这个比例提升到了百分之九十二,导致了另一种风险,一旦在cms回收过程中,内存被占满,jvm便冻结用户线程,让Serial Old来进行垃圾回收,这样垃圾回收的时间便长了。
新生代和老年代
jdk9之后,正式取代Parallel Scavenge和Parallel Old组合,正式成为全堆垃圾回收器
将垃圾接口进行了统一的管理
他将面对堆内的任何区域进行回收,不在根据具体是哪一个分代。
将整个内存区域分为若干个Region,将Java堆划分为多个大小相等的独立区域,每一个Region都可以扮演Eden或者Survive区域。
G1认为,只要是大于Region一半的对象就是大对象,大对象将会被优先回收,也就是优先级较高的区域回收方式。
占用内存较大,大约百分之二十。
由于记忆堆由于存储跨Region引用对象,每个Region都维护着自己的记忆集,记录着其他Region指向本Region的指针,本质上是一种哈希表,Key是Region的起始地址,Val是一个集合里面记录着卡表的索引号。
1.并发标记:
直接标记和GCroot直接关联到的对象,会停顿线程,但是耗时很短。
2.并发标记:
遍历整个GCRoot,测试其可达性,对所有的关联对象进进行标记,耗时较长,可以和用户线程并发执行,无需停顿。
3.最终标记:
整理在并发标记期间内发生变化的对象,补充标记,对用户线程做一个短暂的暂停。
4.筛选回收:
根据不同的Region回收时间,找出符合用户线程预期的回收区域进行回收,暂停用户线程,将存活的对象复制到新的Region中,完成回收。
回收器的选择:
CMS在小内存的情况下回收体验要好于G1,这个临界点大约是6GB-8GB.
垃圾回收器的三个指标:内存占用,暂停时间,吞吐量,三者是不可能三角。
对于现在的应用来说,我们更加关注的是垃圾回收器的暂停时间,越发的可以容忍回收器多占用一点内存。
一是因为现在的硬件扩充更加的容易
二是因为内存的增加,垃圾回收器的回收同样大小的内存时间会更加的长,所以延迟时间更加的被重视。