JVM回收机制
回收机制概念
java语言支持的类型有引用类型和基本类型。
对象,是类的一个实例。不仅可以存在于堆中,也可以存在方法区。
但是一些小对象, 在没有逃逸的情况下, 分配在栈中
逃逸: 指的是比如在方法外面定义的变量, 然后在方法里面new的, 就是说这个new出来的对象已经跑出了它的作用域, 这就是逃逸
public calss A{
//常量存在于方法区中的运行时常量池中
public static final int C=2;
//反射时的java.lang.Class对象也存在于方法区
public static void main(String args[]){
//a标识符指向的对象存在于堆中
A a=new A();
}
}
垃圾回收(GC)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象所占据的内存空间的一种机制。GC在执行时会进行可达性分析, 如果内存的对象不可达, 则会被回收
引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
引用又分为强引用、软引用、弱引用、虚引用四种:
手动GC System.gc();
自动GC: 模拟内存溢出, 如下代码:
List<byte[]> list=new ArrayList<>();
for(int i=0;i<100000;i++) {
list.add(new byte[1024*1024]);
}
-
强引用(Strong Reference):如
Object obj = new Object()
,这类引用是java中最普遍的,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象,不管内存是否够用。 -
软引用(Soft Reference):它用来描述一种可能有用,但不是必须的对象,在系统内存不够用时,这类引用会在垃圾收集器下一次回收垃圾时被回收。
SoftReference<Object> c2=new SoftReference<Object>(new Object());
-
弱引用(Weak Reference):它是用来描述非必须的对象,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集器回收垃圾之前,不论系统内存是否够用,都会回收掉只被弱引用关联的对象。
WeakReference<Object> c3=new WeakReference<Object>(new Object());
-
虚引用(Phantom Reference):最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。
一般来说,
new Object()
不分配引用变量, 就是虚引用
/**
* FAQ:
* 1)当对象存在引用时,是否可能在系统触发GC时被回收?可能,要看引用类型?
* 2)当GC系统触发时,假如对象要被销毁了会执行finalize方法吗?会的。
*
* JVM GC 系统
* 1)市场常见GC算法?(不限于java平台)
* 2)JVM中常见GC算法?(不同规范的实现,算法不相同)
* 3)JVM规范的实现HotSpot虚拟机有什么GC算法的应用?具体应用场景有什么不同。
*/
class Container{
private Object[] array;
public Container(int cap) {
this.array=new Container[cap];
}
//....
@Override
protected void finalize() throws Throwable {
System.out.println("==finalize()==");
}
}
public class TestGC02 {
public static void main(String[] args) {
doMethod01();
}
/**
* 在JAVA中我们可以使用的对象引用方式有四种:
* 1)强引用:此引用引用的对象,生命力最强。(对象不会被GC)
* 2)软引用:此引用引用的对象,在内存不足时可能会被GC。
* 3)弱引用:此引用引用的对象,在GC执行时可能直接会被销毁(即便是内存充足)。
* 4)虚引用:用的最少,类似没有引用,主要用于记录对象的销毁。---了解
*
* 说明:软引用和弱引用通常会应用在一些缓存产品的设计中。
*/
private static void doMethod01() {
//1.强引用
//Container c1=new Container(100);//c1就是强引用
//c1=null;//此时c1指向的Container对象不可达(也就是说JVM访问不到了)
//2.软引用
//SoftReference<Container> c2=new SoftReference<Container>(new Container(20));
//Container cc=c2.get();//这种写发是又将软引用转换为了强引用。不推荐
//System.out.println(c2.get());//通过软引用获取和操作对象
//3.弱引用
WeakReference<Container> c3=new WeakReference<Container>(new Container(100));
System.out.println(c3.get());
//手动GC
System.gc();//GC启动以后,GC系统会对内存中的对象进行可达性分析。访问不到则进行标记。
//自动GC(通过JVM参数进行分析)
// List<byte[]> list=new ArrayList<>();
// for(int i=0;i<100000;i++) {
// list.add(new byte[1024*1024]);
// }
}
}
1.引用计数器算法
解释
系统给每个对象添加一个引用计数器,每当有一个地方引用这个对象的时候,计数器就加1,当引用失效的时候,计数器就减1,在任何一个时刻计数器为0的对象就是不可能被使用的对象,因为没有任何地方持有这个引用,这时这个对象就被视为内存垃圾,等待被虚拟机回收
优点
客观的说,引用计数器算法,他的实现很简单,判定的效率很高,在大部分情况下这都是相当不错的算法
其实,很多案例中都使用了这种算法,比如 IOS 的Object-C , 微软的COM技术(用于给window开发驱动,.net里面的技术几乎都是建立在COM上的),Python语言等.
缺陷
无法解决循环引用的问题.
这就好像是悬崖边的人采集草药的人, 想要活下去就必须要有一根绳子绑在悬崖上. 如果有两个人, 甲的手拉着悬崖, 乙的手拉着甲, 那么这两个人都能活, 但是, 如果甲的手拉着乙, 乙的手也拉着甲, 虽然这两个人都认为自己被别人拉着, 但是一样会掉下悬崖.
比如说 A对象的一个属性引用B,B对象的一个属性同时引用A A.b = B() B.a = A(); 这个A,B对象的计数器都是1,可是,如果没有其他任何地方引用A,B对象的时候,A,B对象其实在系统中是无法发挥任何作用的,既然无法发挥作用,那就应该被视作内存垃圾予以清理掉,可是因为此时A,B的计数器的值都是1,虚拟机就无法回收A,B对象,这样就会造成内存浪费,这在计算机系统中是不可容忍的.
解决办法
在语言层面处理, 例如Object-C 就使用强弱引用类型来解决问题.强引用计数器加1 ,弱引用不增加
Java中也有强弱引用
2.可达性分析算法
解释
这种算法通过一系列成为 "GC Roots " 的对象作为起始点,从这些节点开始向下搜索所有走过的路径成为引用链(Reference Chain) , 当一个对象GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达),则证明此对象是不可用的
JVM是通过可达性分析算法判断对象是否存活的,这个算法的基本思想是:通过一系列被称为”GC Roots”的对象作为起点,向下搜索,搜索所走过的所有路径称为对象的引用链,当一个对象通过引用链无法到达“GC Roots”时,说明该对象是不可用的,是可以回收的(不一定回收)。
优点
这个算法可以轻松的解决循环引用的问题
大部分的主流java虚拟机使用的都是这种算法
图解
在可达性分析之后,如果判断对象通过引用链无法到达“GC Roots”,则认为该对象是可回收的,但是不保证一定会回收这个对象。对象的回收至少需要经过两次标记过程:
- 判断对象通过引用链不可达之后,进行第一次标记并且进行一次筛选,筛选的条件是判断对象是否有必要执行finalized()方法。如果对象没有覆盖finalize()方法或者不是虚拟机第一次调用,则判断为没有必要执行finalize()方法。
- 如果对象有必要执行finalized()方法,则会将该对象放到一个F-Queue队列中,并且虚拟机会自动创建一个低优先级的Finalizer线程执行。“执行”的含义是虚拟机会去触发Finalizer线程的执行,但是不会等待结果的返回。因为如果finalized()方法执行时间较长或者产生死循环的话,将导致F-Queue中的其他对象永远处于等待状态。甚至导致整个回收系统的奔溃。finalized()方法是对象实现逃脱的唯一一次机会。如果对象没有逃脱,则在JVM进行第二次标记之后会回收该对象。
方法区回收
方法区处于永久代中,永久代中主要有两部分回收内容:废弃的常量和无用的类。
-
回收常量:回收常量和回收堆对象是相同的,如果一个常量在在常量池中已经存在,如果在系统中没有一个对象引用常量池中的常量的话,那么这个常量就需要被回收。
-
回收类:回收类的条件比回收常量的苛刻许多:以下三点为必要不充分条件:
- 该类所有的实例都已经被回收,也就是说java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.Java语言中的GC Roots
在虚拟机栈 (其实是栈帧中的本地变量表) 中引用的对象
在方法区中的类静态属性引用对象
在方法区中的常量引用的对象
在本地方法栈中JNI(即一般说的Native方法)的引用对象
小案例
创建一个类:Ponit类
/**
* 定义一个点对象类型
*/
public class Point{
int x;
int y;
public Point(int x,int y) {
this.x=x;
this.y=y;
}
@Override
protected void finalize() throws Throwable {
System.out.println("finalize()");
}
}
finalize()方法的作用:
finalize方法会在对象被回收(GC)之前执行,可以对对象的回收进行监控,
也可以在对象回收之前进行一些资源释放操作。
进行以下测试:
//通过JVM参数检测是否触发了GC操作:-XX:+PrintGCDetails
public class TestGC01 {
static Map<String,Object> objectPool=new HashMap<>();
public static void main(String[] args) throws InterruptedException {
//构建一个实例对象,并通过P1引用指向这个对象
Point p1=new Point(10,20);//p1为一个强引用
objectPool.put("point", p1);//Spring中的singleton作用域
p1=null; //Spring中prototype作用域对象的销毁
objectPool.clear();//Spring中Singleton作用域的销毁
//请问对于p1引用的这个对象何时会被标识垃圾对象,何时会被回收,如何确定此对象被回收了
//1)当p1引用不再指向构建的Point对象时,此对象会被GC系统认为是垃圾对象。
//2)当JVM系统触发了GC操作时,对象可能会被回收。
//3)此对象会被JVM中的垃圾回收系统(GC系统)进行回收。(释放内存)
//触发GC操作?(GC系统触发时会对内存中的对象进行可达性分析,就是检测是否还可以通过引用
//访问到此对象,假如不能通过任何引用访问此对象,这个对象就会被标识垃圾)
//1.手动GC
//System.gc();
//2.自动GC(满足了GC条件时或者说内存使用达到一定的GC启动标准)
List<byte[]> list=new ArrayList<>();
for(int i=0;i<100000;i++) {
list.add(new byte[1024*1024]);
}
}
}
GC没有启动, 对象是否可能被回收
当GC系统没有启动时,对象是否可能被回收?可能,看你对象分配在哪了?
说明:JAVA中大多数对象都会存储在堆(Heap)中,但对于一些没有逃逸的小对象现在也可能分配在栈上(JVM发展历程中的一种新的优化方式。)。
public class TestGC03 {
//-XX:+PrintGCDetails
public static void main(String[] args) {
for(int i=0;i<100000000;i++) {
doMethod01();
}
}
static byte[] b2;
static void doMethod01() {
//小对象,未逃逸,栈上分配,栈上分配对象的对象,方法调用结束,对象销毁。
//byte[] b1=new byte[1024*1024]; //小对象,未逃逸(方法外界没有引用指向此对象),可以直接分配在栈上
//小对象,已逃逸,堆上分配,对象回收需要借助GC系统。
b2=new byte[1];//小对象,但逃逸了(方法外部有引用指向此对象),对象分配在堆上
}
//JDK8中默认会打开逃逸分析选项,希望未逃逸的小对象分配在栈上,这样可以避免启动GC对对象进行回收。
}
总结:
小对象,未逃逸,栈上分配,栈上分配对象的对象,方法调用结束,对象销毁。
小对象,已逃逸,堆上分配,对象回收需要借助GC系统。