参考
判断对象是否存活
引用计数法
给每个对象设置一个计数器,当引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。
引用计数器实现简单,效率高;但不能解决循环引用问题,同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后不再使用。
根搜索方法(可达性分析算法)
根搜索方法是通过一些“GCRoots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(ReferenceChain),当一个对象没有被GCRoots的引用链连接的时候,说明这个对象是不可用的。
GCRoots对象包括:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)的引用的对象。
Java四引用
引用强度按顺序减弱。
强引用(StrongReference)
强引用是使用最普遍的引用。Object obj = new Object()
只要强引用还存在,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题
软引用(SoftReference)
如果内存空间不足了,就会回收这些对象的内存,多用于高速缓存。SoftReference<A> softReference = new SoftReference<A>(a);
只要垃圾回收器没有回收它,软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中
弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中
虚引用(PhantomReference)
虚引用在任何时候都可能被垃圾回收器回收,主要用来跟踪对象被垃圾回收器回收的活动,被回收时会收到一个系统通知。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
加深理解可以阅读
com.google.common.cache.LocalCache
- Java中的WeakReference与SoftReference的用法
- ReferenceQueue的使用
JVM运行内存的分类
程序计数器
当前线程所执行的字节码的行号指示器,用于记录下一条要运行的指令,线程私有
注:如果正在执行的是Native方法,计数器值则为空
Java虚拟机栈
存放基本数据类型、对象的引用、方法出口等,线程私有
Native方法栈
和虚拟栈相似,只不过它服务于Native方法,线程私有
Java堆
java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享
方法区
存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享
F-Queue
即使可达性分析算法中的不可达对象,也并非“非死不可”。对象至少经历两次标记过程:第一次发现没有GC Roots引用链会被第一次标记且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法被调用过,将被视为“没有必要执行finalize方法”。如果判定为“有必要执行finalize方法”时,将会把这个对象放置在F-Queue队列中,并在低优先级的Finalizer线程中执行它。
JVM垃圾回收算法
标记-清除算法
标记-清除(Mark-Sweep)算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
缺点:
1、效率问题,标记和清除两个过程的效率都不高;
2、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
复制算法可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)。
图的上半部分是未回收前的内存区域,图的下半部分是回收后的内存区域。通过图,我们发现不管回收前还是回收后都有一半的空间未被利用。
优点
效率高,没有内存碎片
缺点:
1、浪费一半的内存空间
2、复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。
标记整理算法
它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
缺点:
1、效率问题,(同标记清除算法)标记和整理两个过程的效率都不高;
优点:
2、相对标记清除算法,解决了内存碎片问题。
3、没有内存碎片后,对象创建内存分配也更快速了(可以使用TLAB进行分配)。
分代收集算法
当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法,而老年代因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收。
图的左半部分是未回收前的内存区域,右半部分是回收后的内存区域。
对象分配策略:
对象优先在Eden区域分配,如果对象过大直接分配到Old区域。
长时间存活的对象进入到Old区域。
改进复制算法:
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor 。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。