阅读 292

Java-第十四部分-JVM-垃圾回收相关概念(内存溢出、并发并行、安全点及引用等)

JVM全文

System.gc()

  • 显式触发Full GC,同时对老年代和新生代进行回收,附带一个免责声明,无法保证对垃圾收集器的调用
  • 性能基准测试时,会在运行之间调用
  • 底层调用本地方法
public static void gc() {
    //效果一致
    Runtime.getRuntime().gc();
}

public native void gc();
复制代码
  • 强制调用失去引用的对象的finalize()方法
System.runFinalization();
复制代码
  • 实例
public class GCTest {
    public static void main(String[] args) {
        //当GC1执行完成,GC1的栈变量的引用都会失效,进行回收
        GC1();
        System.gc();
    }
    public static void GC1() {
        {
            byte[] buffer = new byte[1024 * 1024];
        }
        //执行完上面的作用域后,buffer引用并不会直接失效,而是是一个可重用的状态
        //如果没有新的变量产生,这个引用还是存在的
        //定义了value,就会替换buffer变量,引用失效,gc后就被回收
        int value = 10;
        System.gc();
    }
}
复制代码

内存溢出与内存泄漏

  • 内存指的并不是物理内存,而是虚拟机内存,取决于磁盘交换区设定的大小
  • 内存溢出 OOM
  1. 应用程序占用的内存增长速度非常快,造成垃圾回收跟不上内存消耗速度
  2. 在抛出OOM异常之前,会进行独占式的Full GC,回收大量内存;JVM会尝试回收软引用指向的对象
  3. 当分配超大对象,超过堆的最大值,JVM判断垃圾收集并不能解决这个问题,直接抛出OOM
  4. 解释,没有空闲内存,且垃圾回收器也无法提供更多内存
  • 原因
  1. 堆内存设置不够,存在内存泄漏问题
  2. 代码中创建了大量大对象,长时间不能被垃圾回收器收集,如在运行时存在大量动态类型生成的场合、intern字符串缓存占用太多空间(永久代时期)
  • 内存泄漏 Memory Leak
  1. 对象不再被程序用到了,但是GC又不能进行回收
  2. 某些操作导致对象的生命周期很长,导致OOM,如,一个可以定义在方法内的变量定义成了成员变量,并用static修饰;web程序,对象数据存储到上下文对象或者会话对象中
  3. 一旦发生内存泄漏,内存会被逐步蚕食,直到耗尽所有内存,有可能导致OOM
  4. 对于java来说,这个对象是不再被使用的,但是对这个对象的的引用是忘记断开的,根据可达性分析仍能找到这个对象,因此不能被GC回收,会导致内存泄漏
  5. 对于引用计数器的情况下,循环引用会导致内存泄漏
  • 举例
  1. 单例模式,单例的生命周期和应用程序一样长,如果持有对外部对象的引用,这个外部对象是不能被回收的,导致内存泄漏
  2. 一些提供close的资源未关闭,导致内存泄漏,连接一直存在,如数据库连接、网络连接和io连接等必须手动close,对于访问外部资源的对象,必须进行即时关闭,方便进行垃圾回收

STW

  • Stop The World,GC事件发生过程中,产生应用程序的停顿,导致整个用户线程被暂停,没有任何响应
  1. 可达性分析枚举根节点会导致执行线程挺短
  2. 分析工作必须在一个确保一致性的快照中进行,分析过程中对象引用关系不能发生变化,冻结在某个时间点
  3. 需要减少STW的出现
  4. 与采用哪种GC无关,所有的GC都有STW;G1也不能完全避免,只能提高回收效率,尽可能缩短暂停时间
  5. 在后台自动和自动完成的,在用户不可见的情况下,把用户称唱的工作线程停掉
  6. 开发正不要调用System.gc() 会导致STW发生

