前提
本文分析的是hotspot虚拟机,实际上还有好多虚拟机,虽然好多地方都大同小异,但也有些不同点。
可以看看《Java虚拟机中内存模型简述》了解啥是堆内存
GC主要针对的是堆内存,Java 堆内存分为新生代和老年代,新生代中又分为1个 Eden 区域 和两个 Survivor 区域,新加入的对象一般放在Eden 区(大对象直接放在老年代),进行一次垃圾回收后没有被清理的对象放入Survivor ,再次回收还没被收则进行观察年龄够数就放入老年代,研究表明新生待对象大部分都是很快就没用得需要直接清理,所以分配空间比一般比较大比如E:S=8:1左右这样子。
回收前先看看对象如何分配内存
当一个对象需要分配空间时,新创建的对象除非是大对象否则直接放在eden空间里,虚拟机分配空间相当划出一块确定大小的内存出来给对象用,假如空间是绝对规整的连续的,所有用过的空间放一边,没用过的放一边,中间放着一个指针,当分配内存时将指针移动向空闲的一边与对象大小等距即可,这种方法叫做指针碰撞法,如果内存不规整不连续的情况就不可以用,这时候虚拟机必须维护一个列表,列表上记录哪些内存可用,分配空间时从列表中找到一块足够大小的空间划分给对象即可,这种方法叫做空闲列表,采用哪种方法由内存是否规整决定。
问题又来了,当一个正在给一个线程分配空间时,指针还没来得及移动另一个线程又使用指针进行内存分配岂不是很乱,而且如果每次分配内存都同步的话岂不是很慢,想想多线程时每次创建一个对象都要等其他对象分配完再分配,体验不好。所以虚拟机采用了一种策略,为每一个线程预先分配一小块内存(称为本地线程分配缓冲)当线程需要分配空间时先分配缓冲上的内存,当这小块内存用完了才需要同步的再分配一笑块给线程,这样效率就高很多了。
-
穿插内容:
对象的内存分布
分为对象头和实例数据和填充三部分
分为对象头放些什么类指针,hashcode,年龄信息,锁信息GC标记啥的东西
实例数据就是对象里的数据,保存的其实就只有包括继承过来的所有属性值,同个类不同对象的区别就是属性值了。
填充就是hotspot虚拟机为了方便管理要求对象的起始地址必须是8字节的整数倍,也就是说对象大小必须为8字节的整数倍,当不够时需要填充。
如何判断是否是垃圾?
- 引用计数算法
一个对象,如果被引用则计数+1,引用失效计数-1,当计数为0时就是垃圾。简单但是有一个致命的缺点,当两个对象相关引用时就不会被回收。 - 可行性分析算法
这个方法通过一系列的GC roots节点向下搜索,一直搜索,能到达的对象即为有用的对象还被引用的对象不应该回收,不可以到达的对象为不可达对象即为没有东西引用了没用了,判断为垃圾需要回收。hotspot用的就是这个算法
关键是这个GC roots节点怎么选取呢?选好不好关乎到回收效率
这个设计师们考虑的一下情况可以作为GC roots节点
1、虚拟机中栈的引用对象
2、方法区类静态引用的对象
3、方法区类常量引用的对象
4、本地方法栈中JNI引用的对象 - 这里提一下引用类型
对象只有被引用和不被引用两个状态吗?不太狭隘了,有些对象还有可回收也可以不会收状态,可能后面还有用,但是也可以先回收。
所以虚拟机允许对引用扩展分为四类,都有相应的类进行定义
强引用-——通常用的引用只要还有强引用就不会被回收
软引用——当空间不紧张时不回收,当空间紧张时对这些引用的对象进行二次回收
弱引用——比软引用更弱,当第一次GC时如果空间不紧张时不回收,但是第二次GC时无论紧张不紧张都回收。
虚引用——虚引用的存在不会对生命周期有影响,也不可以通过虚引用访问对象,存在的唯一目的是当虚引用指向的对象被回收时收到一个系统通知。 - 最后
Object还有一个finalize()方法可以在要被回收时救命一下,不过这个方法只是为了使C/C++程序员更加容易接受引入的,官方强烈建议别用,因为开销大,而且没必要。所以当做没学最好。
怎么收集垃圾?
垃圾即为死了的对象,活着即不为垃圾
- 标记-清除
和名字一样,先标记出需要删除的内存,之后统一进行删除,简单但是会出现很多碎片内存,当需要连续内存时看着还有内存确不能分配。
- 复制算法
这个算法用在hotspot中的新生代里面,就是把内存划分为两块,每次只用一块,当用完一块时将不需要清理的对象复制到另外一块去,然后把已经使用的统一清理完,这种方法保证剩余内存的连续,不过复制代价高,但在不需熬清理的对象比较少时很有用,比如新生代里。
- 标记-整理
复制算法不能有效解决存活率很高的情况,这个算法就适合改进了标记清除算法,不过后面不是直接清除而是先移动,先标记需要清理的内存,将不需要清理的对象移动到一边,再进行清理标记部分,这种情况可以保证内存的连续,但效率低,不过存活率高就是清理的少的情况很适合。这种情况合适老年代的回收。hotspot老年代用这种了
- 分代回收
看到上面的算法应该就明白为啥要把堆内存分成新生代和老年代了,复制算法适合存活率比较低的情况,只用复制一点就可以,存活率越高越不适合,标记-整理整理可以弥补这个。如何让存活率低呢?好用复制算法,若有一些一直存活岂不是每次GC都要进行扫描,所以用分代解决,当存活几趟GC后放到老年代,用其他条件GC,减少扫描次数,这部分应该用的标记-整理整理进行回收,因为这部分存活率高。
GC在什么时候?
总的来说当内存不足时出发垃圾回收,具体如何判定空间不足如下
GC又分为 minor GC(发生在新生代的GC) 和 Full GC (也称为 Major GC )(发生在老年代的GC,通常比minor GC慢而且通常会伴随一次minor GC)
-
minorGC的触发条件
1、Eden区域满了,或者新创建的对象大小 > Eden所剩空间
2、CMS设置了CMSScavengeBeforeRemark参数,这样在CMS的Remark之前会先做一次Minor GC来清理新生代,加速之后的Remark的速度
3、Full GC的时候会先触发Minor GC -
fullGC的触发条件
1、Minor GC后发生的担保失败(担保待会说)
2、大对象直接进入老年代时,如果剩下空间小于大对象则触发GC -
说一下担保
担保就是用老年代的空间进行新生代的GC的担保,保证新生代GC能顺利进行,当要发生minorGC时先检查老年代连续的空间是否大于新生代所有对象的总和(用的标记整理,剩余的空间保证连续),是则担保成功继续minorGC,否则担保失败,则继续判断老年代连续可用空间是否大于历次(以往的统计得来得)晋升至老年代得平均空间,若大于则会继续minorGC,若小于则进行fullGC。
为什么要这样子担保呢?
因为新生代用的复制算法(专门研究表明新生代大多对象都是活不久的需要直接清理的所以只需要付出少量的代价复制少量的存活的对象到一边去即Survivor去让你后直接清理就可以所以复制算法最优),虽然说理论上可以这样子复制,但是实际上有极端存在,比如GC时所有的对象都是存活的,这种情况虽然少见但是还是有可能(像贷款大部分情况会还,但也不是不可能不还所以加个担保人不还时就给担保人还否则没办法就错误),这给老年代就相当于担保人,当极端时新生代复制到Survivor装不下时直接进入老年代
为什么处于Survivor区的对象年龄够了或者满足动态对象年龄判定时,进入老年代不需要判断空间和fullGC 呢?
因为上面担保操作保证了这部分空间肯定够的。
只要空间不够随时都可以GC?
-
背景
在空间不够的时候虚拟机触发GC还需要等到所有线程都处于安全点或者安全区域才可以进行GC。
那么什么是安全点和安全区域呢?
虚拟机用的可达性分析算法,需要知道GC roots节点才可以进行引用链的搜索,GC roots节点即为满足条件的引用(见上面的可行性分析算法),由于编译后并不会保存变量属于什么类型,所以要找出GC roots节点必须遍历所有数据一个一个找,非常消耗时间,而且可行性分析里必须保证一致性也就是说当找出来的GC roots节点的过程中应该是某个冻结的时间点上,不然分析过程引用还不断的变化,就不能保证准确性(像边打扫边丢垃圾一样),所以当找GC roots节点时所有线程应该停顿(stop the world),而遍历所有内存需要太多时间所以会导致停顿太久一点也不利于体验,所以hotspot虚拟机(其他虚拟机也有类似的不过不叫这个名字)设计师们通过一个叫oopMap的特殊数据结构来记录GC roots节点的地址偏移量,这样当GC时就只用遍历oopMap就可以不用遍历整个内存,这个oopMap里的数据是在类加载过程中或者在编译时计算得到的,可以把oopMap简单理解成是调试信息。在源代码里面每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。oopMap就是一个附加的信息,告诉你栈上哪个位置本来是个什么东西。 这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。 -
简言之 oopMap就是用来记录记录GC roots节点位置的
又因为不可能每条指令都要有一个oopMap这样子开销也很大,所以安全点就出现了。safepoint把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。 循环中引用多个对象,肯定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。《专门讲oopMap的这篇不错》
-
安全点简言之就是程序中某一个固定的地方,当运行到该点时,进行oopMap的修改更新记录
所以当要进行GC时,得先让oopMap准备好才能进行GC吧,不然GC roots节点不完整GC就效率不高,所以要等所有线程都处于安全点时才能够进行GC,当然不是所有的线程都会同时到达安全点,所以当虚拟机触发GC时,先所有的线程都进入安全点才能停顿线程进行GC,已经到达的线程需要等待其他的线程也到达才能同时进行GC,所以这安全点不能过于密也不能过于散,过密则会引起CPU负担(oopMap更新这个太多耗时)过散会引起不必要的等待,当GC触发时其他线程都到了安全点会等待没到的线程,干等太久会导致停顿太久所有也会让GC等待过久。那问题来了当线程处于等待啥的没有获取CPU资源时岂不是无法进入安全点触发GC也无法执行,所以把这种线程处于等待啥的,引用不会发生变化时GC都可以执行这种情况称为安全区域。
-
简言之安全区域就是引用不会发生变化的部分(线程等待啥的),这个区域只要虚拟机触发GC随时可以执行GC不需要等到安全点。
什么时候会进入老年代呢?
-
大对象直接进入老年代
虚拟机机提供一个参数设置一定大小的对象直接进入老年代,典型的大对象就是长长的字符串或者数组等,大对象需要很多连续的空间,让这些大对象直接进入老年代的目的是这样做可以防止大对象在ehen和Survivor之间复制过多消耗过大,而且经常出现大对象很容易出现出现内存不足的情况,所以直接放老年代去。 -
长期存活的对象进入老年代
如果对象经过一次GC后仍然存活将会被移动到Survivor区并且年龄+1,每次GC存活者年龄+1,当年龄到一定的值后将会进入老年代,这个年龄上限可以通过虚拟机设置。 -
也不一定全部要到年龄了才可以进入老年代
这样当存活率比较高的情况下,如果不及时移到老年代会在新生代GC时复制过多,这是不希望看到的,所以当Survivor空间上相同的年龄的对象内存超过Survivor空间的一半时大于或者等于该年龄的对象直接进入老年代,不需要等到年龄大大阈值。 -
就是担保的情况,见上面的担保介绍