内存溢出和内存泄漏

  • 其实学习jvm 的垃圾回收,基本上就是在解决这两类问题——内存泄漏和溢出,泄露了就得存代码角度去解决这个问题;如果不是因为泄漏导致的溢出,那就得去调整参数或者升级配置了。
  • gc 可以回收的话就不会内存溢出,但是频繁GC 可能导致性能问题
  • 对象的内存分配在java虚拟机的自动内存分配机制下,一般不容易出现内存泄漏问题。但是写代码难免会遇到一些特殊情况,比如OOM神马的。。尽管虚拟机内存的动态分配与内存回收技术很成熟,可万一出现了这样那样的内存溢出问题,那么将难以定位错误的原因所在。

内存溢出(OOM)

  • 系统已经不能再分配出你所需要的空间,比如你需要100M的空间,系统只剩90M了,这就叫内存溢出
  • 一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢,就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错.
  • 不行的时候就直接告诉你我不行,我装不下了,然后直接报错,就是这么的直接。

内存溢出的分类

数据量太大
  • 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
内存泄漏
  • 集合类中有对对象的引用,使用完后未清空,使得不能被JVM回收;(这就是为什么看到很多人在演示内存溢出的时候,平时通过list 来演示的原因)
代码bug
  • 代码中存在死循环或循环产生过多重复的对象实体;
内存分配不当
  • 启动参数内存值设定的过小(如果不存在内存泄漏,换句话来说,就是内存中的对象,还是都得存在,那就得修改启动参数,加大内存了,如果已经达到了物理机的限制,那就得升级配置了)

OutOfMemoryError 类

    • OutOfMemoryError,说的是java.lang.OutOfMemoryError,是JDK里自带的异常,顾名思义,说的就是内存溢出,当我们的系统内存严重不足的时候就会抛出这个异常(PS:注意这是一个Error,不是一个Exception,所以当我们要catch异常的时候要注意哦)
  • 既然要说OutOfMemoryError,那就得从这个类的加载说起来,那这个类什么时候被加载呢?你或许会不假思索地说,根据java类的延迟加载机制,这个类一般情况下不会被加载,除非当我们抛出OutOfMemoryError这个异常的时候才会第一次被加载,如果我们的系统一直不抛出这个异常,那这个类将一直不会被加载。说起来好像挺对,不过我这里首先要纠正这个说法,要明确的告诉你这个类在jvm启动的时候就已经被加载了,不信你就执行java -verbose:class -version打印类的加载信息来看看
  • jvm启动过程中加载了OutOfMemoryError这个类,并且创建了好几个OutOfMemoryError对象,每个OutOfMemoryError对象代表了一种内存溢出的场景,比如说Java heap space不足导致的OutOfMemoryError,抑或Metaspace不足导致的OutOfMemoryError,上面的代码来源于JDK8,所以能看到metaspace的内容,如果是JDK8之前,你将看到Perm的OutOfMemoryError

何时抛出OutOfMemoryError

  • 要抛出OutOfMemoryError,那肯定是有地方需要进行内存分配,可能是heap里,也可能是metsapce里(如果是在JDK8之前的会是Perm里),不同地方的分配,其策略也不一样,简单来说就是尝试分配,实在没办法就gc,gc还是不能分配就抛出异常。

  • 正确情况下对象创建需要分配的内存是来自于Heap的Eden区域里

    • 当Eden内存不够用的时候
    • 某些情况下会尝试到Old里进行分配(比如说要分配的内存很大),如果还是没有分配成功
    • 于是会触发一次ygc的动作,而ygc完成之后我们会再次尝试分配,如果仍不足以分配此时的内存
    • 那会接着做一次full gc(不过此时的soft reference不会被强制回收),将老生代也回收一下,接着再做一次分配
    • 仍然不够分配那会做一次强制将soft reference也回收的full gc
    • 如果还是不能分配,那这个时候就不得不抛出OutOfMemoryError了。这就是Heap里分配内存抛出OutOfMemoryError的具体过程了。

