内存管理机制中讲述了java运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。而java堆和方法区则不一样,这个部分的内存的分配和回收都是动态的,垃圾收集器所关注的是这部分的内存。在堆中,垃圾收集器的回收率比较高,尤其是新生代,一次大约可以回收70%到95%的空间。而方法区(永久代)的回收效率远低于此。
一、怎么判断对象是无用的对象?垃圾收集器主要对被判定无用的对象进行回收。有以下几种算法:1、引用计数算法;2、可达性分析算法。
1、引用计数算法是当一个地方引用它是计数器就加1,引用失效时计数器就减1,计数器为0时就是没有被引用的对象。很多主流虚拟机没有使用这个算法,因为它很难解决对象之间互相循环引用的问题;
2、可达性分析算法的基本思路是通过一系列被称为“GC Roots”的对象最为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有引用链时,就判定对象无引用。可是为GC Roots的对象有:虚拟机栈(栈帧中的本地变量表)中的引用对象、方法区中的类静态属性引用的对象、方法区中常量池引用的对象、本地方法栈中JNI(一般说的本地方法,即Native方法)引用的对象。
二、什么是引用?如果reference类型的数据中储存的数据代表的另一块内存的地址,那这个数据就是一个引用。JDK1.2之后对引用进行了扩展,分为: 1、强引用;2、软引用3、弱引用;4、虚引用。
1、强引用就是引用还存在,垃圾收集器永远不会回收掉被引用的对象;
2、软引用是在系统将要发生内存溢出前把这些对象列进回收范围之中进行二次回收,如果内存还是不足,才抛出内存溢出异常,软引用使用SoftReference类来实现;
3、弱引用是对象只能活到下一次垃圾收集发生之前,无论内存是否足够,都会被回收掉。弱引用使用WeakReference类实现;
4、虚引用完全不会对其生存时间构成影响,也无法通过虚引用获得对象实例。虚引用的唯一作用就是在实例被回收之前收到系统的通知。使用PhantomReference类来实现虚引用。
三、对象的finalize()方法。对象在被判定没有引用后和被垃圾回收之前会至少进行2次标记,第一次为可达性算法判定对象没有引用链时,会对还有必要执行finalize()方法的对象进行标记,当对象没有覆盖finalize()方法或者该方法已经被执行过后,虚拟机将视为没有必要执行此方法。之后队列被放进一个F-Queue队列等待执行finalize()方法,然后GC将对对象进行第二次标记。
四、方法区(永久代)的回收内容主要是两部分:废弃常量和无用的类。废弃常量:如常量池中的“abc”,如果没有一个String对象叫做“abc”也没有其他类使用“abc”,那这个常量就是废弃常量。判定一个类是无用的类有三个必要条件:1、该类所有的实例都已经被回收,也就是java堆中不存在该类的实例;2、加载该类的ClassLoader已经被回收;3、该类对java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。
五、垃圾收集算法
1、标记-清除算法 是将被标记为可回收的对象进行清除,此算法有两大问题:效率问题和空间问题。因为标记和清除两个过程效率都不高,并且清除后的内存空间不连续;
2、复制算法 是将内存分为两块,一块满时就将存活的对象按顺序的复制到另一块内存中,然后将原有的类删除。有点是实现简单,运行高效,但是可用内存减小了一半。为此HotSpot虚拟机默认Eden区和Survivor区的比例是8:1,以Eden区和一块Suvivor区作为新生代,另一块Suvivor区作为保留区域,每当垃圾回收时将存活的对象复制到保留区Suvivor中,清除新生代的所有对象。如果存在对象100%存活的场景,不能使用此算法
3、标记-整理算法 标记的过程和标记-清除算法相同,然后让所有的存活对象向一边移动,然后将存活端边的对象清除。
4、分代收集算法 只是把堆分成新生代和老年代,然后根据不同的代使用不同的回收算法。新生代一般使用复制算法,老年代则必须使用标记-清除算法或者标记-整理算法
六、收集器是收集算法的具体实现
1、Serial收集器(新生代、单线程、复制算法) 是最基本、发展历史最悠久的收集器。它在JDK1.3之前是新生代的唯一选择。这个收集器是一个单线程收集器,这里的单线程是指在收集器工作时,必须暂停其他线程的工作,一般称为Stop The World。新生代采用复制算法,暂停所有线程。
2、ParNew收集器(新生代、多线程、复制算法) 其实就是Serial收集器的多线程版本,出了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The world、对象分配规则、回收策略等都与Serial收集器一样。使用-XX:+UseConcMarkSweepGC选项后默认使用ParNew收集器,也可以使用-XX:UseParNewGC来选择使用。可以使用-XX:ParallelGCTreads来限制垃圾收集的线程数。
3、parallel Scavenge收集器(新生代、多线程、复制算法) 它也是采用复制算法的收集器,看上去和ParNew都一样,但是Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。所谓吞吐量,是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。高吞吐量可尽快的运行完用户的代码。Parallel Scavenge收集器提供了两个准确控制吞吐量的参数,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills参数和直接设置垃圾收集时间和总时间的比例的-XX:GCTimeRatio。MaxGCPauseMills参数调的越大,垃圾收集次数越频繁,吞吐量就越小。打开-XX:+UseAdaptiveSizePolicy参数后,就不需要手工配置新生代大小,Eden区和Suvivor区的比例等参数了。
4、Serail Old收集器(老年代,单线程、标记-整理算法)
5、Parallel Old收集器(老年代、多线程、标记-整理算法)
6、CMS收集器(老年代、多线程并发、标记清除算法) 整个过程分为4个步骤:1、初始标记(Stop The World);2、并发标记;3、重新标记(Stop The World);4、并发清除。但是有三个缺点:1、对CPU资源敏感;2、无法处理浮动垃圾(在垃圾回收过程中产生的类),JDK1.5之后老年代在使用了68%内存后默认开启CMS收集器,可以通过-XX:CMSInitiatingOccupancyFranction参数来设置触发半分比;3、标记清除算法带来的空间使用不充分,可以开启-XX:+UseCMSCompactAtFullColection参数在触发Full GC前整合碎片空间,但是停顿时间会相应变长。另一个参数-XX:CMSFullGCCsBeforeCompaction是设置多少次Full GC后必须出现一次整合。
7、G1收集器 结合了以上收集器的特点,并行并发,分代收集(G1收集器能独立管理整个GC堆),空间整合(G1收集器从整体上看是基于标记-整理算法实现的,从局部上看是基于复制算法实现的),可预测的停顿。
六、内存分配
1、对象优先在Eden分配,当Eden区内存不足时会发生一次Minor GC,可通过-XX:+PrintGCDetails打印日志。
2、大对象直接进入老年代,需要连续内存空间的Java对象比如很长的字符串和数组,程序应该避免这种写法。可以通过设置-XX:PertenureSizeThreshold参数来判断内存大于多少的对象直接被放到老年代。
3、长期存活的对象将进入老年代,在Eden区中经过一侧Minor GC后并且成功进入到Survivor区的对象年龄计数器加1,并且在之后的Minor
GC发生后继续累加,累加到阈值后进入老年代,可以通过-XX:MaxTenuringThreshold参数设置这个阈值。
4、动态对象年龄判定是指虚拟机并不是永远的要求对象年龄必须达到阈值后才能被转移到老年代,如果Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象直接被转移到老年代。
5、空间分配担保是指在发生Minor GC之前虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandelPromotionFailure设置值是否允许担保失败,如果允许。那么会继续检查老年代最大可用的连续空间是否大于历次转移到老年代对象的平均大小,如果大于,则尝试一次Minor GC,这次GC会有风险。如果小于就进行一次FULL GC(清理整个堆)。