目录
三.垃圾收集器与内存分配策略
1.1 概述
1.2 对象已死吗?
1.2.1 引用计数法(RC Reference Counting)
1.2.2 可达性分析算法
1.2.3 引用
1.2.4 生存还是死亡
1.2.5 回收方法区
1.3 垃圾收集算法
1.3.1 标记 - 清除算法
1.3.2 复制算法
1.3.3 标记 - 整理算法
1.4 HotSpot的算法实现
1.4.1 枚举根节点
1.4.2 安全点
1.4.2 安全区域
1.5 垃圾收集器
1.5.1 Serial收集器
1.5.2 ParNew收集器
1.5.3 Parallel Scavenge 收集器
抛出问题
如何判断对象已经死亡:
原理:每个对象都有一个引用计数器,每当其他地方引用它时,计数器加一。引用失效时,计数器减一。
当计数器减到0,表示对象不再被引用。
缺点:当两个对象互相引用时,那么他们永远不会被GC回收。
优点:实现简单,判定效率高,但是主流的JVM中没有选用RC的算法
原理:一系列被称为GC Roots的对象作为起始点,当一个对象没有任何引用链连接GC Roots时,证明此对象不可用。
可以当做GC Roots对象的有:
------------- 最后结束修改于2019年01月 15日 22:18 待续... -------------
定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称为这块内存代表一个引用。
JDK1.2扩充:
将引用分为了 强引用,软引用,弱引用,虚引用。强度依次减弱。
强引用:
Object obj = new Object();
类似这种引用,只要强引用一直存在,垃圾收集器永远不会回收被引用的对象。
软引用:
软引用被用来描述还有用但是又非必须的对象,在系统将要发生内存溢出时,将会把这些对象列入回收范围。
弱引用:
被软引用指向的对象,不论下一次垃圾回收器收集时,内存足够与否,都会被回收。
虚引用:
最弱的一种引用,此引用不会对对象何时被垃圾回收影响,唯一的作用是被此引用指向的对象只会在被垃圾回收的时候返回一个通知。
即使可达性分析算法中不可达的对象,也并非一定会回收,真正宣告一个对象死亡至少需要经过两次标记。
第一次标记:
塞选条件是:是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法,或finalize()已经被虚拟机调用过时,没有必要执行finalize()方法
第二次标记:
补充:任何对象的finalize方法只会被系统自动调用一次。
方法区,也被HotSpot虚拟机中的GC机制形容为永久代。我认为应该是垃圾回收效率常常很低,因此叫永久代。
方法区主要回收两部分内容:
废弃常量和无用的类
废弃常量:
比如当没有任何一个String对象引用指向常量池的字面量时,此字面量在必要的情况下会被回收。
常量池中的其他类,接口,方法,字段的符号引用同理。
无用的类:
当必须同时满足一下三条时,可以算是无用的类。
类的 所有实例都被回收,堆中没有此类的实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
补充:
在大量使用反射,动态代理,CGLib···频繁自定义ClassLoader的场景都要求虚拟机具备类卸载的功能,以保证永久代不会溢出。
先想象一个网格,网格中随机出现需要收集的垃圾对象。
原理:首先标记出所有需要回收的对象,然后统一清楚被标记的对象。
优点:最基础的垃圾回收算法,有啥子优点?
缺点:标记与清除两种操作效率都不高,回收后产生大量空间碎片。当分配大对象时,没有足够的内存,会触发另一次的垃圾回收。
原理:通过将内存分为两块,每次只使用一块,当这一块内存使用完之后,会把还存活的对象搬到另一块内存,然后一次清空之前使用的那块内存。
优点:不用再考虑空间碎片的情况。另一半分配内存时,只需顺序分配内存,实现简单,运行高效。
缺点:只使用了一半的内存。
原理:标记过程,与标记-清除算法一样,但是之后做的是,把所有存活的对象都向一端移动,之后清楚掉边界以外的内存。意思就是把有用的对象放在一堆,然后直接清楚掉堆外的内存。
优点:没有了空间碎片 也没有浪费内存(人类总是这么喜欢追求极致不是?)
------------- 最后结束修改于2019年01月 23日 16:41 待续... -------------
什么叫枚举根节点呢?顾名思义,把根节点枚举出来。这样可以减少对根节点的判定!
在目前主流的Java虚拟机中,例如HotSpot中虚拟机采用OopMap这种数据结构来知道哪些地方存放这对象的引用,在类加载完成之后,Hotspot把对象内什么偏移量上是什么数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样在GC扫描时可以直接遍历map得知哪些引用还有效。完成GC Roots的枚举。
PS:枚举出根节点干嘛呢?因为GC Roots判断对象已死?需要用到根节点,然而这些根节点的选用,就需要被枚举出来。而不是每次读取判断哪些是roots(可以复习一下可达性分析算法)
安全点为的是保证在进行GC时,线程需要运行到一个安全点才能进行GC,这里我的理解是,随时停止一个线程的执行是有风险的,因此在停止一个或暂停一个线程时,需要设置一个安全点。
因此GC时需要所有的线程都运行到安全点。
如何实现呢?
一种是 抢先式中断:
GC时,首先中断所有线程,当有线程未到安全点时,唤醒响应线程,使之运行到安全点。
一种是 主动式中断:
不对线程做操作,仅设置一个标志,让各个线程执行时主动去轮询这个标志,标志为真时,自己就挂起,轮询标志的地方与安全点重合。
轮询标志实现原理:
虚拟机会把线程中执行的某个指令的内存页设置为不可读,当线程执行到此指令时,会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待,这样实现一条汇编指令完成安全点轮询和触发线程中断。
安全点有一个问题,当线程未被分配cpu时间片时,此线程无法响应JVM的中断请求,运行到安全点去中断挂起。
此时需要安全区域来解决此问题。
安全区域指的是,在某代码片段中,引用关系不会发生变化,因此在任何地方开始GC都是安全的。
实现原理:
当线程执行到Safe Region中的代码时,首先标识自己已经进入安全区域,因此JVM要GC时,就不需要考虑这些标识自己进入安全区域的线程了,当线程需要离开安全区域时,它需要检查系统是否已经完成了根节点的枚举或者GC的整个过程,如果完成,那么线程继续执行,否者继续等待收到可以安全离开安全区的信号为止。
PS:这里讨论JDK1.7之后HotSpot虚拟机的垃圾收集器
如图所示:
新生代使用的垃圾收集器有:Serial 收集器,ParNew 收集器,Parallel Scavenge 收集器,G1 收集器
老年代使用的垃圾收集器有:CMS 收集器,Serial Old 收集器,Parallel old 收集器,G1 收集器
互相连线的表示可以搭配使用。具体情况和场景有不同的搭配。
特点:
特点:
特点:
吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾回收时间)
停顿时间越短,越适合做交互类的程序,提高响应速度,提升用户体验,高吞吐量可以提高CPU利用时间,主要适合低交互的任务
特点:
特点:
特点:
初始标记:需要Stop The World,标记GC roots 能关联到的对象。速度很快。
并发标记:需要Stop The World,进行GC Roots Tracing。一个根节点遍历的过程。
重新标记:为了修正并发阶段用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。此过程花费时间比初始标记阶段稍长,远比并发标记时间短。
并发清除:垃圾清理线程开始清除无用对象。
1.对cpu资源非常的敏感。
原理:
与单CPU年代PC机操作系统使用的抢占式来模拟多任务机制的思想一样,就是在并发标记,清理的时候让GC线程,用户线程交替执行,减少GC线程独占系统资源的时间,整个垃圾收集过程会被拉长,但是减小对用户线程的影响,有点像当下分期购物一样。降低大出血的购物对当下的生活水平的影响。
实践证明 i-CMS 效果很一般。分期购物并不能减少还款的金额对吧。
2.CMS无法处理浮动的垃圾(Floating Garbage)。
3.既然是标记-清楚算法,那么就会有大量的碎片,导致分配大对象时如果没有足够的连续空间。会导致一次FullGC。
特点:
并行与并发:
- 充分利用多cpu,多核环境,来缩短STW的停顿时间。
- 其它收集器需要停顿的,G1依旧可以并发的使Java程序继续执行。
分代收集:
- G1可以独立的管理整个GC堆,但它能采用不同的方式处理新创建的对象和已经经过多次GC的旧对象。以获得更好的收集效果。
空间整合:
- G1与CMS的标记-清除不同,G1更像是标记-整理。从局部上(两个region)看又像是复制算法。
- 因此这两种算法都不会在G1运行期间产生内存空间碎片。收集之后都有规整的可用内存已被大对象使用
可预测的停顿:
- 这是与CMS相比更大的优势,降低停顿时间是G1与CMS共同的目标。
- G1还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎是实时JAVA的垃圾收集器的特征。
Remember Set:G1中,每个Region都有一个与之对应的Remember Set,虚拟机发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象)
然后通过CardTable把相关引用信息记录到被引用对象所属的Region的Remember Set之中。
当进行内存回收时,做可达性分析时,在GC根节点的枚举范围中加入Remember Set即可保证不对全堆扫描也不会有遗漏。
PS:我对于他的理解就像数据库建立索引一般,把索引放入Set中,枚举的时候在枚举中加入Set中的引用链,从而避免全堆扫描。
初始标记:
标记一下GC roots能直接关联到的对象。需要停顿线程,耗时短。
并发标记:
开始从GC root中对堆中的对象进行可达性分析。可与用户线程并发执行,耗时长。
最终标记:
为了修正 在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面。最后把Logs里的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。
筛选回收:
最后对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。