内存泄漏(Memory Leak)

  • 你用资源的时候为他开辟了一段空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出
  • 强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM也不会去回收他指向的对象
  • 一般我们所说的内存泄漏指的是堆内存的泄露,堆内存是指程序从堆中分配的,大小随机的用完后必须显示释放的内存,C++/C中有free函数可以释放内存,java中有垃圾回收机制不用程序员自己手动调用释放;如果这块内存不释放,就不能再用了,这就叫这块内存泄漏了

内存泄漏的分类

常发性内存泄漏
  • 发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏
  • 发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏。
  • 发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
隐式内存泄漏
  • 程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

内存泄漏的原因

  • JVM引入了垃圾回收机制,垃圾回收器会自动回收不再使用的对象,了解JVM回收机制的都知道JVM是使用引用计数法和可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)
静态集合类
  • 如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
  • 简单而言,长生命周期的对象持有短生命周期对象的引用尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收
各种连接,如数据库连接、网络连接和IO连接等
  • 在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象
  • 否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏
变量不合理的作用域
  • 一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
public class UsingRandom {

		private String msg;

		public void receiveMsg(){
		
		readFromNet();// 从网络中接受数据保存到msg中
		
		saveDB();// 把msg保存到数据库中
    }
}
  • 通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏
  • 实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间
内部类持有外部类
  • 如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
改变哈希值
  • 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
import java.util.Arrays;
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}


缓存泄漏
  • 内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘,对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值
监听器和回调
  • 内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存他的若引用,例如将他们保存成为WeakHashMap中的键

垃圾回收理论

  • 程序计数器、虚拟机栈、本地方法栈,3个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。
  • 垃圾:存在于内存中的、不会再被使用的对象。
  • 回收:将内存空间空闲的区域腾出来
  • 如果大量不会被使用的对象一直占用空间不放,需要内存空间时,无法使用这些被垃圾对象占用的内存,从而有可能导致内存溢出

确定对象是否存活

引用计数法

引用计数法的实现
  • Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。
  • 引用计数法的实现是这样的,给对象添加一个引用计数器,每当对象被引用的时候,就给这个计数器家1,当引用失效的时候就减1,当引用计数为0 的时候,就认为这个对象不再被使用了(因为对象被创建的时候,就已经存在一个引用了,为0的时候就证明最初的那个引用也失效了)
优缺点
  • 优点是实现简单,缺点是存在循环引用,并且每个对象都维护一个引用计数器有一定的消耗

如上图,当两个引用失效的时候,堆中的两个对象(都有一个Object 字段),都应该被回收;
但是当两个对象的字段有相互引用的时候,这两个对象都不能被释放,即使"引用1"和"引用2"都没用了

如上图所示,这个时候,因为对象之间的循环引用,导致计数器的值依然不为0,对象依然不能被回收
代码演示
  • 演示jvm 采用的不是引用计数法
public class ReferenceCountGC {
    public Object instance;
    private static int _1MB = 1024 * 1024;
    private byte[] bigSize = new byte[8 * _1MB];

    public static void main(String[] args) {
        ReferenceCountGC a = new ReferenceCountGC();
        ReferenceCountGC b = new ReferenceCountGC();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
    }
}

  • 加上垃圾回收之后

  • 从上面的结果可以看出,垃圾回收机制,的确将16M的内存空间成功回收了,所以可以判定JVM的垃圾回收算法用的不是引用计数法

可达性分析

  • 通过一系列名为"GC Roots" 的对象(集合,一些列的对象)作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GCRoots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的就自然被判定为死亡。注意:tracing GC的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。

GC ROOTS
  • GC ROOTS 可以看做图论 G=(V, E)中的点,这些点的定义是人为的,也就是你定义的比较重要的点,然后回收的对象就是和这些重要的点,都没有连线的点(对象),这里的连线包括间接和直接两种。
  • 说白了GC ROOTS 其实就是一组活跃的引用,主要在全局引用(类静态属性和常量)与执行上下文(当前栈帧的局部变量表)中,其实主要就是系统启动的时候的对象和当前正在用的对象
  • 被常量、静态变量、全局变量、运行时方法中的变量直接引用的对象,原则上不能被GC释放
虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 其实这里应该是当前栈帧中引用的对象,因为栈中引用的对象正在使用,所以栈帧的局部变量表可以作为GC ROOTS 对象(注意随着方法的调用接结束,栈帧弹出,那些被引用的对象就可以被回收了),主要包括当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
方法区中类静态变量引用的对象
  • 因为类不被回收的时候,静态变量就可能被使用到,所以这些变量不能被回收,所以类的静态变量可以作为GC ROOTS
方法区中常量引用的对象
  • 常量引用的对象也不能被回收,是因为这个常量也在其他地方呗引用到了
本地方法栈(即一般说的 Native 方法)中JNI引用的对象、
  • 本地方法栈和虚拟机栈是一样的道理
GC ROOTS 到底有哪些
mat 的安装
  • 下载 https://www.eclipse.org/mat/
  • 双击得到应用软件,直接点击报错

  • 报错日志如下

  • 右击程序,显示包内容

  • 在如下配置文件中添加两行

  • 这个时候可以在命令行里面使用,方便起见配个环境变量,也可以直接 mat程序移动到应用程序文件夹,然后就可以直接在应用程序里面打开了,这两种方式都是很方便的,可以选择自己喜欢的方式

mat使用
  • 打开软件,界面如下

  • 点击 Open a Heap Dump(关于dum 文件怎么来,自行百度)
    • 可以使用这个jvm参数,然后不断创建对象生成dump 文件 -Xmx20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/XXX/workspace/tmp/jvm -XX:+PrintGCDetails -verbose:gc
    • 或者使用jmap 命令
  • 选择dump 文件之后,点击如下菜单(数据库形状)
    • Java Basics->GC Root

  • 即可看到当前的gc roots 有哪些

优点
  • 更加精确和严谨,可以分析出循环数据结构相互引用的情况
缺点
  • 实现比较复杂、需要分析大量数据,消耗大量时间、分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题)。

垃圾的两次标记

  • 在可达性分析中,不可达的对象,也并非是"非死不可",这个时候它们暂时处于"缓刑"阶段,要宣告一个对象死亡,至少要经过两次标记

第一次标记

  1. 如果对象进行可达性分析算法之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。
筛选条件
  • 判断此对象是否有必要执行finalize()方法。
筛选结果
  • 当对象没有覆盖finalize()方法、或者finalize()方法已经被JVM执行过,则判定为可回收对象。
  • 如果对象有必要执行finalize()方法,则被放入F-Queue队列中。稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;
  • 有必要指的是finalize() 方法被重写了,但是没有被执行过。

第二次标记

  • GC对F-Queue队列中的对象进行二次标记。
  • 如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。
自我救赎
  • 被放入F-Queue中的对象,在执行finalize 方法的时候,将自己和某个类的变量,进行关联,这个时候它将逃脱垃圾回收。

finalize() 方法

  • finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用
并不提倡在程序中调用finalize()来进行自救。建议忘掉Java程序中该方法的存在。因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)

代码演示

对象的自我救赎之路1
  • 从引用的角度进行一次演示
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    public static void isAlive(FinalizeEscapeGC obj){
        if (obj==null){
            System.out.println("我死了");
        }else{
            System.out.println("我还活着");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        SAVE_HOOK = null;
        System.gc();
        // 优先级比较低,等待这个方法被执行
        TimeUnit.SECONDS.sleep(5);
        isAlive(SAVE_HOOK);
        System.out.println("==================================");
        SAVE_HOOK = null;
        System.gc();
        TimeUnit.SECONDS.sleep(5);
        isAlive(SAVE_HOOK);

    }
}
  • 执行结果

1. 对象的finalize() 方法最多被执行一次
2. finalize() 方法的确被垃圾回收器触发过
3. 这里有个共识就是当引用被置为null 的时候,我们认为对象就是垃圾了,应该被回收,所以这里是通过对象是否null ,判断对象是否活着
对象的自我救赎之路2
  • 从内存占用的角度进行一次演示,基本代码如下
