• JVM进阶(3)


    一)什么是垃圾?

    垃圾指的是在应用程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾,如果不及时的针对内存中的垃圾进行清理,那么这些垃圾对象所占用的内存空间可能一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至有可能导致内存溢出

    JVM规范说了并不需要必须回收方法区,不具有普遍性,元空间使用的是JVM之外的内存

    二)如何判断一个对象是否是垃圾:
    2.1)引用计数器:

    引用计数器优点:实现简单,垃圾对象便于标识,判断效率高,回收没有延迟性

    引用计数器缺点:效率要比可达性分析要强,随时发现,随时回收,实现简单,但是可能存在内存泄漏

    1)它需要单独的字段存储计数器,这样的做法增加了存储空间的开销

    2)每一次赋值都是需要更新计数器,伴随着加法和减法的操作

    3)引用计数器又一个严重的问题,就是无法处理循环引用的问题,这是一条致命缺陷

    虽然在JAVA中没有使用循环引用但是Python中使用了两种方法解决了这个问题:

    1)手动解除,很好理解,就是在合适的时机,接触引用关系

    2)使用弱引用weakref,

    2.2)可达性分析:

    局部变量表,静态引用变量,通过引用链关联的引用链是不会被回收,局部变量表天然作为GCROOTS,就是只是进行新生代回收的时候老年代的引用也可以作为GCROOTS

    1. 1)虚拟机栈中引用的对象(栈帧中的本地方法表)
    2. 2)方法区中(1.8称为元空间)的类静态属性引用的对象
    3. 一般指被static修饰的对象,加载类的时候就加载到内存中
    4. 3)方法区中的常量引用的对象。
    5. 4)本地方法栈中的JNI(native方法)引用的对象。
    6. 注意即使可达性算法中不可达的对象也不是一定要马上被回收还有可能被抢救一下
    7. 要真正宣告对象死亡需经过两个过程:
    8. 1)可达性分析后没有发现引用链
    9. 2)查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。

    如果要是使用可达性分析算法来判断内存是否要进行回收,那么分析工作必须要在一个能够保持一个一致性的快照来进行,这一点不满足的话分析结果的准确性就无法保证,这一点也就是GC必须进行STW的一个重要原因,即使是号称几乎不会发生停顿的CMS垃圾回收器枚举根节点的时候也是必须要停顿的

    三)对象的finalize方法详解:

    1)JAVA语言提供了对象终止机制来允许开发人员提供对对象销毁之前的自定义处理逻辑,当垃圾回收器发现没有引用指向一个对象的时候,就是垃圾回收器在进行回收此对象之前,总是会调用这个对象的finalize方法,当垃圾回收器发现没有任何一个引用指向该对象的时候,总是会调用这个对象的finalize()方法,finalize()方法允许在子类中被重写,用于在垃圾回收时进行资源释放和垃圾清理的工作,关闭文件,套接字和数据库连接等等

    2)永远不要试图调用某一个对象的finalize方法,应该交给垃圾回收器来调用

    2.1)finalize方法可能会导致对象复活

    2.2)finalize()方法执行时间是没有保障的,他完全由GC线程所决定,极端情况下,如果不发生GC,那么一个糟糕的finalize()方法会影响程序的执行性能

    如果说所有的根节点都无法访问到某一个对象,说明该对象已经不再被使用了,一般来说,此对象需要被回收,但事实上,也并非是非死不可的,这个时候他们暂时处于唤醒状态,一个无法触及的对象很有可能在某一个条件下复活自己,如果这样没那么对于他的回收就是极其不合理的,为此,定义虚拟机中的对象可能的三种状态:

    1)可触及的:从根节点开始可以到达这个对象

    2)可复活的:对象的所有引用都被释放,但是对象很有可能在finalize()中复活

    3)不可触及的:对象的finalize()方法被调用并且没有复活,那么就会进入到不可及状态,不可触及的对象不可能复活,因为finalize()方法只会被调用一次;以上三种状态中,是由于finalize()方法的存在进行的区分,只有在对象不可触及的时候才可以被回收

    3)所以说判断一个对象是否可以进行回收至少要经历两次标记过程

    3.1)如果说对象A到Gcroots不存在引用链,那么就进行第一次标记

    3.2)如果筛选,进行判断对象是否执行了finalize()方法

    a)如果对象没有重写finalize()方法或者是finalize()方法已经被虚拟机调用过,那么虚拟机不会再重新调用该方法,直接该对象就被标记成不可达的

    b)如果对象A重写了finalize()方法,还没有被执行过,那么该对象会被插入到一个队列中,这是由虚拟机自动创建的低优先级的finalizer线程触发其finalizer方法执行

    c)finalize()方法是对象进行逃脱死亡的最后机会,稍后GC就会对队列中的对象做第二次标记,如果该对象和引用链上面的任意一个对象建立了联系,那么在第二次标记的过程中此对象会被移出即将回收的集合,之后,对象会再次出现没有引用存在的情况,在这种情况下fnalize()方法不会被再次调用,对象会直接变成不可触及的状态

    代码执行两次,一次分为finalize()方法没有被注释,一种有注释

    1. public class Test {
    2. public static Test obj;//这是一个类变量
    3. // @Override
    4. // protected void finalize() throws Throwable {
    5. // System.out.println("调用当前链上的finalize方法");
    6. // obj=this;//当前带回收的对象在finalize方法上和一个引用链上面的对象建立了联系
    7. // }
    8. public static void main(String[] args) throws InterruptedException {
    9. obj=new Test();
    10. //对象第一次拯救自己
    11. obj=null;
    12. System.gc();//调用垃圾回收器
    13. System.out.println("第一次GC");
    14. //因为finalizer线程优先级很低,主线程暂停2s来等待他
    15. Thread.sleep(3000);
    16. if(obj==null){
    17. System.out.println("对象已经死了");
    18. }else{
    19. System.out.println("对象还活着");
    20. }
    21. obj=null;
    22. System.gc();//调用垃圾回收器
    23. System.out.println("第二次GC");
    24. //因为finalizer线程优先级很低,暂停2s来等待他
    25. Thread.sleep(3000);
    26. if(obj==null){
    27. System.out.println("对象已经死了");
    28. }else{
    29. System.out.println("对象还活着");
    30. }
    31. }
    32. }
    四)垃圾回收算法:

    垃圾回收任何时候都可能,当系统觉得你内存不足了就会开始回收常见的比如分配对象内存不足时这里的内存不足有可能 不是占用真的很高,可能是内存足够,但是没有连续内存空间去放这个对象,当前堆内存占用超过阈值时,手动 调用 System.gc() 建议开始GC时,系统整体内存不足时等

    4.1)标记清除算法:

    标记是非垃圾的对象就是可达的对象,然后清除的是垃圾对象,要先递归进行遍历所有可达对象,然后清除的时候需要再开始遍历一遍整个内存空间,还需要进行维护空闲列表

    就比如说我们的硬盘,只要你不小心点击了格式化,此时也不是真正的进行格式化,只是标记性删除,但是千万不要再向里面存放数据,因为数据会覆盖,就不好恢复了

    当堆中有效的空间被耗尽的时候就会停止整个程序进行STW,首先进行标记,然后进行清除

    1)标记:从引用根节点进行标记,标记所有可达的对象,也就不是垃圾的对象,一般是在对象的header头里面标记成可达的对象;

    2)清除:对整个堆内存进行从头到尾的进行线性的遍历,如果发现某一个对象在header中没有被标记,那么直接将其回收

    缺点:效率不算太高,在进行GC的时候需要终止整个应用程序,用户体验差,还有就是这种方式进行清理出来的空闲内存不是连续的,而是会产生内存碎片,还需要维护一个空闲列表

    注意这里面的清空并不是真正的清空,而是需要把消除的对象的地址保存在一个空闲列表里面,下次有新的对象需要加载的时候,要判断垃圾的位置空间是否足够,如果够就进行存放

    4.2)标记整理算法:内存利用率贼低

    首先经过可达性分析在A区找到可达的对象,一旦找到了可达的对象就不需要进行标记,直接将可达的对象进行复制算法放到另一块区域B,另一块空间的所有区域B的对象都是连续的

    将活着的内存空间分为两块,每一次只是用其中一块,再进行垃圾回收的时候将正在使用的内存中的存活对象复制到还没有被使用到的内存快里面,然后最后清楚正在使用到的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收

    1)因为在新生代,对于常规应用的垃圾回收,一般情况下是可以回收很多的内存空间的,回收性价比就比较高

    2)没有标记和清除过程,实现简单,运行高效

    3)复制过去以后保证空间的连续性,不会出现内存碎片问题

    缺点:维护引用和对象的地址映射

    1)需要两倍的内存空间

    2)回收的对象比较少,剩余存活的对象比较多,那么移动的对象比较多,但是还要大量维护指针和对象的关系,老年代不适合使用复制算法,因为很多对象都不死,老年代复制对象开销太大

    3)对于G1这种拆分成大量的Regin的GC,复制而不是移动,意味着GC需要维护Regin之间的对象引用关系,不管是内存占用还是空间开销也不少,如果系统中的垃圾对象很多,复制算法需要复制的存活的数量不算太大,或者说非常低才可以

    4.3)标记整理算法:

    从根节点标记所有被根节点引用的对象,将所有的存活对象压缩到内存的一端,按照顺序进行存放,最后清除所有边界以外的空间,还要移动位置,还要修改引用对象关系很麻烦,这个算法比标记清除算法效率还低

    标记压缩算法的最终效果就是等同于标记清除算法执行完成以后,再来进行一次碎片整理,二者的本质差异就是标记清除算法是一种非移动式的回收算法,标记压缩算法是非移动式的,是否移动回收后的存活对象是一项优缺点并存的风险策略,还可以看到标记的存活对象会被清理,需要按照内存地址进行依次排列,而没有被标记的内存会被回收掉清理掉,如此一来JVM在进行分配内存空间的时候,JVM只是需要维护一个内存的起始地址就可以了,这笔维护一个空闲列表节省了很多开销

    优点:消除了标记清楚算法中的的内存区域分散的特点,我们需要给新对象分配内存的时候,JVM只是需要持有一个内存的起始地址即可,消除了复制算法中内存减半的高额代价

    缺点:从效率上来说,标记整理算法要低于复制算法,移动对象的时候如果对象被其他引用所指向,还需要调整引用的地址,移动过程中,需要全程暂停用户应用程序;

    4.4)分代回收:

    1)在前面的这些算法中,没有一个算法可以完全代替其他算法,它们都具有自己独特的优势和特点,分代收集算法应运而生,分代收集算法是基于这样一个事实,不同对象的生命周期是不一样的,因此不同生命周期的对象可以采取不同的收集方式,一边用力啊提升回收效率,一般是吧JAVA堆分成新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,来提升垃圾回收的效率;

    2)在JAVA程序运行的过程中会产生大量的对象,其中有一些对象是和业务信息相关,比如说Http请求的session对象,线程,Socket连接,这类对象和业务直接挂钩,因此生命周期比较端,比如说String对象,由于不可变的特性,系统会产生大量的这些对象,甚至有的对象只使用一次就被回收,新生代:老年代=1:2,edin区:幸存者1区:幸存者2区;

    3)目前几乎所有的GC都是采用粉黛收集算法来执行垃圾回收的,在HotSpot虚拟机中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点

    年轻代:

    区域相比于老年代较小,对象的生命周期比较短,存活率低,回收比较频繁,这种情况复制算法的回收整理,速度是最快的,渎职算法的效率值和当前存活对象有关,因此很是适合年轻代的回收,而复制算法解决的是内存利用率不高的问题,通过两个幸存者区得到缓解

    老年代:

    区域比较大,对象的生命周期比较长,存活率高,回收不及年轻代频繁,这种情况存在大量存活度高的对象,复制算法明显是非常不合适的,一般是由标记整理或者是标记清楚算法的混合实现,标记阶段的开销和存活对象的数量成正比,清除阶段的开销和所管理区域的大小成正比,整理阶段的开销和存活对象的数据成正比;

    标记的开销和存活的对象成正比,因为标记只能标记存活的对象

    清除阶段要进行全堆空间线性的遍历,压缩或者是整理和存活对象的大小成正比

    以HotSpot中的CMS垃圾回收器为例,CMS是基于标记压缩清除来实现的,对于对象的回收效率很高,但是对于碎片问题,CMS会使用基于标记压缩算法的Serial Old回收器作为补偿机制,当内存回收不佳的时候,将采用Serial Old执行Full GC来达到对于老年代内存的管理

    对于STW的理解:

    先确定GCROOTS,枚举根节点,此时要进行Stop The World,确保数据的一致性

    stop the world停止的是用户线程,就是为保证一致性

    可达性分析算法中枚举根节点会导致所有Java执行线程停顿

    衡量一个垃圾回收器的标准就是吞吐量和低延迟

    4.5)增量收集算法:

    用户暂停时间/垃圾回收时间

    吞吐量:工作线程一共的执行业务的时间

    比如说我现在有一个房子,我一直不进行清理,一直制造垃圾,直到三个月之后才清理一次,此时清理的时间就比较长,阻隔用户线程的时间就比较长,但是如果说隔一会清理一会效果就会比较好,用户线程和回收线程协调交替执行,看样子就是并发的执行从而到达一种低延迟的行为,就是为了让用户感觉好一点;

    被STW中断的应用程序线程会在完成GC之后恢复,频繁的中断会让用户感觉像是网速不快造成的电影卡顿一样,CMS自称低延迟,开发中不要显示的进行GC,导致STW

    就是类似于洗衣服,假设现在我只是做两件事情:在宿舍给别人讲题和在卫生间洗衣服

    工作线程:在宿舍给别人讲题

    GC线程:在卫生间洗衣服

    吞吐量:给宿舍讲题时间越长,吞吐量越高

    停顿时间:在卫生间洗衣服越长,STW时间就越长

    如果想要达到极高的吞吐量,那么就少去卫生间洗衣服,一次洗的多一点,这样吞吐量就特别高

    停顿时间:多去洗衣服,每一次一会就回来,这样会使停顿时间最短,不让舍友等待时间过长,但是存在着从宿舍去卫生间和从卫生间回到宿舍时间的开销,会降低讲题总时间

    4.5)分区算法降低停顿时间,主要是保证低延迟而不是吞吐量

    有的分区存放大对象,有的区域存放小对象,回收区域的个数取决于时间的长短,可以控制可控时间,一般来说在相同条件下,堆空间越大,一次GC所需要时间就越长,有关GC产生的停顿时间也是越长的,为了可以更好地控制GC产生的停顿时间,可以将一块大的内存区域分割成多个小块内存区域,根据目标的停顿时间,每一次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿;

    分代算法将按照对象的生命周期长短划分成两个部分,分区算法是将整个堆空间划分成连续的不同的小区间,每一个兄啊去见都独立使用,独立回收,这种算法的好处就是可以控制一次会回收多个小区间

    五)System.gc()的理解:

    1)提醒JVM垃圾回收器去执行GC,但是不确实马上执行GC,底层是调用RunTime().getTime().gc(),进而也不能确定finlizle方法一定会被调用,Full GC 就是收集整个堆,包括新生代,老年代和方法区,但是调用System.gc系统做了免责声明,GC具体干不干,不怪这个方法,仅仅是提醒JAVA虚拟机进行垃圾回收,但是实际上是否进行垃圾回收System.GC()不保证,仅仅起到一个提醒JVM虚拟机进行垃圾回收

    2)在默认情况下,通过System.gc()或者是RunTime.getRunTime().gc()的调用,会显示的触发FullGC,同时针对于老年代和新生代进行回收,尝试释放被对其对象所占用的内存

    3)然而System.gc()调用负责一个免责声明,无法保证对于垃圾收集器的调用,JVM实现者通过System.gc()来决定与JVM的GC行为,垃圾回收应该是自动进行的,无需手动进行,特殊的情况下,比如说现在正在编写一个性能基准,可以在运行之间调用System.gc();

    4)做性能测试之前先进行GC,防止初始情况下一些因为内存不够的原因导致性能测试不太精准了,局部变量表第一个位置存放的是this

    System.runFinalization(),//强制调用失去引用的对象的finallize方法

    上面这个结果不会触发垃圾回收,虽然已经过了作用域,但是buffer的变量槽仍然没有被覆盖

    此时buffer所指向的对象会被回收,buffer作用域已经过了,所以buffer肯定用不上了,系统会判定buffer占用的slot为可覆盖的slot,局部变量表的第一个位置永远是this,一但value覆盖buffer所在的槽,buffer引用被覆盖,此时没有任何引用指向字节对象数组,所以此时触发System.gc就会回收垃圾;

    六)内存溢出:

    1)内存溢出相比于内存泄漏来说,他也是引起程序崩溃的罪魁祸首之一,由于GC在一直进行发展,所有一般情况下,除非应用长须占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太可能出现OOM

    2)大多数情况下,GC会进行各种年龄段的垃圾回收,实在是空间不够就来一次独占式的Full GC操作,这个时候挥挥手大量的内存,来供应用程序继续使用

    3)OOM:没有空闲内存,并且垃圾回收器也无法提供更多的空闲内存

    导致栈溢出的最常见的情况就是死循环和无限递归,方法自己调用自己,这样JAVA虚拟机栈就会只是入栈而不进行出栈,当到达JAVA虚拟机栈的最大数之后就会出现OOM异常

    堆溢出的常见场景:代码中创建了大量大对象,且长时间也没有被垃圾收集器收集

    1)内存泄漏:最常见的情况就是内存泄漏,就是对象在被创建以后不再被使用,但是还没有被释放,这样就会导致堆中的对象数量持续增加,比如说ThreadLocal使用不当,使用完成以后没有及时的调用remove方法导致内存泄漏,已经忘记释放各种连接,也会导致内存泄漏,比如说数据库连接,Socket连接以及IO连接等等

    2)无限递归创建大对象:无限的调用一个方法可能会导致栈溢出,但是如果递归方法中创建了大量的对象并且持续性地进行递归,也可能会导致堆溢出

    3)创建大量大对象:创建大量大对象,比如说数组或者是集合,可能会导致堆溢出,如果没有连续的内存来存储大对象,堆溢出就会发生;

    4)没有合理的设置堆大小:JAVA虚拟机的堆内存设置不够,堆的大小不合理,没有显式地指定堆的大小或者是指定数值偏小

    方法区溢出:

    1)因为永久代的大小是有限的,并且JVM对于永久代垃圾回收,比如说常量池回收,卸载不再需要的类型,非常不积极,所以当我们不断添加新类型的时候,永久代出现OOM已非常正常,类似于intern字符串缓存占用太多空间,也会导致OOM问题:

    java.lang.OutOfMemory:permGen space和但是随着元空间的引入,方法去内存已经不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息变成了:java.lang.OutOfMemory:Metaspace直接内存不足,也会报OOM

    实际上在抛出OOM异常之前,通常来说垃圾收集器会被触发,尽可能地来进行清理出所用空间,比如说在引用机制分析过程中,涉及到JVM会进行回收软引用所指向的对象

    Java堆溢出一般是JVM堆内存设置不合理或者是内存泄漏导致的:

    如果是内存泄漏,那么就可以通过工具查看泄露对象到GCROOTS的引用链,掌握了泄露对象的类型信息以及GC ROOTS的引用链信息,就可以精准地找到泄露代码出现的位置

    如果不是内存泄漏,那么就是内存中的对象都还是必须地存活者,那么就应该检查虚拟机的堆参数,查看你是否可以将虚拟机的内存调大些

    七)内存泄漏的原因:

     

    严格的来说只有对象不被使用到了,但是GC又不能将他们进行回收的情况,才叫做内存泄漏,但是很多情况下一些不太好的实践或者是疏忽会导致对象的生命周期变长甚至有可能导致OOM,也可以叫做宽泛意义上的内存泄漏,尽管内存泄露不会导致程序立即崩溃,但是一旦发生内存泄漏,程序中的可用内存可能就会步步蚕食,直至耗尽所有内存最终出现OOM

    1)宽泛意义上的内存泄漏:其实可以把对象定在方法内部作为局部变量,当方法执行完成以后,对象就要被回收了,但是如果将这个变量成员变量,那么这个对象的存活周期就变得很长,但是如果是这个变量静态变量还是大对象,类变量,随着类的加载而加载随着类的消亡而消亡,也会理解成宽泛的内存泄露,还比如说Session会话,不使用就没有必要存放,内存泄漏可能会导致内存溢出,如果出现很多生命周期很长的对象再加上很多没有办法回收的数据的存在就很有可能造成内存泄露;

    2)存在很多生命周期很长的对象,而本身生命周期没有这么长的对象而又生命很长可以称之为是内存泄漏;

    严格意义+宽泛意义上的内存泄漏,有些对象不使用,但是还存着引用链,有可能忘记断开引用,如右图,对象没有用处,但是还存在引用链,可能造成内存回收,ThreadLocal

    1)单例模式中的对象是static,单例对象的生命周期和应用程序是一样长的,一个进程只有这一个实例,就比如说RunTime实例,声明的是静态的,每一个进程只有一个实例会随着程序的执行而产生,随着进程的结束而销毁,如果此时单例对象关联了一个外部的很大的对象,这个外部对象用一会就不用了,单例对象的生命周期非常长,但是这个引用关系又断不掉,所以此时连带着外部对象的生命周期也很长,本身又不用,但是无法释放内存,此时这个对象还无法回收,引用链条得不到释放;

    2)当内部资源外部资源需要交互的时候,是需要进行手动的关闭资源链接,没有关闭资源,GC就无法回收这些对象,只有当程序结束的时候才能回收这些链接的对象,此时可能就会发生内存泄漏;

    八)内存泄露的常见场景:

    1)静态集合类:静态集合类,比如说HashMap,LinkedList,如果这些容器是静态的,那么他们的生命周期和JVM的保持一致,那么容器中的对象在程序结束之前不能被释放,从而造成内存泄露,简单来说,长的生命周期的对象持有着短的对象的生命周期的引用,尽管短的生命周期的引用的对象不再进行使用,但是却因为长的生命周期的对象持有者它们的引用而不能被回收:

    1. List list=new ArrayList<>();
    2. public void run(){
    3. Object obj=new Object();
    4. list.add(obj);
    5. }
    6. 2)单例模式:和上面的情况一样,和静态集合导致的内存泄漏的原因是相似的,因为单例的静态特性,它的生命周期和JVM的生命周期是一样长的,所以说如果单例对象如果持有着外部对象的引用,那么这个外部对象也不会回收,那么就容易造成内存泄露

      3)内部类持有外部类:内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,那么这个外部类的对象不会被垃圾回收,会造成内存泄漏

      4)各种链接,数据库连接,网络连接:

      5)变量不合理的作用域:

      6)改变哈希值:

      九)如何排查OOM解决OOM:有些对象生命周期太长其实没有必要

      1)要想解决OOM或者是堆空间溢出的问题,一般的时候是通过内存映像分析工具(Ecplice Memory Analyzer)来针对于dump出来的堆转存储内存快照进行分析,重点是进行确认内存中的对象是否是有必要的,也就是说要仔细分析看看到底是出现了内存溢出还是内存泄漏

      OOM通常是堆空间或者是元空间,更多的时候是堆空间,需要导出各个堆内存空间使用的快照,dump文件,通过jvisvm或者是jpofile工具可以解析dump文件看啊可能是否是出现了内存泄漏还是内存的溢出

      2)如果是内存泄漏,可以进一步通过工具来查看泄露对象到GCROOTS的引用链,于是就找到泄露对象是通过了什么样的路径和GCROOTS相关联并导致垃圾回收器无法回收它们,掌握了泄露对象的类型信息,以及GCRROTS引用链的关系就能精准的确定出泄露代码的位置

      3)如果不存在内存泄露,换句话来说就是内存中的对象确实还必须都存活着,那么此时就应该检查虚拟机的堆参数,和机器的物理内存相比看看是否还可以调大,从代码的角度上来查

      4)看是否存在某些对象生命周期过长,持有状态的时间过长,尝试减少运行期间的内存消耗

      看看有没有内存泄漏的问题,要及时地找到内存溢出的对象将他的指针给断掉,查看引用链

      如果不是内存泄漏,那么就适当提升方法区和堆空间大小的参数,再看看某些对象的生命周期是否合理,有些对象没有必要静态就不要静态,有些对象只是在方法内使用,就没有必要再方法外声明等等,调整对象的生命周期,及时的进行GC;

      十)程序中的并发和垃圾回收过程中的并行和并发:

      程序中的并行和并发(一个CPU快速切换CPU,不是真正意义上的同时执行):

      并行就是在具体某一个时刻的时候有三个线程同时的进行执行,主要是取决于多核CPU,而并发在某一个时间点上面只能有一个进程在执行,在时间段内是可能有多个线程在切换执行

      下面的绿线表示用户线程,红色表示垃圾回收器,串行垃圾回收器很慢

       
      十一)安全点和安全区域(了解)

      1)用户线程在进行执行的时候,并非在所有地方都是能够停下来进行GC的,也就是说用户线程不是可以在所有的位置停下来进行STW的,安全点的选择是很重要的,如果安全点太少可能会导致GC的停顿时间太长可能GC线程等待太久而导致OOM,如果太频繁可能会导致性能问题,因为如果savepoint太多,经常执行垃圾回收,可能造成线程频繁切换导致性能问题

      2)通常会选择是否让程序具有长时间运行的特征为标准,比如说选择一些执行时间比较长的指令作为Safe Point,比如说方法调用,循环跳转和异常跳转;

      3)安全点比较少,这个时候GC等待时间太长,用户线程执行时间过长,还有可能会导致OOM点太多,STW时间也会变长,这个时候切换线程开销很大

      4)最好是在跳转的时候或者调用新方法的时候,执行时间比较长,比如说在进行方法调用的时候,要将方法压入虚拟机栈,把它们作为savepoint,最好不要在程序指令执行很快的时候设置saveponint,sleep和blocking引用关系也不会发生变化,防止影响性能;

      安全区域:

      线程处于睡眠或者是阻塞状态,这个线程无法响应JVM中断请求,此时你在让去走到中断安全点挂起是不可能的,不可能唤醒吧;

      安全区的对象引用关系不会发生变化,因此安全区域发生GC,程序依然会继续执行, 要出安全区域了但GC没有结束,程序会等待,安全区域应该是并发的,但不能走出安全区域;

      1)当线程运行到Safe Regin的代码的时候,首先表示已经进入了Safe Regin,如果这段时间发生GC,JVM会忽略表示为Safe Regin状态的线程

      2)当线程即将离开Safe Regin的时候,会检查JVM是否已经完成GC,如果完成了,那么继续运行,否则线程必须要等待直到收到了Safe Regin的信号为止;

      十二)JAVA中的引用类型:

      引用:前提是引用关系在的情况下,都是可达的,没有引用关系啥引用都会被回收

      软引用:当内存足够的时候能保留在内存中,当内存空间不足的时候,那么直接可以抛弃这些对象,在JAVA.lang.ref中可以直接使用

      强引用可以直接访问目标对象,强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会强制回收强引用所指向的对象,强引用最有可能导致内存泄漏

      软引用:内存足够不会回收可触及的也就是软引用可达的对象,当堆内存不够的时候,才会回收,软引用是不会报内存溢出的,Mybatis内部缓存就是用到软引用

      此时如果说刚好能容得下大数组,大对象,还刚好容不下这个弱引用,不一定说回收软引用就会发生OOM,不一定报OOM之前才会回收,在JAVA1.2中提供了java.lang.SoftReference来实现软引用

      1. import java.lang.ref.SoftReference;
      2. class User{
      3. public String username;
      4. public String password;
      5. public User(String username,String password){
      6. this.username=username;
      7. this.password=password;
      8. }
      9. @Override
      10. public String toString() {
      11. return "User{" +
      12. "username='" + username + '\'' +
      13. ", password='" + password + '\'' +
      14. '}';
      15. }
      16. }
      17. public class Test {
      18. public static void main(String[] args) throws InterruptedException {
      19. //声明一个强引用,将强引用的对象放入到软引用的形参里面
      20. User user=new User("李四","12503487");
      21. SoftReference reference=new SoftReference<>(user);//创建软引用
      22. user=null;//但是此时s1所指向的对象既有强引用s1的关联,又存在着弱引用的reference的关联,所以此时应该把强引用的关联解除,也就是让s1里面不指向任何数据
      23. //上面的三行代码等价于 SoftReference reference=new SoftReference<>(new User("李四","12503487"))
      24. //1.从软引用中可以获取到软引用对象的实例,可以获取到强引用的实例,由于堆空间内存足够,所以不会回收软引用的可达对象
      25. System.out.println(reference.get());
      26. //2.创建一个大对象
      27. try {
      28. int[] array = new int[10000 * 100000];
      29. }catch (Throwable e){
      30. e.printStackTrace();
      31. }
      32. System.gc();
      33. System.out.println(reference.get());//在报OOM的时候,垃圾回收器会回收软引用的可达对象
      34. }
      35. }
        1. public static void main(String[] args) throws InterruptedException {
        2. //声明一个强引用,将强引用的对象放入到软引用的形参里面
        3. Object s1=new Object();
        4. SoftReference reference=new SoftReference<>(s1);//创建软引用
        5. s1=null;//但是此时s1所指向的对象既有强引用s1的关联,又存在着弱引用的reference的关联,所以此时应该把强引用的关联解除,也就是让s1里面不指向任何数据,保证这个对象只有一个软引用关联
        6. System.out.println(reference.get());//防止强引用来干扰
        7. }
        8. 弱引用:非必需的对象

          软引用不如弱引用回收的快,因为软引用要使用算法判断内存是否不足

          1)虚引用:一旦将弱引用回收,就会将虚引用存放到引用队列中,可以追踪垃圾回收过程,虚引用的作用主要是将回收的对象放在队列中 进行GC对象追踪

          2)虚引用也被称之为是幽灵引用或者是幻影引用,是所有引用类型中最弱的一个

          3)一个对象是否有虚引用的存在,完全不会决定对象的生命周期,如果一个对象持有虚引用,那么这个对象和没有引用指向是一模一样的,随时都有可能被垃圾回收器所回收,它不能被单独使用,也不能通过虚引用来直接获取到被引用的对象,当时图尝试通过虚引用的get()方法获取到对象的时候,总是null,为对象设置一个虚引用的目的就是为了在于追踪一个对象的垃圾回收过程,比如说能在这个对象被垃圾回收的时候收到一个系统通知

          4)虚引用必须和引用队列一起使用,虚引用在进行创建的时候必须提供一个引用队列作为参数,当垃圾回收器准备回收这个对象的时候,如果发现她还存在虚引用,那么就会再回收对象以后,将这个虚引用加入到引用队列,来通知应用程序对象的回收情况,由于虚引用可以追踪对象的回收时间,因此也可以将一些资源释放的操作在虚引用中进行执行和记录;

          5)在JDK1.2中提供了PhantReference类来实现虚引用

          1. Object obj=new Object();
          2. ReferenceQueue queue=new ReferenceQueue<>();
          3. PhantomReference reference=new PhantomReference<>(obj,queue);
          4. obj=null;
            1. import java.lang.ref.PhantomReference;
            2. import java.lang.ref.ReferenceQueue;
            3. public class Test{
            4. public static Test test;//对当前对象的声明
            5. public static ReferenceQueue queue=null;//声明引用队列
            6. public static class WorkThread extends Thread{
            7. @Override
            8. public void run() {
            9. while (true) {
            10. if (queue != null) {
            11. PhantomReference reference = null;
            12. try {
            13. //如果这个对象被回收了,那么虚引用会被放到等待队列里面
            14. reference = (PhantomReference) queue.remove();
            15. } catch (InterruptedException e) {
            16. e.printStackTrace();
            17. }
            18. if (reference != null) {
            19. System.out.println("追踪垃圾回收过程:当前Test实例被GC了");
            20. }
            21. }
            22. }
            23. }
            24. public static void main(String[] args) {
            25. Thread t=new WorkThread();
            26. t.setDaemon(true);//设置为守护线程:当程序中没有非守护线程时,守护线程也就执行结束
            27. t.start();
            28. //1.创建虚引用队列
            29. queue=new ReferenceQueue<>();
            30. Test test=new Test();
            31. //2.创建Test 对象的虚引用
            32. PhantomReference ref=new PhantomReference<>(test,queue);
            33. test=null;
            34. //3.尝试获取到虚引用中的对象获取失败,因为虚引用的对象不可以被获取到
            35. System.out.println(ref.get());
            36. System.gc();
            37. //4.执行GC之后,虚引用引用的对象会被回收,此时会把虚引用放入到引用队列里面
            38. }
            39. }
            40. }

            不同的引用类型主要是取决于不同对象的可达性状态和对象垃圾收集的影响,强引用就是普通对象的一个引用,只要有一个强引用指向一个对象就表示这个对象还活着,垃圾回收器就永远无法回收这一类的对象,只有没有其他引用关系或者是超过了引用的作用域或者是显示将引用设置为null,才会进行垃圾回收;

            软引用:只有当JVM认为内存不足的时候才会进行试图回收引用所指向的对象,软引用主要适用于实现内存敏感的缓存,如果还有空闲内存,就可以暂时去保留缓存,当内存不足的时候会清理掉,这样就可以保证使用缓存的同时,不会耗尽内存

            弱引用是相对于强引用关联的,不管内存是否足够都会回收弱引用

            虚引用不会决定对象的生命周期,它提供了一种确保对象被finlize之后,去做某些事情的一种机制,当垃圾回收器准备去回收一个对象的时候,如果发现她还存在虚引用,就会在回收对象的内存之前,就会把这个虚引用加入到与之关联的引用的队列里面,那么程序可以通过判断引用队列是否已经加入了虚引用来去了解被引用的对象是否要进行垃圾回收,然后就可以在引用对象被回收之前来采取必要的一个行动;

            终结器引用:finalReference

          5. 相关阅读:
            单模连接器损耗与影响因素
            第五章 C++与STL入门 例题
            前端学习笔记--ES6
            RedHat8升级GLIBC_2.29,解决ImportError: /lib64/libm.so.6: version `GLIBC_2.29
            Flet教程之 13 ListView最常用的滚动控件 基础入门(教程含源码)
            跨境电商小知识之跨境电商物流定义以及方式讲解
            docker搭建的jenkins,jmeter和ant环境变量环配置
            架构与思维:互联网高性能Web架构
            Yapi 1.10.3迁移踩坑记
            您电脑的网络管家 -NetSetMan
          6. 原文地址:https://blog.csdn.net/weixin_61518137/article/details/134087635