并行与并发

  • 并发,在一个时间段中,有多个程序处于运行期间,并且在同一个处理器上运行
  1. 多个事情,在同一个时间段内同时发生,互相抢占资源
  2. CPU切换执行
  3. 并不是真正意义上的同时进行,只是CPU把一个时间段划分成几个时间片段,然后在这几个时间片段来回切换,每个时间片段执行某个程序上的任务
  • 并行,当有一个以上CPU时,一个CPU执行一个进程,另一个CPU执行另一个进程,两个进程互不抢占CPU资源
  1. 多个事情,在同一个时间点上同时发生,不抢占资源
  2. 决定并行的不是CPU数量,而是CPU核心数量,一个CPU多核也可以实现
  3. 只有在多CPU或者一个CPU多核才会发生并行
  • 垃圾回收中,针对的是垃圾收集线程是否在同一个CPU上
  1. 并行,多条垃圾收集线程并行运行,在多个CPU上同时执行,用户线程处于等待状态ParNew/Parallel/Scavenge/Parallel Old
  2. 串行,单线程执行,如果内存不够,程序暂停,启动垃圾回收器进行垃圾回收
  3. 并发,用户线程和垃圾回收线程同时执行,垃圾回收器与用户线程在两个CPU上运行,垃圾回收线程在执行时不会停顿用户程序的运行,但是会有STW,需要保证可达性的准确性,可能会交替执行,CMS/G1

安全点与安全区域

  • 安全点,Safe Point,程序执行时并非在所有地方都能停顿下来,开始GC,只有在特定的位置才能停顿下来
  1. 如果安全点太少,可能导致GC等待时间太长
  2. 如果太频繁,导致运行的性能问题
  • 安全点的选择,通常会根据是否具有让程序长时间执行的特征为标准,选择一些执行时间较长的指令作为安全点,如方法调用、循环跳转和异常跳转
  1. 运行到这几个地方,线程的一些状态可以被确认,便于保证对象的引用不发生改变
  2. 循环的末尾 
  3. 方法临返回前 / 调用方法的call指令后 
  4. 可能抛异常的位置
  5. 主要的目的就是避免程序长时间无法进入Safe Point。比如 JVM 在做 GC 之前要等所有的应用线程进入安全点,如果有一个线程一直没有进入安全点,就会导致 GC 时 JVM 停顿时间延长。比如这里,超大的循环导致执行 GC 等待时间过长。
  • 保证所有线程都跑到最近的安全点停顿
  1. 抢先式中断,中断所有线程,如果还有线程在不安全点,恢复线程,让线程跑到安全点
  2. 主动式终端(基本采用),设置一个终端标志,各个线程运行到安全点时,主动轮询这个标志,如果中断标志为真,线程主动暂停
  • 安全区域,Safe Region,线程处于不执行的状态,sleep/blocked状态,设置一段区域,这段区域内对象的引用关系不发生变化,在这个区域内任何位置开始的GC都是安全的,被扩展的安全点
  1. 当程序运行到安全区域时,标识线程已经进入Safe Region,如果这段时间发生GC,JVM会忽略标识为Safa Region状态的线程,不对该线程进行STW,该线程等待被唤醒
  2. 当线程被唤醒,即将离开安全区域时,会检查JVM是否已经完成GC,如果完成则继续运行,否则必须等待直到收到可以安全离开安全区域的信号

引用

  • 能够描述一类对象,当内存空间还足够,则保留在内存;如果内存空间在进行垃圾回收后,还是很紧张,抛弃这些对象
  • 强引用Strong Reference、软引用Soft Reference、弱引用Weak Reference、虚引用Phantom Reference,引用强度依次减弱

image.png

  1. 强引用,可触及的只要强引用关系还在,垃圾回收器永远不会回收掉引用对象
  2. 软引用,软可触及的,如果系统将要发生内存溢出之前,将会把这些对象列入二次回收范围内;如果报OOM之前的回收,仍然没有足够内存,就会对软引用对象进行回收,如果回收后,还没有足够的内存,抛出OOM异常
  3. 弱引用,弱可触及的,只要垃圾收集器工作,无论内存是否足够,都会被回收
  4. 虚引用,虚可触及的,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,无法通过虚引用获得一个对象实例,为一个对象设置虚引用关联的唯一目的,就是能在这个对象被收集器回收时,收到一个系统通知,对象回收跟踪

强引用

  • new的形式都是强引用,默认引用类型,可以直接访问目标对象
  • 强引用的对象都是可触及的,即使抛出OOM,垃圾收集器也不会回收
  • 是造成Java泄漏的主要原因之一