/**
 * jvm 参数如下
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class FinalizeEscapeGC2 {

    public static FinalizeEscapeGC2 SAVE_HOOK;
    private static int _1MB = 1024 * 1024;
    private byte[] bigSize = new byte[8 * _1MB];

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeEscapeGC2.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK= new FinalizeEscapeGC2();
        SAVE_HOOK = null;
        System.gc();
        TimeUnit.SECONDS.sleep(5);
       /* SAVE_HOOK = null;
        System.gc();*/
    }
}
  • 直接执行上面的代码,输出如下
  • 去掉两行注释,然后执行,输出如下

引用的分类与应用

  • 无论引用计数法还是可达性分析,都离不开引用本身
  • 引用本身的定义还是比较简单的,java 里面有Reference类,一个Reference类型的对象的数据中存储的数值代表的是另外一块内存的起始地址,那么这块内存就是代表这个一个引用,而这个对象就是一个引用对象。

引用的分类

  • 上面关于引用定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态
  • 对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。 我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。 很多系统的缓存功能都符合这样的应用场景
  • 强引用怎么样都不会回收,软引用不足的时候才回收,弱引用一旦发现就回收。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SJJfuXXI-1642564631012)(https://note.youdao.com/yws/res/33175/0310168BC916499B9C9FDC6DB43F0F71)]

强引用
  • 强引用是使用最普遍的引用,类似 Object obj = new Object(); 这类的引用,只有强引用还存在,GC就永远不会收集被引用的对象。当内存空间不足,Java虚拟机宁愿抛出OutOfMmoryError错误,使程序异常终止,也不会靠随意回收具有强引用 对象来解决内存不足的问题。
  • 对于集合中的对象,应在不使用的时候移除掉,否则会占用更多的内存,导致内存泄漏
软引用
  • 指一些还有用但并非必需的对象。直到内存空间不够时(抛出OutOfMemoryError之前),才会被垃圾回收。采用SoftReference类来实现软引用——其实也就是说当对象是Soft reference可达时,gc会向系统申请更多内存,而不是直接回收它,当内存不足的时候才回收。
  • 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
软引用关联的对象不会被GC回收。JVM在分配空间时,若果Heap空间不足,就会进行相应的GC,但是这次GC并不会收集软引用关联的对象,但是在JVM发现就算进行了一次回收后还是不足(Allocation Failure),JVM会尝试第二次GC,回收软引用关联的对象。
应用范围
  • 像这种如果内存充足,GC时就保留,内存不够,GC再来收集的功能很适合用在缓存的引用场景中。在使用缓存时有一个原则,如果缓存中有就从缓存获取,如果没有就从数据库中获取,缓存的存在是为了加快计算速度,如果因为缓存导致了内存不足进而整个程序崩溃,那就得不偿失了
弱引用
  • 用来描述非必须对象。只能生存到下一次垃圾回收的时候,当垃圾收集器工作时就会回收掉此类对象(被软引用引用的对象),无论内存时候够用。采用WeakReference类来实现弱引用。
  • 在垃圾回收器扫描它所管辖的内存区域过程中,一旦gc发现对象是weakReference可达,就会把它放到ReferenceQueue中,等下次gc时回收它
虚引用
  • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用获得一个对象实例, 唯一目的就是能在这个对象被回收时收到一个系统通知, 采用PhantomRenference类实现
  • 虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用或者弱引用关联着对象,那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null。
  • jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。

引用的应用

编程建议

根据GC的工作原理,我们可以通过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。一些关于程序设计的几点建议:

1.最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为 null.我们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC 回收它们一般效率较低。如果程序允许,尽早将不用的引用对象赋为null.这样可以加速GC的工作。

2.尽量少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。

3.如果需要使用经常使用的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory.

4.注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象(dangling reference),造成内存浪费。

5.当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。使用增量式GC可以缩短Java程序的暂停时间。

软引用的应用
  • 基本代码
/**
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class SoftReferenceDemo {
    public static final int _4MB = 4 * 1024 * 1024;
    public static void hradReference(){
        List<byte[]> lists = new ArrayList<>();
        for (int i = 0; i <5 ; i++) {
            System.out.println(i);
            lists.add(new byte[_4MB]);
        }
    }

    public static void softReference(){
        List<SoftReference<byte[]>> lists = new ArrayList<>();
        for (int i = 0; i <5 ; i++) {
            System.out.println(i);
            lists.add(new SoftReference<byte[]>((new byte[_4MB])));
        }
        /**
         * 输出null 可以看出对象已经被回收,但是可以看到引用本身还是存在的,只是软引用引用的对象被回收了
         */
        for (SoftReference<byte[]> list : lists) {
            System.out.println( list.get());
        }
    }

    public static void normalObject(){
        List<byte[]> lists = new ArrayList<>();
        for (int i = 0; i <5 ; i++) {
            System.out.println(i);
            lists.add(new byte[_4MB]);
        }
    }

    public static void main(String[] args) {
        //softReference();
        normalObject();
    }
}
  • 普通的方法运行

  • 软引用的方法运行

