内存溢出、如何判断对象已死和对象拯救

JVM内存区域划分->判断对象是否存活->垃圾回收算法(必考)->垃圾回收器(针对垃圾回收算法的实现)->JVM性能检测
单核CPU:每一个具体时刻只有一个线程抢占CPU资源。
局部变量表:存放基本数据类型(8种)、对象引用(占4个字节)
局部变量表所需的空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。
变量存放内存分析:
public class Test1{
	public static void main(String[] args){
		Test1 test1=new Test1();
		int a=10;//10在常量池
}
}
public class Test1、public static void main(String[] args)-》类的信息,                       放在方法区中
new  Test1()-》堆中
test和a放在栈中
10->基本数据类型的值,放在运行时常量池。

一、OOM溢出和栈溢出

1.OOM溢出(OutOfMemory)

设置堆的最大值为20M:-Xmx20M
设置堆的最小值为20M:-Xms20M
如果最大值和最小值相等,说明不允许对堆进行扩容。
这个参数在Run-VM Options中设置。
 OOM异常代码(内存泄漏):
import java.util.ArrayList;
import java.util.List;

class OOM{}
public class Test1{
public static void main(String[]args){
    List<OOM> list=new ArrayList<>();
    while(true){
        list.add(new OOM());
        }
    }
}

OOM溢出分为两种:内存溢出(常见)和内存泄漏

内存溢出:内存中的对象确实还应该存活,但由于堆内存不够用产生的异常。

内存泄漏:无用对象无法被GC。

判别内存溢出和内存泄漏?

如果分配较多的内存可以解决,不再报错,则为内存溢出;否则仍然报错,为内存泄漏。显然,上面为内存泄漏。

2.栈溢出

设置参数:-Xss128K
import java.util.ArrayList;
import java.util.List;

/**
*OOM-Xss128K  栈溢出代码示例(单线程)
*/
public class Test1{
private int length=1;
public void stackTest(){
    length++;
    stackTest();
}
public static void main(String[]args){
    Test1 test1=new Test1();
    try{
        test1.stackTest();
    }catch(Throwable e){
        e.printStackTrace();
        System.out.println("栈的深度:"+test1.length);
    }
    }
}

结果:每次运行结果都可能不同。

如果默认不设置栈大小,大概是2万多。

多线程版本,可能会造成电脑死机、黑屏。

二、判断对象已死

1.引用计数法

算法思想:给每个对象附加一个引用计数器,每当有一个地方引用此对象,计数器+1;每当有一个引用失效时,计数器-1;在任意时刻,只要计数器值为0的对象就是不能再被使用的,即对象已死。

优点:引用计数法实现简单,判定效率也较高。Python使用引用计数法来管理内存。C++中的智能指针一部分也用到了。但是无法解决循环引用问题。(缺点)

JVM未采用此算法。

/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC(未置空,对象回收前)
*/
public class Test{
    private Object instance;
    private static int_1MB=1024*1024;
    private byte[] bytes=new byte[2*_1MB];
    public static void main(String[]args){
        Test test1=new Test();
        Test test2=new Test();
        //循环引用
        test1.instance=test2;
        test2.instance=test1;

        //test1=test2=null;

        System.gc();
}
}

结果说明在垃圾回收前,有7433K;垃圾回收后,有4880K.

/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC(回收后)
*/
public class Test{
    private Object instance;
    private static int_1MB=1024*1024;
    private byte[] bytes=new byte[2*_1MB];
    public static void main(String[]args){
        Test test1=new Test();
        Test test2=new Test();
        //循环引用
        test1.instance=test2;
        test2.instance=test1;

        test1=test2=null;

        System.gc();
    }
}

结果:在垃圾回收前,有7433K;在垃圾回收后,剩余752K.说明Java虚拟机并没有采用引用计数法。

2.可达性分析算法

Java采用可达性分析算法来判断对象是否存活(C#、Lisp).

核心思想:

通过一系列“GC Roots”的对象作为起点,从这些节点开始向下搜索对象,搜索走过的路径,称为“引用链”,当一个对象到任意一个GC Roots对象没有任何的引用链相连时(从GC Roots到对象不可达),认为此对象已死。

在Java中能作为GC Roots的对象包含以下四种

1)虚拟机栈中(局部变量表)引用的对象

