JVM垃圾回收机制原理
1.JVM内存模型
根据 JVM 规范,JVM 内存共分为虚拟机方法区、堆、栈、程序计数器、本地方法栈五个部分。
PC寄存器(程序计数器):用于记录当前线程运行时的位置,每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。
java 虚拟机栈:在创建线程时创建的,用来存储栈帧,因此也是线程私有的。java程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的临时数据和中间结果,包括局部变量表、操作数栈、动态链接、方法出口等信息。这些栈帧就存储在栈中。如果栈深度大于虚拟机允许的最大深度,则抛出StackOverflowError异常。
局部变量表:方法的局部变量列表,在编译时就写入了class文件
操作数栈:int x = 1; 就需要将 1 压入操作数栈,再将 1 赋值给变量x
java 堆:java堆被所有线程共享,堆的主要作用就是存储对象。如果堆空间不够,但扩展时又不能申请到足够的内存时,则抛出OutOfMemoryError异常。
方法区:方发区被各个线程共享,用于存储静态变量、运行时常量池等信息。
本地方法栈:本地方法栈的主要作用就是支持native方法
2.什么才是垃圾?
JVM垃圾回收仅针对公共内存区域,即:堆和方法区进行,因为只有这两个区域在运行时才能知道需要创建些对象,其内存分配和回收都是动态的。
对象之间的引用可以抽象成树形结构,通过树根(GC Roots)作为起点,当一个对象到GC Roots没有任何引用链相连时(不可达),则证明这个对象为可回收的对象。
有一下三种:
(1)栈帧中的本地变量表所引用的对象。
(2)方法区中类静态属性和常量引用的对象。
(3)本地方法栈中JNI(Native方法)引用的对象。
【垃圾产生的情况举例】
1.改变对象的引用,如置为null或者指向其他对象
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2; //obj1成为垃圾
obj1 = obj2 = null ; //obj2成为垃圾
2.引用类型:
强引用:是最难被GC回收的,宁可虚拟机抛出异常,中断程序,也不回收强引用指向的实例对象。
//强引用(指向实例对象,存在堆中)出现内存不够用OutOfMemoryError也不会回收
Object obj=new Object();
软引用 (SoftReference),在内存不足时,GC会回收软引用指向的对象(软引用可用来实现内存敏感的高速缓存。)
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
//软引用
String str=“hello yihaha”;
SoftReference soft=new SoftReference(str);//将强引用转成软引用
System.out.println(soft.get());
弱引用(WeakReference),不管内存足不足,只要我GC,我都可能回收弱引用指向的对象(。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些弱引用的对象。)
//弱引用
WeakReference wReference=new WeakReference(str);
System.out.println(wReference.get());
3.循环每执行完一次,生成的Object对象都会成为可回收的对象
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.println(obj.getClass());
}
虚引用(PhantomReference ),虚引用必须和引用队列(ReferenceQueue)联合使用。
当垃圾回收器发现一个对象有虚引用时,首先执行所引用对象的finalize()方法,在回收内存之前,把这个虚引用对象加入到引用队列中,
你可以通过判断引用队列中是否有该虚引用对象,来了解这个对象是否将要被垃圾回收。
然后就可以利用虚引用机制完成对象回收前的一些工作。(注意:当JVM将虚引用插入到引用队列的时候,虚引用执行的对象内存还是存在的。但是PhantomReference并没有暴露API返回对象。
所以如果我想做清理工作,需要继承PhantomReference类,以便访问它指向的对象。)
//虚引用
ReferenceQueue queue=new ReferenceQueue<>();
PhantomReference phantomReference=new PhantomReference(str,queue);
System.out.println(phantomReference.get());
4.类嵌套
class A{
A a;
}
A x = new A();//分配了一个空间
x.a = new A();//又分配了一个空间
x = null;//产生两个垃圾
5.线程中的垃圾
calss A implements Runnable{
void run(){
//…
}
}
//main
A x = new A();
x.start();
x=null; //线程执行完成后x对象才被认定为垃圾
3.如何发现垃圾?
引用计数算法:在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。但是如果两个垃圾对象相互引用,那么这两个垃圾对象的引用计数永远不为0。
根搜索算法(GC Root)
栈帧里的局部变量所引用的对象;
方法区的静态变量和常量所引用的对象;
4.清除垃圾
1.标记-清除(Mark-Sweep):最基础的垃圾回收算法
标记-清除算法分为两个阶段:标记阶段和清除阶段。
标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
标记-清除算法容易实现,但问题是,容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
2.复制(Copying):
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
复制(Copying)算法实现简单,运行高效且不容易产生内存碎片,但是能够使用的内存缩减到原来的一半。
Copying算法的效率跟存活对象数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将大大降低。
3.标记-整理(Mark-Compact):
该算法标记阶段和标记-清除(Mark-Sweep)一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
4.分代(Generation Collection),借助前面三种算法实现:
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。
核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下,将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),
老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数比较少,但是实际中并不是按照1:1的比例来划分新生代的空间,
一般来说是将新生代划分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden区和其中一块Survivor区,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor区中,
然后清理掉Eden和刚才使用过的Survivor区。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。
5.内存泄漏和内存溢出
内存泄漏是指程序里有无法被回收的对象。
内存溢出是指程序在申请内存时,没有足够的内存空间供其使用。大多数原因都是因为内存泄漏产生大量无法回收对象占用大量内存,导致内存溢出。
6.使用高效代码的技巧
尽量不要在循环中: 使用try…catch、new 对象;
把频繁使用的短命对象缓存起来;
尽可能使用栈内变量(方法内局部变量)
用线程池、连接池,不要自己创建
学会读Java核心API源代码