软引用

  • 内存不足及回收
  • 描述还有用,但非必需的对象,只要被软引用关联着的对象,在系统发生内存溢出异常前,会把这些对象列为回收范围进行二次回收,如果这次回收之后还没有足够的内存,抛出OOM
  • 通常用来实现内部敏感的缓存,如高速缓存MyBatis内部类,保证使用缓存的同时,不会耗尽内存
  • 垃圾回收器在某个时刻决定回收软可触及的对象时,清理软引用,并可选地把引用存放在一个引用队列
  • 类似弱引用,JVM会尽量让软引用存活时间长一点
SoftReference softReference = new SoftReference<Person>(new Person("zhangsan",123));
//获取引用实例
System.out.println(softReference.get());
System.gc();
//堆空间内存足够,不会回收软引用可达对象
System.out.println(softReference.get());

try {
    //内存不够
    byte[] b = new byte[1024 * 1024 * 8];
    //内存紧张,需要回收掉软引用对象,才能放得下这个数组
    //byte[] b = new byte[1024 * (7168 - 483)];
} catch (Exception e) {
    e.printStackTrace();
} finally {
    //内存不足,回收软引用可达对象,null
    System.out.println(softReference.get());
}
复制代码

弱引用

  • 进行GC时,发现即回收
  • 由于垃圾回收线程优先级较低,不能及时发先,可以存在较长的时间
  • 在构造软引用时,指定一个引用队列
  • 适合保存可有可无的缓存数据
  • 三级缓存,内存(软引用、弱引用来保存)、本地、网络
WeakReference<Person> test = new WeakReference<>(new Person("test", 13));
System.out.println(test.get());
System.gc();
System.out.println(test.get());
复制代码
  • 面试题 weakHashMap,在内存不足会被及时回收
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    ....
}
复制代码

虚引用

  • 对象回收跟踪,最弱的一个,一旦被回收,就会将这个虚引用添加到引用队列中
  • 一个对象是否有虚引用的存在,完全不会决定对象的生命周期
  • 一个对象只有虚引用,就跟完全没有被引用是一样的,随时会被回收
  • 通过虚引用get()也无法获取被引用的对象
  • 唯一目的在于跟踪垃圾回收过程,能在这个对象被收集器回收的时候收到一个系统通知
  • 可以将一些资源释放操作,放在虚引用中执行操作
People people = new People("zhangsan", 13);
ReferenceQueue<People> phantomQueue = new ReferenceQueue<>();
PhantomReference<People> pf = new PhantomReference<>(people, phantomQueue);
people = null;
复制代码
  • 守护线程,当程序中没有非守护线程时,线程直接结束
  1. java中只有两种线程 用户线程、守护线程
  2. 垃圾回收线程就是一种守护线程
  3. 当没有用户线程时,虚拟机退出
  • 实例
public class PhantomReferenceTest {
    static PhantomReferenceTest obj; //当前对象声明
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null; //引用队列
    public static class CheckRefQueue extends Thread {
        @Override
        public void run() {
            while (true) {
                if (phantomQueue != null) {
                    PhantomReference<PhantomReferenceTest> objt = null;
                    try {
                        objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    if (objt != null) {
                        System.out.println("虚引用被加入引用队列");
                    }
                }

            }
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize()...");
        //复活
        obj = this;
    }

    public static void main(String[] args) {
        //启动线程检查引用队列
        Thread t = new CheckRefQueue();
        t.setDaemon(true); //设置为守护线程
        t.start();
        //初始化
        phantomQueue = new ReferenceQueue<>();
        obj = new PhantomReferenceTest();
        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<>(obj, phantomQueue);
        try {
            //null 获取不到虚引用的对象实例
            System.out.println(phantomRef.get());
            //去除强引用
            obj = null;
            //第一次gc,将虚引用标记为不可达,但是调用finalize()方法,导致其复活
            System.gc();
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj is dead.");
            } else {
                System.out.println("obj is live.");
            }
            //第二次gc
            obj = null;
            System.gc();
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj is dead.");
            } else {
                System.out.println("obj is live.");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

终结器引用

  • FinalReference
class FinalReference<T> extends Reference<T> {

    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}
复制代码
  1. 实现对象的finalize()方法
  2. 无需手动编码,内部配合引用队列使用
  3. 在GC时,终结器引用入队,由Finalizer线程,通过终结器找到被引用对象并调用finalize()方法,第二次GC时才能回收对象
文章分类
后端
文章标签