概述
Java GC机制概括的说就是:该机制对JVM中的对象进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,保证JVM中的内存空间,防止出现内存泄漏和溢出问题。
内存区域
在Java运行时的数据区里,由JVM管理的内存区域主要分5部分:
.
程序计数器 (Program Counter Register)
程序计数器是一个比较小的区域,它主要用来指示当前线程所执行的字节码执行到了第几行,可以理解为当前线程的行号指示器。字节码解释器可以通过改变它的值来获取下一条指令。
每个程序计数器只用来记录一个线程的行号,所以它是 线程私有的,一个线程一个程序计数器。由于程序计数器知识纪录当前指令地址,所以不存在内存溢出的情况。因此程序计数器是所有JVM内存区域中唯一没有OutOfMemoryError的区域。
虚拟机栈 (JVM Stack)
一个线程的每个方法执行时,都会创建一个栈帧(Stack Frame),栈帧中存储有局部变量表、操作栈、动态链接、方法出口等。当方法被调用时,栈帧被压入虚拟机栈,方法完成时,栈帧出栈。
局部变量表中存储方法的局部变量,包括基本数据类型、对象引用和返回地址等。局部变量表是在编译时确定,运行时所分配的空间是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈定义了两种异常,如果线程调用的栈深度大于虚拟机所允许的最大深度,则抛出StackOverFlowError;如果内存不足,则抛出OutOfMemoryError。
每个线程对应一个虚拟机栈,所以虚拟机栈也是线程私有的
本地方法栈 (Native Stack)
本地方法栈作用、运行机制、异常类型等都与虚拟栈相同,唯一的区别是:虚拟机栈时执行Java方法的,而本地方法栈是用来执行native方法的,很多虚拟机会将这两个放在一起。
堆区(Heap)
在JVM管理的内存中,堆区是最大的一块,也是GC机制所管理的主要内存。堆区由所有线程共享,在虚拟机启动时创建,堆区的存在时为了存储对象实例。如果在执行垃圾回收后,仍然没有足够的内存分配,也不能在扩展时,将会抛出OutOfMemoryError:Java heap space异常。
方法区(Method Area)
在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实场,方法区并不是堆(Non-Heap),但是它也是一般人理解中的永生代。
方法区是所有线程共享的,用于存储已经被虚拟机加载的类信息(包括版本、Field、方法、接口等信息)、final常亮、静态变量、编译器即时编译的代码等。
方法区上一般执行的垃圾收集很少,但这并不代表方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池和对已加载类的卸载。
但在方法区上做垃圾收集,条件苛刻而且效果也不理想,所以一般不会太考虑。如果内存不足时,会抛出OutOfMemoryError:PermGen space异常。
内存分配
这里所说的内存分配,主要是在堆区的分配。Java的内存分配和回收规则概括的说就是:分代分配,分代回收。对象根据存活时间被分到:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。
年轻代(Young Generation)
对象被创建时,首先分配到年轻代,大对象会直接分配到年老代,大部分对象在创建后很快就不用了,因此很快变的不可到达,被年轻代的GC回收。
年轻代分为3个区域:伊甸区(Eden区)和两个存活区(Survivor 0, Survivor 1)。
- 绝大多数刚创建的对象被分配到Eden区,大多数很快会被回收,Eden区是连续的内存空间,分配内存极快;
- 存活区是用来放置Minor GC中幸存的对象的,也是实施“停止-复制”回收的关键。
在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers)
- 由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;
- TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。
TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。
年老代(Old Generation)
如果对象在年轻代存活足够多的时间而没有被清理掉,则会被复制到年老代。年老代的空间一般比年轻代大,能够存更多的对象,在年老代发成的GC次数要比年轻代少很多。当年老代内存不足时,会执行Major GC,也叫Full GC。
如果对象比较大,年轻代无法存放,大对象会直接分配到年老代上,所以尽量少用大对象。
可能存在年老代的对象引用年轻代对象的情况,如果需要执行Minor GC,则可能需要查询整个年老代以确定是否可以清理回收,这过于低效。解决方法:年老代中维护一个512 byte的块--“Card Table”,所有老年代引用新生代对象的纪录都在这里,Minor GC只需要查询这个块就可以。
垃圾回收机制
垃圾回收的基本规则:分代收集,每代的回收机制都不同。
年轻代
- 当Eden区满的时候,执行Minor GC,将存活对象复制到Survivor0 中,清空Eden区;
- 下次Eden区满了,再执行Minor GC,将存活对象复制到Survivor1 中,清空Eden区;
- 将Survivor0 中可以晋级的对象挪到年老代,存活的对象复制到Survivor1 中,清空Survivor0区;
- 对象在两个Survivor区中复制多次后,(Hot Spot默认15次),对象会复制到年老代。
从上面的过程看出,Eden区是一个连续的空间,且Survivor区总有一个为空。经过一次GC和复制,一个Survivor区保存当前存活的对象,而Eden区和另一个Survivor区的内容都不在需要,直接清空。到下一次GC时,两个Survivor区的角色互换。这种方式分配内存和清理内存效率极高,这种垃圾回收的方式就是 停止-复制(stop-and-copy)清理法。
由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到年老代。
年老代
年老代存储的对象比年轻代多很多,而且不乏大对象,对年老代实施停止-复制算法,则相当低效。一般,年老代使用的算法是“标记-整理”算法,即:标记仍然存活的对象,将所有存活的对象向一端移动,以保证内存的连续。
在发生Minor GC时,虚拟机会检查每次晋升进入年老代的对象大小是否大于年老代的剩余空间。如果年老代的剩余空间不够,则直接出发一次Full GC。否则,就需要看是否设置 XX:+HandlePromotionFailure,如果不允许升级失败,则每次Minor GC都会触发Full GC,哪怕年老代还有足够的内存空间。
方法区(永久代)
永久代的回收有两种:常量池中的常量和无用的类信息。其中对于无用的类进行回收,必须保证3点:
- 类的所有实例都已经被回收
- 加载类的ClassLoader已经被回收
- 类对象的Class对象没有被引用(即没有通过反射引用该类)
垃圾收集器
在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如:
年轻代
需要明确一点,年轻代采用的停止-复制算法中,停止的意义是在回收内存时,需要暂停其他所有线程的执行,这个是很低效的,现在各种年轻代收集器越来越优化这一点,但仍然只是将停止时间变短,并未彻底取消停止。
- Serial收集器:使用一个线程进行GC,其他工作线程暂停;
- ParNew收集器:Serial收集器的多线程版本,用多个线程进行GC,并行,其他工作线程停止,缩短垃圾回收时间;
- Parallel Scavenge 收集器:关注CPU吞吐量,即玉宁用户代码的时间/总时间,比如JVM运行100分钟,其中运行用户代码99分钟,垃圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运行
年老代
- Serial Old收集器:单线程收集器,串行,使用标记-整理算法,整理的方法是Sweep(清理)和Compact(压缩),使用单线程进行GC,其他工作线程暂停;
- Parallel Old收集器:多线程并行收集器,多线程机制和Parallel Scavenge差不多,使用标记整理,整理时Summary(汇总)和Compact(压缩),执行时,仍然要暂停其他线程。它在多核计算中很有用;
- CMS(Concurrent Mark Sweep)收集器:致力于获取最短停顿时间,使用标记清除算法,多线程,优点是并发收集,用户线程可以和GC线程同时工作,停顿小