JVM 收集算法
前面我们了解了整个堆内存实际是以分代收集机制为主,但还是没有讲到具体是怎么实现的,那么具体的过程到底是怎么样来实现的呢?我们来了解下
标记-清除算法
这个回收方法就是首先需要标记出需要回收的对象,然后再依次回收掉被标记的对象,或者是标记出所有不需要回收的对象,只回收未标记的对象
虽然此方法非常简单,但是缺点也是非常明显的,首先如果内存中存在大量的对象,那么可能就会存在大量的标记,并且大规模进行清除。并且一次标记清除之后,连续的内存空间可能会出现许许多多的空隙,碎片化会导致连续内存空间利用率降低.
标记-复制算法
我们标记算法就是将堆的区域分成两块大小相同放入区域,然后每次只会使用其中一块区域,每当到垃圾回收的时候,将需要回收的对象标记出来,之后将没有标记的复制到另外一边区域,最后将一次清空当前区域,虽然复制浪费了一些时间,但这样能够很好的解决对象大面积回收后造成的碎片化问题
这种算法就非常适用于新生代(因为新生代的回收效率极高,一般不会留下太多的对象)的垃圾回收,而我们之前所说的新生代Survivor区其实就是这个思路,包括8:1:1的比例也正是为了对标记复制算法进行优化而采取的。
标记-整理算法
上述我们提到了复制算法,此算法在新生区应用完全应用完全没有问题,但如果用在老年区就显得很鸡肋,因为老年区基本都是一些钉子户,它不像新生区那样每次回收都会腾出大量空间,对象,才有机会进入到老年代,所以老年代一般都是些钉子户,可能一次GC后,仍然存留很多对象。而标记复制算法会在GC后完整复制整个区域内容,并且会折损50%的区域,显然这并不适用于老年代。
那么我们能否这样,在标记所有待回收对象之后,不急着去进行回收操作,而是将所有待回收的对象整齐排列在一段内存空间中,而需要回收的对象全部往后丢,这样,前半部分的所有对象都是无需进行回收的,而后半部分直接一次性清除即可。
虽然这样能保证内存空间充分使用,并且也没有标记复制算法那么繁杂,但是缺点也是显而易见的,它的效率比前两者都低。甚至,由于需要修改对象在内存中的位置,此时程序必须要暂停才可以,在极端情况下,可能会导致整个程序发生停顿
所以,我们可以将标记清除算法和标记整理算法混合使用,在内存空间还不是很凌乱的时候,采用标记清除算法其实是没有多大问题的,当内存空间凌乱到一定程度后,我们可以进行一次标记整理算法。
JVM垃圾收集器
Serial收集器
该收集器是比较元老的一个收集器,在较早的jdk,是虚拟机新生代区域收集器的唯一选择,这是一款单线程的垃圾收集器,也就是说,当开始进行垃圾回收的时候,需要暂停所有的线程,直到垃圾收集工作结束,他的新生代收集算法采用的是标记复制法,老年代采用的是标记整理法
ParNew收集器
这款垃圾收集器相当于是Serial收集器的多线程版本,它能够支持多线程垃圾收集:
除了多线程支持以外,其他内容基本与Seria收集器一致,并且目前某些JVM默认的服务端模式新生代收集器就是使用的ParNew收集器。
Parallel Scavenge /Parallel Old收集器
Parallel Scavenge同样是一款面向新生代的垃圾收集器,同样采用标记复制算法实现,在JDK6时也推出了其老年代收集器Parallel Old,采用标记整理算法实现:
与ParNew收集器不同的是,它会自动衡量一个吞吐量,并根据吞吐量来决定每次垃圾回收的时间,这种自适应机制,能够很好地权衡当前机器的性能,根据性能选择最优方案。
目前JDK8采用的就是这种 Parallel Scavenge + Barallel Old的垃圾回收方案。
CMS收集器
在JDK1.5,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS (Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发(注意这里的并发和之前的并行是有区别的,并发可以理解为同时运行用户线程和GC线程,而并行可以理解为多条GC线程同时工作)收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
它主要采用标记清除算法:
Garbage First(G1)收集器
我们知道,我们的垃圾回收分为Minor GC 、Major GC和Full GC,它们分别对应的是新生代,老年代和整个堆内存的垃圾回收,而G1收集器巧妙地绕过了这些约定,它将整个Java堆划分成2048个大小相同的独立Region 块,每个Region块的大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且都为2的N次幂。所有的Region 大小相同,且在JVM的整个生命周期内不会发生改变。那么分出这些Region有什么意义呢?每一个Region都可以根据需要,自由决定扮演哪个角色(Eden、Survivor和老年代,收集器会根据对应的角色采用不同的回收策略。此外,G1收集器还存在一个Humongous区域,它专门用于存放大对象(一般认为大小超过了Region容量一半的对象为大对象)这样,新生代、老年代在物理上,不再是一个连续的内存区域,而是到处分布的。
它的回收过程与CMS大体类似:
初始化标记:标记出对象能够关联到的对象
并发标记:通过可达性分析,递归整个堆里的对象图,找出要回收的对象
最终标记:对用户线程做一个短暂的暂停,用于处理并发标记阶段漏标的那部分对象。
筛选回收:制定回收计划
元空间
在JDK8之后,我们堆里面就没有之前的永久代了,随之产生的是一个叫做元空间的东西,类的元信息被存储在元空间中,元空间没有使用堆内存,理论上系统可以使用的内存有多大,元空间就有多大,不会出现永久代存在时的内存溢出问题,永久代就被完完全全的抛弃了
引用
强引用
在Java中如果变量是一个对象类型,那么它实际上存放的是对象的引用,类似于Object o = new Object()这样的引用类型就是强引用
我们通过前面的学习可以明确,如果方法中存在这样的强引用类型,现在需要回收强引用所指向的对象,那么要么此方法运行结束,要么引用连接断开,否则被引用的对象是无法被判定为可回收的,因为我们说不定后面还要使用它。
所以,当JVM内存空间不足时,JVM宁愿抛出OutOfMlemoryError使程序异常终止,也不会靠随意回收具有强引用的“存活"对象来解决内存不足的问题。
强引用写法: Object o = new Object();
软引用
软引用不像强引用那样不可回收,但一旦JVM内存不足的时候,它会确保抛出异常之前,清理掉软引用指向的对象
软引用写法:SoftReference reference = new SoftReference<>(new Object());
弱引用
弱引用的生命周期比软引用的还要短,在进行垃圾回收的时候,不管当前内存是否充足,都会回收它的内存
弱引用写法
WeakReference reference = new WeakReference<>(new Object());
虚引用
随时可能被回收
也就是说我们无论调用多少次get()方法得到的永远都是null,因为虚引用本身就不算是个引用,相当于这个对象不存在任何引用,并且只能使用带队列的构造方法,以便对象被回收时接到通知。