2)类静态变量引用的对象

3)常量引用的对象

4)本地方法栈中引用的对象

在上面的例子中,由于test1和test2不是上面四种情况的一种,虽然它们互相指(引用),但是因为不是GC Roots对象,所以JVM认为它们已死。

3.引用的概念(*****)

JDK1.2之后,对于引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种强度依次递减。

3.1 强引用:指的是代码中普遍存在的,类似于Object obj=new Object()。

在JVM中,只要强引用还存在,垃圾回收器永远不会回收此对象实例。

3.2 软引用:软引用用来描述一些有用但不必须对象。对于仅被软引用指向的对象,在系统将要发生内存溢出前,会将所有软引用对象进行垃圾回收。若内存够用,这些对象仍然保留。在JDK1.2之后,提供了SoftReference来实现软引用。

3.3 弱引用:弱引用强度比软引用更差一点。仅被弱引用关联的对象最多只能生存到下一次GC之前。当垃圾回收器开始工作前,无论当前内存是否够用,都会回收掉仅被弱引用关联的对象。在JDK1.2之后,使用WeakReference来实现弱引用。

3.4 虚引用:也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是在这个对象被GC之前,收到一个系统通知。在JDK1.2之后,提供了PhantomReference来描述虚引用。

import java.lang.ref.SoftReference;

/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC
*软引用
*/
public class Test{
    private Object instance;
    private static int_1MB=1024*1024;
    private byte[] bytes=new byte[2*_1MB];
    public static void main(String[] args){
        Test test=new Test();
        SoftReference softReference=new SoftReference(test);//让软引用指向这块内存
        test=null;
        System.gc();
    }
}

importjava.lang.ref.WeakReference;

/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC
*弱引用
*/

public class Test{
    private Object instance;
    private static int_1MB=1024*1024;
    private byte[] bytes=new byte[2*_1MB];
    public static void main(String[] args){
        Test test=new Test();
        WeakReference weakReference=new WeakReference(test);//让弱引用指向这块内存
        test=null;
        System.gc();
    }
}

结果说明:垃圾回收后和软引用相比,剩余空间变小,说明被弱引用的对象已经被回收。

三、对象的自我拯救(finalize())

Object类的方法:
protected void finalize() throws Throwable{
}

}

在可达性分析算法中不可达的对象,也并非"非死不可",所有不可达的对象处于"缓刑"阶段。

要宣告一个对象的彻底死亡,需要经历两次标记 一次筛选过程:

1)若对象在进行可达性分析之后发现到GC Roots不可达,此对象会进行第一次标记并且进行一次筛选过程。筛选的条件是此对象是否有必要执行finalize()。当对象没有覆盖finalize()方法或finalize()方法已被JVM调用过,JVM会将此对象彻底宣判死亡。

2)筛选成功(对象覆写了finalize()并且未被执行过),会将此对象放入F-Queue,如果对象在finalize()中成功自救(此对象与任意一个GC Roots建立联系),则对象会在第二次标记时被移除回收集合,成功存活;若对象在finalize()中仍然与GC Roots不可达,宣告死亡。

/**
*查看系统的垃圾回收,打印它的日志。-XX:+PrintGC
*/
public class Test{
private staticTesttest;

public static void isAlive(){
    if(test!=null){
        System.out.println("I am alive");
    }
}
@Override
protected void finalize() throws Throwable{
    super.finalize();
    System.out.println("finalize method execute");
    test=this;//成功自救
}

public static void main(String[] args){
    test=new Test();
    test=null;
    System.gc();
    try{
        Thread.sleep(500);
    }catch(InterruptedExceptione){
        e.printStackTrace();
    }
    if(test!=null){
    test.isAlive();
    }else{
    System.out.println("I am dead");
    }
//=============================
    test=null;
    try{
    Thread.sleep(500);
    }catch(InterruptedException e){
        e.printStackTrace();
    }
    if(test!=null){
    test.isAlive();
    }else{
    System.out.println("I am dead");
    }
    }
}

第二次对象死亡的原因:任何一个对象的finalize()只会被系统调用一次,因此第二次回收对象失败。finalize()不推荐使用,在JDK1.9之后,finalize()加上了@Deprecated。

发布了148 篇原创文章 · 获赞 32 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/smell201611010513/article/details/94060250
今日推荐