一、内存分配
Java程序运行时的内存分配,按照JVM规范,包括以下几个区域:程序计数器、虚拟机栈、本地方法栈、方法区、堆。其中,前三个是线程私有的,与线程生命周期相同,线程退出内存自动回收;后两者是所有线程共享内存的,只在垃圾回收机制被触发时,被动回收。
* 程序计数器,内存区域极小,是当前线程的字节码执行行号指示器;
* 虚拟机栈、本地方法栈,即平时所说的“栈”,是虚拟机用来执行方法(包括Java、非Java方法)时,使用的临时内存空间,用来存储当前方法、局部变量等,全部基本类型变量,以及类对象的引用都存储在栈中;
* 方法区,全局共享区域,用来存储已经被虚拟机加载的Class信息、常量(如字符串字面常量)、静态变量,以及编译器编译后的代码等;
* 堆,是Java虚拟机管理中最大的一块内存,为所有线程所共享,用来存储所有Java类实例。需要注意的是,实例数据在堆中开辟内存,而对象的引用相当于指针,存储在各线程的栈中。
二、垃圾回收算法
垃圾回收需要解决的三个问题:
1)哪些内存可回收。上面各内存区域中,只有方法区、堆是全局共享的内存块,需要垃圾回收来处理,定时回收内存。确定某个对象是否可回收,需要通过“可达性分析算法”来判断,哪些内存块是孤立无源、不可达的对象或对象集合。
2)什么时间能执行回收。执行垃圾回收时,需要完全地“冻结”现场,达到所谓“stop the world”,以避免在GC的过程中,引用关系发生变化,而引发问题。虚拟机的做法是让所有线程主动式中断,但并非立刻强制中断线程,而是GC线程设置一个标识,让其它所有线程在到达其下一个“安全点”时,根据此标识而主动挂起线程,等待GC线程的操作;
3)如何执行垃圾回收。垃圾回收算法有很多,但总体策略是分代回收,针对新生代、老生代特点,采取的GC算法是不同的。
* 对于新生代内存块,对象存活率低,需要频繁清理,为了避免“标记-清除”算法的碎片化问题,也为了提高效率,采取“复制”算法,即将新生代(young区)分为eden+survivor1+survivor2几块空间,在进行GC时,将eden/survivor1中的存活对象,复制到survivor2空间中暂存,然后整块清理掉前面的eden+survivor1区域,回收这块通常占比90%的内存区域。
* 对于老生代内存块,对象存活率高,不需要进行频繁的存活性判断的扫瞄,当进行GC时,通常使用“标记-整理”算法:标记可回收区域、移动集中存活对象内存、最终对剩余的垃圾区域进行回收。
三、垃圾收集器实现
GC线程在工作时,会完全或部分中断用户线程,影响系统吞吐率,使用户任务出现停顿甚至卡死。新生代的收集器有Serial/ParNew/Parallel Scavenge,老生代的收集器有CMS/Serial Old/Parallel Old。
* Serial/Serial Old是单线程收集器,在进行GC时,会中断所有用户线程,在新、老生代分别采用复制算法和标记-整理算法进行垃圾清理;
* ParNew是多个GC线程并行,但同样会中断所有用户线程,算法层面和Serial系列一致;
* Parallel收集器,同样也是多线程并行的回收器,不同的是其关注的是系统的吞吐率的指标,即用户任务的停顿时间比例,而不是停顿的绝对时间。
* CMS(Concurrent Mark Sweep)收集器,目的是追求最短停顿时间,目前流行最为广泛的收集器实现。分为initial mark/concurrent mark/remark/concurrent sweep四个步骤。其中,initial mark仅标记一下GC Roots能直接关联到的对象,concurrent mark是多GC线程同时与用户线程进行并发,进行GC Roots Traceing,实质是存活对象的联通域生长与标记,remark阶段对concurrent mark阶段用户线程并发执行时的对象引用变动,进行修正,concurrent sweep是并发地对垃圾进行清理。第1、3步,所有用户线程依然会中断,但因为其操作十分简单,所以速度很快,第2、4步,耗时较长,但是与用户线程并发进行的,因此整体来看,CMS GC的用户任务停顿时间极短。