1. JVM的自动内存管理机制

1.1 对象状态算法

  • 引用计数算法: 很难解决对象之间相互引用的问题,内存泄露。

  • 跟搜索算法:通过一系列名为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索的路径成为引用链,当一个对象到GC Root没有一个引用时,则证明此对象是不可用的。

注:1. GC对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI。2. 跟搜索算法中,没有引用的对象并非非死不可,还有经历两次标记,第一次发现没有引用,则进行标记,并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或finalize()已经被调用过,虚拟机将这两种情况都视为没有必要执行。

1.2 回收方法区

方法区主要回收废弃变量和无用的类。无用的类:该类所有实例都已被回收,加载该类的ClassLoader被回收,该类对应的java.lang.Class对象没有在任何地方被引用。只有同时满足无用的类的三个条件,才可以被回收。反射、动态代理、CGLib等bytecode框架场景以及频繁定义ClassLoader的场景都需要具备卸载功能,以保证永久代不会溢出。

在JDK7中

使用永久代(Permanent Generation)实现方法区,这样就可以不用专门实现方法区的内存管理,但这容易引起内存溢出问题;

有规划放弃永久代而改用Native Memory来实现方法区;

不再在Java堆的永久代中生成中分配字符串常量池,而是在Java堆其他的主要部分(年轻代和老年代)中分配;

更多请参考:docs.oracle.com/javase/8/do…

2、在JDK8中

永久代已被删除,类元数据(Class Metadata)存储空间在本地内存中分配,并用显式管理元数据的空间:

 从OS请求空间,然后分成块;

 类加载器从它的块中分配元数据的空间(一个块被绑定到一个特定的类加载器);

 当为类加载器卸载类时,它的块被回收再使用或返回到操作系统;

 元数据使用由mmap分配的空间,而不是由malloc分配的空间;

1.3 垃圾收集算法

  • 标记清除算法: 标记所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。效率问题,空间问题(内存碎片)。

  • 复制算法: 新生代使用复制算法回收,由于新生代98%的对象都是朝生夕死的,所以不需要1:1的比例划分内存空间(Hotspot默认8:1),而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一个Survivor。当回收时,将Eden和Survivor中还存活的对象一次性的拷贝到另一块的Survivor上。最后清理掉Eden和刚才使用过的Survivor。当Survivor空间不够用时,需要依赖其他的老年代进行分配担保(Handle Promotion)。分配担保:如果Survivor空间没有足够的空间存放上次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。

  • 标记-整理算法:让所有存活的对象向一端移动,然后清理掉端边界以外的内存。老年代常用的算法。

  • 分代收集算法: 根据对象的存活周期不同,将内存划分为几块,根据各个年代的特点进行垃圾回收。

1.4 垃圾收集器

垃圾回收器
图片来自《深入理解Java虚拟机》
如果两个收集器之间存在连线,就说明他们可以搭配使用。

  1. Serial

    复制算法,新生代的单线程收集器,简单高效,stop the world。

  2. ParNew

    复制算法,Serial的多线程版本,stop the world。首选的新生代收集器。

  3. Parallel Scavenge

    新生代收集器,使用复制算法,多线程并行, stop the world。目标达到可控制的吞吐量。运行用户代码时间/CPU消耗总时常=吞吐量。该收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills参数及直接设置吞吐量大小的-XX:GCTimeRatio参数。MaxGCPauseMills参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。GCTimeRatio参数的值应该是一个大于0的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。除了这两个参数,还有一个-XX:+UseAdaptiveSizePolicy值得关注。打开这个参数后,不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量这种方式称为GC自适应的调节策略(GC Ergonomics)

  4. Serial Old

    Serial收集器的老年代版本,单线程收集器,使用标记-整理算法,stop the world。主要有两个用途:1. 在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候用。

  5. Parallel Old

    Parallel Scavenge的老年代版本,标记-整理算法,stop the world。

  6. CMS(Concurrent Mark Sweep)收集器

最短回收停顿时间为目标的收集器,重视服务响应速度,标记-清除算法实现。第一款并发收集器,可与用户线程并发执行。执行过程分为4个步骤:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)。初始和重新stop the world。初始标记只是标记GCRoots能直接关联的对象,速度很快,并发标记进行GCROOTs Tracing的过程。重新标记为了修复并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
优点:并发收集、低停顿。
缺点:1. 对CPU资源非常敏感,解决方案,减少GC线程的独占时间。2. 无法处理浮动垃圾。CMS运行期间,用户线程产生的垃圾为浮动垃圾,CMS无法在本次收集中个清理这些浮动垃圾。如果CMS运行期间,内存无法满足用户线程要求,就会出现Concurrent Mode Failure失败,可能导致另一个FullGC产生,启动预备方案 Serial Old。3. 产生大量的空间碎片。

  1. G1收集器

    标记整理算法实现,精确的控制停顿,指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。G1将整个Java堆(老年代,新生代)划分多个大小固定的独立区域(Region),并且追踪这些区域里面的垃圾堆积程度。在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。

1.5 内存分配与回收

对象在Eden中分配内存,没有足够的空间进行垃圾回收。MinorGC。

MinorGC: 新生代垃圾回收的动作,频繁且速度快。

MajorGC(FullGC): 指发生在老年代的GC,慢,清理新生代和老年代,CMS

大对象指需要大量连续空间的Java对象。典型就是很长的字符串和数组。直接进入老年代分配,避免在Eden及Survivor中大量复制拷贝。

长期生活的对象进入老年代。对象年龄计数器,每一次MinorGC增长1岁。默认15岁进入老年代。

动态对象年龄判断,如果在Survivor空间中,相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄段的对象直接进入老年代。

空间担保分配,每次MinorGC,检测升级到老年代的大小是否大于老年代的空间,如果大于,则进行FullGC。

参考资料

深入理解Java虚拟机-JVM高级特性与最佳实践
Getting Started with the G1 Garbage Collector
G1垃圾回收器详解
详解 JVM Garbage First(G1) 垃圾收集器
Charlie H, Monica B, Poonam P, Bengt R. Java Performance Companion
Java Virtual Machine Enhancements in JDK 7
Java内存管理:Java内存区域 JVM运行时数据区