虚引用的应用
  • 基本代码
    public static void weakReference() {
        List<WeakReference<byte[]>> lists = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
            lists.add(new WeakReference<byte[]>((new byte[_4MB])));
        }
        /**
         * 输出null 可以看出对象已经被回收,但是可以看到引用本身还是存在的,只是虚引用引用的对象被回收了
         * 和 SoftReferenceDemo 比起来只回收了一个,并且是在minor-gc 下回收的
         */

        for (WeakReference<byte[]> list : lists) {
            System.out.println(list.get());
        }

        System.gc();
        /**
         * 在一次full- GC 之后全部被回收了
         */
        for (WeakReference<byte[]> list : lists) {
            System.out.println(list.get());
        }
    }

方法区的回收

  • 方法区在hotspot虚拟中,jdk7 以前的版本中的实现是永久代
  • 虚拟机规范中,没有强制要求,必须实现方法区的垃圾回收,而且在方法区进行垃圾回收一般"性价比"比较低,主要是因为方法区的垃圾回收条件比较苛刻。
  • 主要回收无用的类和常量池中废弃的常量

回收无用的类

  • 该类所有的实例都已经被回收,也就是Java堆中无任何改类的实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对同时满足这三个条件的类进行回收,但不是必须进行回收的。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用 -verbose:class和 -XX:TraceClassLoading 和 XX:TraceClassUnLoading 查看类加载和卸载信息。

回收无用的常量

  • 假如一个字符串“abc” 已经进入常量池中,但当前系统没有一个string对象是叫做abc的,也就是说,没有任何string对象的引用指向常量池中的abc常量,也没用其他地方引用这个字面量。如果这是发生内存回收,那么这个常量abc将会被清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

STW

  1. Stop-The-World(STW):垃圾回收时,会产生应用程序的停顿,整个应用被卡死,没有任何响应。
  2. 目的:终止所有线程执行,此时没有新的垃圾产生,保证系统状态在一个瞬间的一致性,益于标记垃圾对象

详解

  • 在jvm里有这么一个线程(VMThread)不断轮询它的队列,这个队列里主要是存一些VM_operation的动作,比如最常见的就是内存分配失败要求做GC操作的请求等,在对gc这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的stop the world(STW),整个进程相当于静止了

哪些内存要回收

  • java内存模型中分为五大区域已经有所了解。我们知道程序计数器、虚拟机栈、本地方法栈,由线程而生,随线程而灭,其中栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来【忽略JIT编译器做的优化,基本当成编译期间可知】,当方法或线程执行完毕后,内存就随着回收,因此无需关心。

  • Java堆、方法区则不一样。方法区存放着类加载信息,但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不一样【只有在运行期间才可知道这个方法创建了哪些对象没需要多少内存】,这部分内存的分配和回收都是动态的,gc关注的也正是这部分的内存。

总结

  • 垃圾回收的区域——公共区域(堆和方法区域)
  • 垃圾回收的对象——无引用(可达性分析和引用计数法)
  • GC ROOTS 对象——正在使用的对象

思考题

  • 文中的代码因为内存溢出而抛出异常,为什么至少是两次full gc 之后才抛出。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