学习JVM(垃圾收集和内存分配)
虽然Java的垃圾回收机制已经十分优秀,但是为了出现问题时,调试优化更容易,这里继续学习垃圾收集器和相关内存分配。
由于程序计数器、虚拟机栈、本地方法栈生命周期岁线程变化,因此是类结构确定下来时就已知的。因此这几个区域的内存分派和回收都是确定的,不需要过多的考虑回收问题。
一、如何判断对象死亡
1、 引用计数算法
当有一个地方引用该对象,计数器+1,当引用失效,计数器-1。任何时刻计数器为0的对象不再可用。
无法解决对象间相互引用的问题,
2、 可达性分析
通过一系列称为GCRoots的对象作为起始点,从这些节点开始向下搜索。所走过的路成为引用链。当一个对象到GCRoots没有任何引用链相连时,则称为不可达。
GC Roots:
1)虚拟机栈引用的对象
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈JNI引用的对象
3、四种引用模式
1) 强引用:只要强引用存在,则永远不会回收。
2) 软引用:GC开始工作时,不一定会回收。在即将出现OOM时,将这类对象回收,如果内存还是不够,则抛出OOM
3) 弱引用:只要当GC开始工作时,弱引用对象就会被回收。
4) 虚引用:虚引用唯一的目的是在对象被GC回收时收到一个系统通知。
4、如何判断对象死亡
5、回收方法区
只有同时满足以下三个条件才可以被回收
1) 该类的实例全部被回收
2) 加载该类的ClassLoader被回收
3) Class对象没有在任何地方被引用。
在大量使用反射、动态代理的场景可能涉及到类卸载的场景。
二、垃圾回收算法
1、 标记-清除算法
标记出所有需要回收的对象,标记完成后统一回收。
缺点:效率不高,会产生大量不连续的内存碎片
2、 复制算法
将可用内存分为两半,每次只使用一块,当这块内存用完了,将还存活的对象复制到另一半,然后将这块内存所有对象清空。
缺点:只使用一般内存,代价比较高
常用于新生代回收。内存分为Eden和两块Survivor。回收时将Eden和Survivor对象复制到另外一块Survivor上。然后清楚这边Eden和Survivor。一般比例为Eden:Survivor=8:1.
3、 标记-整理算法
标记对象,将所有存活的对象移向一段,然后清理掉边界外的内存。
4、 分代算法
根据对象存活周期不同分为几块,不同分区采用不同的垃圾回收算法。
三、如何实现以上算法
1、 枚举GC Roots
如果每次都需要重新搜索有哪些GC ROOTS ,如何建立引用链,那么无疑会消耗太多无意义时间。因此在HotSpot中,使用OopMap记录GC ROOTS。在类加载完成时,HotSpot就把对象相关信息记录下来,在JIT编译时也会记录哪些位置是引用。所以当GC扫描时就可以直接获得这些信息。
2、 安全点:
不能针对每条指令都生成对应的OopMap,所以只在特定的位置记录这些信息,称为安全点。
如何让GC发生时所有线程都在安全点停顿记录OopMap呢:
抢先时中断:GC发生时,将所有线程中断,如果不在安全点,则让它跑到安全点
主动式中断:设置标志位,线程轮询,如果标志为真,则中断挂起进入安全点
四、垃圾收集器
1、 Serial
后台自动发起GC,暂停所有其他的工作线程,直到工作结束。
简单高效,但是如果GC时间过程,会给用户造成停顿时间过程,交互效果不好的影响
2、 ParNew
Serial的多线程版,同样暂停用户线程,但是GC可以并行执行垃圾回收操作
3、 Parrel Scavenge
关注于控制吞吐量的问题:CPU用于工作在用户代码时间和CPU总时间的比率。
适用于后台计算的任务,充分利用CPU的工作。
设置最大垃圾收集停顿时间和吞吐量大小参数。如果停顿时间设置过小,导致新生代内存分配小,则GC发生的更加频繁,导致吞吐量的下降。
4、 Serial Old
单线程收集器,Serial的老年版。
5、 Parallel Old
Parrel Scavenge的老年版,关注于吞吐量。
6、 CMS
获得最短回收停顿时间为目标的垃圾收集器。
清除过程:
初始标记:关闭其他线程,标记GC Roots能直接关联到的对象。
并发标记:并发进行,进行GC Roots Tracing的过程。
重新标记:关闭其他线程,修正并发过程中由于变动的对象标记记录。
并发清除:可以跟其他用户线程一起执行。
缺点:
1) 并发操作占CPU资源。
2) 需要预留内存给并发时产生的垃圾。
3) 由于基于标记-清除,产生大量内存碎片。
7、 G1
优点:
1) 并行和并发
2) 分代收集
3) 空间整合,采用标记-整理。
4) 可预测的停顿
首先G1的面向对象是整个内存区域,而不是新生代或者老年代。它将Java堆分为多个大小相等的独立区域,跟踪每个区域的垃圾堆积的价值大小。在后台维护一个优先列表。每次回收回收价值最大的区域。
运行过程:
1)初始标记:停顿其他线程,标记GC Roots能关联到的对象
2)并发标记:并发执行,利用可达性找到存活的对象。
3)最终标记:停顿其他线程,修正并发期间更改的记录。
4)筛选回收:更具回收价值,筛选回收区域,
五、内存分配和回收策略
1、对象优先在Eden分配
当Eden没有足够空间分配时,发起一次Minor GC。
2、大对象直接进入老年代
设定一个-XX:PretenureSizeThreshold参数,大于这个参数的对象直接进入老年代
3、长期存活的对象进入老年代
对象在Eden经历一次Minor GC并被移入Survivor区时,设置对象年龄为1,此后每经历一次MinorGC,年龄加一。设置-XX:MaxTenuringThreshold,年龄超过这个值进入老年代。
4 动态年龄对象判定
设置如果对象年龄达到某个值的对象占比超过50%,则移入老年代。
5 空间分配担保
发生Minor GC之前,虚拟机检查老年代最大可用连续空间是否大于新生代所有对象的空间,如果是,这次Minor GC是安全的。
如果不成立,看看是否允许担保失败,如果允许,查看老年代最大可用连续空间是否大于是否大于晋升到老年代对象的平均大小,如果大于,尝试进行Minor GC。如果小于或者不允许冒险,则进行Full GC。
关于冒险的解释:由于新生代采用复制算法,在极端条件下,另一个Survivor的大小不足以保存活下来的对象,这时存放在老年代里。如果老年代空间还不够,则需要发生一次Full GC。而老年代的冒险就是为新生代的存活对象担保存放对象。