JVM的垃圾回收机制,在内存充足的情况下,除非你显式调用System.gc(),否则它不会进行垃圾回收;在内存不足的情况下,垃圾回收将自动运行
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器减1。当计数器为0的时候,对象就可以被回收。
缺点:存在循环引用的情况,导致2个循环引用对象的内存得不到释放。目前没有一个JVM的垃圾回收实现是使用这个算法的。
主流的Java虚拟机没有使用引用计数算法来管理内存,因为它很难解决循环引用的问题。
思路是:通过一系列“GC Roots”对象作为起点,从这些节点开始向下进行搜索,搜索所走过的路径被称为“引用链”。当一个对象到GC Roots没有任何引用链相连,也就是说从GC Roots到这个对象不可达,则证明此对象是不可用的。如图object5、object6、object7虽然互相关联,但是他们到GC Roots是不可达的,所以他们将被判断为可回收的对象。
(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)
在Java语言中,可作为GC Roots的对象包含下面几种:
即时在可达性分析法中不可达的对象,也并非是非死不可,要真正宣告一个对象的死亡,只要要经历2次标记的过程:
任何一个对象的finalize方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize方法不会被再次执行。尽量避免使用finalize方法,因为它只是为了使C/C++程序员更容易接收Java所作出的一个妥协,它的运行代价高昂,不确定性达,无法保证各个对象的调用顺序。
public class HYFinalize {
public static void main(String[] args) {
Book book = new Book(true);
book.checkIn();
// 每一本书都应该进行checkIn操作,从而释放内存。
// 这本书没有进行 checkIn操作,因此,没有执行清理操作(没有输出finalize execute)。也就是利用finalize方法进行终结验证,从而找出没有释放对象的内存。
new Book(true);
// 手动调用垃圾回收
System.gc();
}
}
class Book {
boolean checkOut;
public Book(boolean checkOut) {
this.checkOut = checkOut;
}
void checkIn() {
checkOut = false;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (checkOut) {
System.out.println("finalize execute");
}
}
}
Java虚拟机规范确实说过可以不在方法区中实现垃圾收集,方法区的垃圾收集效率也非常低,因为条件苛刻。
方法区(在HotSpot虚拟机中称为永久代)主要回收的内容有:废弃常量和无用的类。
对于废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串”abc“已经进入常量池中,但是当前系统没有任何一个String对象是叫做”abc“的,换句话说,已经没有任何String对象引用常量池中的”abc“常量,也没有其他地方引用了这个常量,如果这个时候发生内存回收,并且必要的话,这个”abc“常量就会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
对于无用的类则需要同时满足下面3个条件:
ClassLoader
已经被回收;java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。这里解释下为什么需要回收该类的ClassLoader?
public Class<?> getDeclaringClass() throws SecurityException {
final Class<?> candidate = getDeclaringClass0();
/*
* 反射里面使用到ClassLoader,因此要把ClassLoader干掉,才能保证没有地方可以通过反射调用到Class类。
* 然后当类的实例都会被回收了,并且该类没有在任何地方被引用到了,那么这个类就可以被回收了
*/
if (candidate != null)
candidate.checkPackageAccess(
ClassLoader.getClassLoader(Reflection.getCallerClass()), true);
return candidate;
}
可以通过虚拟机参数控制类是否被回收 -Xnoclassgc。
在大量使用反射、动态代理、GCLib等ByteCode框架、动态生成JSP 这类频繁自定义ClassLoader的场景,都需要虚拟机具备类卸载功能,以保证永久代不会溢出。
##常见的垃圾回收算法
算法分为标记、清除两个阶段:首先标记处所有需要回收的对象,在标记完成后,统一回收所有被标记的对象。
它的标记过程,使用的是可达性分析算法。
它是最基础的算法,因为后面的垃圾回收算法都是基于标记-清除算法进行改进。标记-清除也是最简单的算法。
实现简单
将可用内存划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还活着的对象复制到另外一块上,然后再把已使用过的内存空间一次性清理掉。
这样每次都是对整个半区进行回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
只是这种算法的代价将内存缩小为原来的一半,代价太高了。
现在商业虚拟机都采用这种方法来回收新生代,IBM公司研究表明,新生代中的对象98%都是朝生暮死的,所以并不需要按照1:1来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一个Survivor空间上,最后清理Eden和刚才使用过的Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存为整个新生代的90%,只有10%的内存会被浪费。
当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%内存大小的对象存活,当Survivor空间不够时,需要依赖其他内存进行分配担保。
分配担保:如果另外一块Survivor空间没有足够空间存放上一次新生代回收存活下来的对象时,这些对象将直接进入老年代。
复制收集算法在对象存活率较高时,就要进行较多的复制操作,导致效率变低。由于老年代存活率较高,所以一般不采用这种算法。
根据老年代的特点,有人提出了”标记-整理“算法,标记过程仍然使用”可达性分析算法“,然后让所有的存活对象向一端移动,然后直接清理掉端边界以外的内存。
只是根据对象的存活周期的不同把堆分成新生代和老年代(永久代指的是方法区),这样就可以根据各个年代的特点采用最适当的收集算法。
“分代收集”是目前大部分JVM的垃圾收集器所采用的算法。
在新生代中,每次垃圾收集都有大量对象死去,只有少了存活,那就采用复制算法,只需付出少量对象的复制成本就可以完成收集。
而老年代中对象存活率高,并且没有其他空间对它进行分配担保,就必须使用标记-清理 或者 标记-整理算法进行回收。
新生代垃圾回收流程
Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”法),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。
##HotSpot的GC算法实现
可作为GC Roots的节点主要在全局性的引用(例如常量或者静态属性)、执行上下文中(例如栈帧中的本地变量表),现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,找出GC Roots节点,那么必然会消耗很多的时间,
另外,可达性分析对执行时间的敏感还提现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里”一致性“的意思是整个系统看起来像被冻结在某个时间点上,不可以出现分析过程中引用关系还在变化的情况,该点不满足的话,分析结果的准确性将无法保证。这点是导致GC进行时必须停顿所有Java执行线程的其中一个重要原因。即时是在号称几乎不会停顿的CMS收集器中,枚举根节点时也是要停顿的。
准确式内存管理,又称为“准确式GC”。
虚拟机可以知道内存中某个位置的数据具体是什么类型。比如内存中有一个32位的整数123456,它到底是一个引用类型,指向123456的地址,还是一个数值为123456的整数,虚拟机将由能力辨别出来,这样子才能在GC的时候,准确判断堆上的数据是否还可能被使用。
由于使用准确式内存管理,Exact VM抛弃了基于handler的对象查找方式(原因是GC后对象可能被移动位置,比如对象的地址原本为123456,然后该对象被移动到654321的地址,在没有明确信息表明内存中的哪些数据是引用的前提下,虚拟机是不敢把内存中所有123456的值改为654321的,因为不知道这个值是整数还是指向另外一块内存的地址,因此有些虚拟机使用句柄来保持引用的稳定),通过准确式内存管理,能快速判断该数据是否引用,就可以避免使用句柄,从而减少一次查找地址的开销,提高执行性能。
由于目前主流的Java虚拟机都是采用准确式GC,所以当执行系统停顿下来后,并不需要一个不漏的检查完执行上下文和全局的引用位置,虚拟机应当有办法直接指导哪些地方存放着对象引用。
在HotSpot实现中,使用一组称为OopMap的数据结构来达到这个目的。
在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定位置记录下栈和寄存器中哪些位置是引用。这样子,在GC扫描的时候,就可以直接知道哪些是可达对象了。
在OopMap的协助下,HotSpot可以快速准确完成GC Roots枚举。可能导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那么将会需要大量的额外空间,这样GC的成本将会变得很高。
实际上,HotSpot也没有为所有指令生成OopMap,只有在特定位置生成这些信息,这个位置称为“安全点”。
程序在执行过程中,并非在所有地方都可以停顿下来进行GC,只有在到达安全点时才能暂停。安全点的选定既不能太以至于让GC等待太长的时间,也不能过多以至于增大运行时的负荷。所以安全点的选定是以“是否让程序长时间运行”为标准进行选定的。长时间运行最明显的特征是指令复用,比如说方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生安全点。
对于安全点,另外一个需要考虑的问题,是如何让所有线程跑到最近的安全点再停顿下来,这里有2种方案可供选择:抢占式中断、主动式中断。
抢占式中断不需要线程的执行代码主动去配合。
在GC发生时,首先把所有线程中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。
现在几乎没有虚拟机采用抢占式中断来暂停线程。个人觉得是太粗暴了,比如直接中断线程。
主动中断的思想是:当GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标记为真就自己中断挂起。轮询标记的位置就是安全点的位置。
使用安全点是否已经完美解决什么时候进入GC的问题。但是假如程序不执行呢?所以的程序不执行就是没有分配CPU时间片,最典型的就是线程处于sleep或者阻塞状态,这时候线程无法执行到安全点,并且响应中断挂起。JVM也不太可能等待线程重新获得CPU时间片,这时候就需要安全区域来解决。
安全区域指在一段代码片段中,引用关系不会发生变化。这个区域任务地方开始GC都是安全的。我们可以把安全区域看做是扩展的安全点。
在线程执行到安全区域时,首先标识自己已经进入安全区域了,那样,当这段时间内发生GC时,就不用管那些标识为安全区域状态的线程了。
在线程要离开安全区域时,它首先检查系统是否已经完成根节点枚举,如果完成,线程就继续执行,否则,它就继续等待直到收到可以离开安全区域的信号。
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
上图展示了不同分代的垃圾收集器,如果两个收集器之间存在连线,那么说明它们可以搭配使用。
垃圾收集器所处的区域,则表明它是新生代收集器,还是老年代收集器。
Serial收集器是最基本,发展历史最悠久的收集器。Serial是一个单线程收集器。是新生代收集器。
Serial收集器在进行垃圾收集的时候,必须暂停其他所有的线程,直到它收集结束。“Stop the World”暂停线程 这个工作是后台自动发起和完成的,在用户不可见的情况下把用户正常工作的线程停掉,这对于很多应用来说是很难接受的。假如你的计算机每运行1个小时就要停顿5分钟,你会有怎样的心情?下图展示了Serial收集器的运行过程:
“Stop the World”是没有办法避免的,举个简单例子:你妈妈在打扫房间的时候,你还一遍扔垃圾,这怎么打扫的完?。目前之间尽量减少停顿线程的时间。
serial收集器仍然是虚拟机运行在client模式下的默认新生代垃圾收集器。它也有由于其他收集器的地方:简单而高效。对于单CPU的环境来说,Serial收集器由于线程交互的开销,专心做垃圾收集,自然可以获得最好的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般不会很大,收集几十兆甚至一两百兆的新生代,停顿时间可以控制在几十毫秒甚至一百毫秒以内,只要不是频繁发生,还是可以接受的。
ParNew收集器其实就是Serial收集器的多线程版本。除了多线程进行垃圾收集之外,其他都和Serial一样。
是新生代收集器。
ParNew收集器的工作过程如图:
ParNew收集器是许多运行在Server模式下的虚拟机首选的新生代收集器。因为除了Serial收集器外,只有它能和CMS收集器配合工作。
Parallel Scavenge是新生代收集器。它也是使用复制算法的收集器,又是并行的多线程收集器。看上去了ParNew一样,那么它有什么特别之处呢?
Parallel Scavenge是为了达到一个可控制的吞吐量。吞吐量=运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集的时间)。高吞吐量表明CPU时间被有效的利用,尽快完成程序的运算任务。
Parallel Scavenge收集器提供了参数控制最大垃圾收集停顿时间,虚拟机将尽可能保证垃圾回收的时间不超过该值。不过大家不要任务把这个参数的值设小一点就可以使垃圾收集速度加快,GC停顿时间缩短,是以牺牲吞吐量和新生代空间来换取的,系统会把新生代调小一些,收集300MB的新生代肯定比收集500MB的快,但这也导致垃圾收集更频繁一些。原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间是下降了,但是系统吞吐量下来了。
由于和吞吐量关系密切,Parallel Scavenge也被称为吞吐量优先收集器。
Parallel Scavenge还有一个参数,这个参数打开以后,就不需要手工指定新生代大小、Eden和Survivor比例等参数,虚拟机会根据运行情况,动态调整这些参数,已提供最适合的停顿时间,这种调节方式成为GC自适应调节策略。自适应调节策略也是Parallel Scavenge收集器和ParNew收集器的一个重要区别。
Parallel Scavenge无法和CMS配合工作。
Serial Old是Serial收集器的老年代版本。它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义是给Client模式下的虚拟机器使用,工作过程如下:
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
这个收集器是JDK1.6之后才开始提供的,在此之前,Parallel Scavenge收集器一直处于比较尴尬的位置,因为如果新生代选择了Parallel Scanvenge收集器,老年代除了Serial Old收集器之外别无选择。老年代Serial Old收集器在服务端的拖累,使用Parallel Scavenge收集器未必能在整体应用上获得吞吐量最大化的效果。由于单线程的老年代收集,无法充分利用服务端多CPU的能力,在老年代很大而且硬件比较高级的环境,这种组合的吞吐量甚至还不如ParNew + CMS组合给力。
直到Parallel Old收集器出现后,Parallel Scavenge才有了比较名副其实的应用组合。在注重吞吐量与多CPU的场景,可以优先考虑Parallel Scavenge 和 Parallel Old收集器。Parall Old工作状态如图:
CMS是一种以获取最短停顿时间为目标的收集器。互联网应用就非常注重服务器的响应速度,希望系统停顿时间最短,已给用户带来最好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器基于标记-清除算法实现的。它的运作过程分为4个步骤:初始标记、并发标记、重新标记、并发清除。
其中,初始标记、重新标记两个步骤仍然需要暂停用户线程。
初始标记仅仅是标记一下GC Roots能够直接关联的对象,速度很快。
并发标记就是进行GC Roots 向下查找过程,也就是从GC Roots开始,对堆中对象进行可达性分析。这时候用户线程还可以继续执行。
重新标记阶段是为了修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象标记记录。
并发清除是GC垃圾收集线程 和 用户线程并行的,清理被回收的对象。
由于整个过程中耗时最长的并发标记和并发清除的阶段收集器都可以和用户线程并行工作,所以总体上来说,CMS收集器的内存回收是与用户线程一起并发执行的
减少了GC停顿时间
G1收集器是当前收集器发展最前沿的成果之一。G1收集器是一款面向服务端应用的垃圾收集器。Hotspot团队希望G1收集器未来能替换掉CMS收集器。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。G1将堆分成许多大小相同的区域单元,每个单元称为Region。Region是一块地址连续的内存空间,G1模块的组成如下图所示:
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region。
G1收集器具备如下特点:
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免对整个堆进行垃圾收集。G1维护了一份优先列表,每次根据允许的收集时间,优先回收价值最大的region。(怎样才是价值大?region采用复制算法,那么如果一个region中垃圾很多,存活对象很少,那么这个迁移存活对象的工作就很少,并且收集完之后,能够得到的内存空间很多,这种就是价值大的region)。
这种使用region划分内存空间,并且有优先级的区域回收方式,保证G1收集器在有限的时间内,可以获取尽可能高的效率。
一个对象分配在某个Region中,但是它可以与整个Java堆中任意对象发生引用关系。那么做可达性分析法判断对象是否存活的时候,岂不是扫码怎么java堆才能确保准确性?这个问题其实并非G1才有,只是G1更加突出而已。如果回收新生代不得不扫描老年代的话,那么Minor GC的 效率可能下降不少。
在G1收集器中,Region之间的引用 和 其他收集器中新生代和老年代之间的引用,虚拟机都是使用Remembered Set来避免全堆扫描。在G1中,每一个Region都有一个Remembered Set。当对引用进行写操作的时候,G1检查该引用的对象是否在别的region中,是的话,则通过CardTable把相关引用信息存到被引用对象的Remembered Set中。当进行内存回收时,把RememberSet加入到GC Roots根节点的枚举范围。这样就可以保证不全堆扫描也不会有遗漏。
如果不计算Remembered Set的操作,G1收集器的运作大致分为如下操作:初始标记、并发标记、最终标记、筛选回收。
对象的分配,主要在新生代的Eden区上,如果启动了本地线程分配缓存,那么将优先在TLAB上分配。少数情况下,也可能直接分配在老年代中。
对象优先在Eden区分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代
大对象是指需要大量连续内存空间的对象。最典型的就是很长的数组或者字符串。经常出现大对象,就容易导致内存还有不少空间的时候,就触发GC来获取足够的连续内存空间来放置这些大对象。
长期存活对象将进入老年代
虚拟机怎么识别哪些对象应该存放到新生代,哪些对象应该存放到老年代?为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器(放在对象头的Mark World中)。
如果对象在Eden区出生,经历了第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间,然后年龄计数器设置为1。
对象在Survivor中每熬过一次Minor GC,年龄就增加1岁。当它的年龄到达一定程度(默认是15岁),就会被移动到老年代。
动态年龄判断
如果Survivor中相同年龄的对象大小总和 大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代。无需等到阈值(比如15岁)。
新生代使用复制算法,但是为了保证内存利用率,因此只用其中一块Survivor区保存存活对象。因此当发生大量对象在Minor GC后仍然存活的情况,就需要老年代进行分配担保,就是指把Survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象能够存活下来,在实际内存回收完成之前是无法知道的,只好取之前成功晋升老年代的对象容量大小的平均值,与老年代剩余空间进行比较。决定是否进行Full GC以腾出更多的空间。
具体操作为:
在进行Minor GC之前,虚拟机将会检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,则认为Minor GC是安全的(老年代可以担保成功)。
如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,将检查老年代最大可用连续空间是否 大于 之前成功晋升老年代的对象容量大小的平均值。如果大于,则尝试进行一次Minor GC。如果小于,或者HandlePromotionFailure设置为不允许担保失败,则进行一次Full GC。
HandlePromotionFailure一般打开,避免频繁Full GC。
在JDK6之后,已经不再使用HandlePromotionFailure这个参数了,JDK6之后的规则变为:
Minor GC又称为新生代GC。指发生在新生代的垃圾收集动作。因为Java对象大多很快死亡,所以Minor GC非常频繁,一般回收速度也比较快。
又称为老年代GC。指发生在老年代的GC。出现了Major GC,经常伴随着一次Minor GC。Major GC一般速度比Minor GC慢10倍以上。
除了Java堆和永久代之外,还有一些区域会占用比较多的内存,这里所有内存总和受到操作系统进程最大内存的限制。
为什么要划分成年轻代和老年代?
为了针对不同的内存区域采用不同垃圾收集算法,从而提高效率
年轻代为什么被划分成eden、survivor区域?
通过划分eden、survivor区,能够提高年轻代的内存使用率。因为年轻代的大部分对象都会很快死去,因此只需要使用少部分的内存来保留存活对象。
老年代都是什么对象?
老年代都是活的比较久的对象。可能为年轻代的对象经过多次GC后存活然后放入老年代,也可能是大对象直接在老年代分配内存空间
https://blog.csdn.net/mccand1234/article/details/52078645
重点: https://www.cnblogs.com/aspirant/p/8662690.html、https://www.cnblogs.com/aspirant/category/1195271.html
https://www.cnblogs.com/1024Community/p/honery.html#25-%E6%96%B9%E6%B3%95%E5%8C%BA%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6
[https://www.cnblogs.com/heyonggang/p/11718170.html#g1%E6%94%B6%E9%9B%86%E5%99%A8