2.JVM性能调优

理解JVM中的几个内存模型

JVM的内存模型如下:

名词解释:
    Math math = new Math();
1.堆:存放具体实例化后的对象内容:new Math()
2.栈:存放对应的引用:Math math存放的是Math对象在堆内存中的地址!
3.本地方法栈:存放的是线程内部的一个底层调用,调用的是c++的方法
4.程序记数器:用于记录程序执行的位置(),试想多个线程并发执行,
    争抢CPU资源,当A执行被B线程抢占CPU资源,处于停滞状态时,再到A执行时,不可能重头执行,程序记数器记录着程序的执行位置, 
    由字节码执行引擎进行维护更改!
5.线程栈:每个线程启动时,都会创建自己的线程栈,维护自己的局部变量,注意的是:造成了线程间的不可见性,即AB线程公用一个变量,A修改了变量值
  B还用的是自己维护的变量原始值,所以涉及到volatile关键字保证可见性,详情参考JUC并发编程

6.栈帧:线程中的每个方法分配的内存空间,如main方法和compute方法等,每个方法都分配个栈帧
7.局部变量表:存放的是方法中用到的局部变量
8.操作数栈:数据暂存的转储地,后面会讲解!
9.动态链接:后面会提及,暂时不会
10.方法出口:即记录着返回到main方法的位置,因为不可能返回以后,main方法重新执行!

1.字节码代码的执行,加深理解内存模型

1.java代码
    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
    } 
2.JVM底层的反汇编码:每个命令都对应这个具体操作,可参考(字节码字典):https://blog.csdn.net/liuqianduxin/article/details/82810048
  public int compute();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn
  发现:
          int a =1;
      在底层对应两句:前面的0/1序号代表代码位置,程序记数器会记录这个位置
           0: iconst_1(对照底层的命令表:iconst_1 将int类型常量1压入操作数栈)
           1: istore_1(istore_1 将int类型值存入局部变量1)

2.堆内存与垃圾回收机制的讲解

可达性分析算法:
    将"GC ROOT"对象作为起点,从这些节点往下搜索引用的对象,找到对象标记为非垃圾,其余未标记的就是垃圾对象
    GC ROOT根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
    
1.开始扫描Eden(伊甸园),将存活的变量移到s1中,剩余的就是垃圾对象,gc会自动清理
2.第二次伊甸园中又有了新对象,伊甸园满了又触发垃圾回收,minor gc会扫描整个的年轻代区域,会将伊甸园和s0区复制的对象再进行扫描,将存活对象转储到s1,剩余的就是垃圾对象,gc自动清理
3.下次伊甸园又满了,会将对象转储到s0,这两个内存倒腾
4.如果15次扫描后,对象依旧存活,会转储到老年代中

在jdk自带的工具中可以看到各内存间的转移关系:
在cmd命令串口中执行:jvisualvm命令出现下面窗口!

 
 

3.常用的调优工具Arthas

参考地址:https://arthas.aliyun.com/doc/
理解为什么要jvm调优,调优到底调的是什么?
    1.调优目的是为了减少full gc线程的执行
    2.重要(STW机制):因为垃圾回收线程执行时,会先暂停其他的用户线程,以垃圾回收优先,会造成暂时卡顿(如果是淘宝等),造成体验不佳,所以要调优

4.如何修改JVM的参数

如图所示:
如何评估jvm的参数如何设置?
    1.评估每秒的最大并发量:例如以订单系统为例
        1.1.如果每秒300个并发,每个订单生成1个Order对象,每个Order对象大小大概1kb(根据对象中的各个字段进行评估,有专门的软件可做评估):此时每秒产生的对象大小-->300*1kb
        1.2.肯定还会有其他的对象如库存,积分,优惠券等等,我们扩大20倍:300*20*1kb
        1.3.除了订单操作还有其他操作如删除订单,查询订单等等,扩大10倍:300*20*10*1kb大概为60M
    得出结论:
        大概每秒钟需要产生60M的对象存储在堆中,一秒钟后操作结束,这些就会成为垃圾对象!

分析:
    -Xms:设置java初始堆的大小
    -Xmx:设置堆的最大大小(尽量和-Xms保持一致)
    --XX:MaxMetaspaceSize:设置方法区(元数据区的大小)
    -XX:MetaspaceSize:设置方法区(元数据区的大小)和MaxMetaspaceSize保持一致

场景:程序启动后,Full gc会频繁启动
分析过程:
    1. -Xms3G -Xmx3G 堆内存分配了3g的内存,底层老年代占用2G,年轻代(伊甸园,S0,S1)占用1G,而年轻代的分配比率是8:1:1,所以伊甸园占800M,s0/s1各占100M
    2.分析过程:每秒大概产生60M的对象存储到伊甸园区,1秒后变成垃圾对象,大概14秒占满伊甸园,触发minor gc回收垃圾
        因为gc线程执行有个STW机制,即其他客户线程全部停止,等待gc线程执行完毕后,再执行
    3.如果当时STW时,客户线程还没有执行完毕,即60M的对象还有引用,不是垃圾对象,所以这些对象会被转移到survior区(s0/s1区)
    4.JVM底层对象内存分配机制中有这么一种机制:
        对象动态年龄判断:即当前对象的Survior区域里,一批对象的总大小超过了这块区域大小的50%(可以通过-XX:TargetSurvivorRatio可以指定),就会把超出的部分直接放入老年代,minor gc就回收不上了
        这个规则的目的是:希望那些可能长期存活的对象,尽早进入老年代
        对象动态年龄判断机制一般在minor gc后触发!
    5.这就会导致老年代中未被回收的垃圾对象原来越多,最终满载,触发Full gc(相当耗费资源)

原因:
    因为survior区大小太小,触发对象年龄判断机制,直接将垃圾对象存入老年代,无法回收导致的,
解决办法:
    增大survior区的大小,可以应对50%的占用,就不会触发对象年龄判断机制,到时就会被minor gc回收掉
      -Xmn2G 设置新生代的大小为2g:按照8:1:1的算法,survior可以每个区分的200M大小,60M对象来了,不超50%不会触发该机制,后会被minor gc回收掉,不会频繁的引起Full gc   

上述办法解决了部分的问题,但是有个场景是一秒钟上百外的请求量,那又该如何解决呢
1.增大内存,给年轻代分50个G,则伊甸园分得40g,s0/s1各分得5g,这时又面临一个问题:即伊甸园满了,minor gc回收时,伊甸园太大,回收时间过长,STW机制会让客户端线程等待,这种情况又怎么办呢
2.10种垃圾回收器里有一种G1,可以设置参数
    1.-XX:+UseG1GC:使用g1收集器
    2.-XX:MaxGCPauseMillis:目标的暂停时间(默认200ms),g1会去伊甸园扫描,一部分一部分扫描,如果发现该部分垃圾回收时间到达200ms时,就回收这部分
     后果:这样会造成minor gc频繁清扫,但也强过一次性扫描40G,STW导致客户线程停太长时间

常见的垃圾回收算法:

1.引用计数(不太使用)
    
2.复制:
    优点:没有像标记清除一样,产生内存碎片
    缺点:有些浪费空间,并且如果有些大内存的对象复制起来耗时

3.标记清除
        优点:标记不用大面积的去复制,节省空间
        缺点:会产生大量的内存碎片
        
4.标记整理:
    优点:没有内存碎片了
    缺点:得移动对象

1.引用计数

2.复制

3.标记清除

4.标记整理

面试

问题1:如何确定是垃圾,什么是GC root?

问题1:如何确定是垃圾,什么是GC root?
    什么是垃圾:简单说就是内存中已经不再被使用到的空间就是垃圾
    如何确定是否可以被回收呢?
        枚举根节点可达性分析(根据搜索路径)

哪些可以作为GC Roots的对象:
    1.虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象
    2.方法区中的类静态属性引用的对象
    3.方法区常量引用的对象
   4.本地方法栈中JNI(Native方法)引用的对象
代码说明:
    /**
     * 4本地方法栈中JNI(即一般锁的Native方法)中引用的对象
     */
    public class GCRootDemo {
        private byte[] bytesArray=new byte[100*1021*1024];
        //1:方法区中类的静态属性所引用的对象
        private static DeadLockDemo deadLockDemo = new DeadLockDemo();
        //2:方法区中常量引用的对象
        private static final DeadLockDemo deadLockDemo1 = new DeadLockDemo();
        public static void m1(){
            //3:虚拟机栈(栈帧中的本地变量表)中引用的对象,每个线程都有自己对应的线程栈,每个方法都会有自己的栈帧,
            GCRootDemo gcRootDemo=new GCRootDemo();
            System.out.println("第一次gc完成!");
        }
    }

问题2:如何盘点查看JVM系统默认值
    JVM的参数类型分为三种:
        1.标配参数(基本不动的参数)
            -version
            -help
            -java -showversion
            
        2.X参数(了解)
            -Xint  解释执行
            -Xcomp 第一次使用就编译成本地代码
           -Xmixed 混合模式 
           
        3.xx参数(重点)
            分为两类
            3.1 Boolean类型
                公式:-XX:+或者-某个属性值(+表示开启  -表示关闭)
                    查看:
                    1.jps查看进程号
                    jps
                        26272 DeadLockDemo
                        40468 RemoteMavenServer36
                        41220 Jps
                        47508
                        65156 KotlinCompileDaemon
                        9048 Launcher
                    2.jinfo -flag 设置  进程号 查看boolean设置值
                    jinfo -flag PrintGCDetails 26272
                        -XX:-PrintGCDetails(-号代表没开启)

            3.2 kv设值类型
                公式:-XX:属性key=属性值value
                
设置中有两个特殊的参数
    -Xms:等价于-XX:InitialHeapSize(设置堆的初始大小)
    -Xmx:等价于-XX:MaxHeapSize(设置堆的最大大小)

1.查看JVM所有参数默认设置(jdk出厂自带设置)命令:java -XX:+PrintFlagsInitial
    java -XX:+PrintFlagsInitial
        [Global flags]
            uintx AdaptiveSizeDecrementScaleFactor          = 4                                   {product}
            uintx AdaptiveSizeMajorGCDecayTimeScale         = 10                                  {product}
            uintx AdaptiveSizePausePolicy                   = 0                                   {product}
            uintx AdaptiveSizePolicyCollectionCostMargin    = 50                                  {product}
            uintx AdaptiveSizePolicyInitializingSteps       = 20                                  {product}
            uintx AdaptiveSizePolicyOutputInterval          = 0                                   {product}
            uintx AdaptiveSizePolicyWeight                  = 10                                  {product}
            uintx AdaptiveSizeThroughPutPolicy              = 0                                   {product}
            uintx AdaptiveTimeWeight                        = 25                                  {product}
             bool AdjustConcurrency                         = false                               {product}
             bool AggressiveOpts                            = false                               {product}
             intx AliasLevel                                = 3                                   {C2 product}
            ......


2.查看更新的值
    java -XX:+PrintFlagsFinal -version
        ...
        uintx InitialBootClassLoaderMetaspaceSize       = 4194304                                   {product}
        uintx InitialCodeCacheSize                      = 2555904 (等号)                             {pd product}
        uintx InitialHeapSize                          := 197132288(:等号)                           {product}
        ...
    =和:=有什么区别呢??
        =代表没有更改的值
        :=代表JVM或者我们自定义设置的值
    
如何在运行时打印出参数:
    java -XX:+PrintFlagsFinal -Xss128k 运行的jar包名称    


3.查看常用内容的值(主要可以查看垃圾回收器)
    java -XX:+PrintCommandLineFlags -version
        -XX:InitialHeapSize=196810432 -XX:MaxHeapSize=3148966912 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers 
        -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation 
(查看垃圾回收器) -XX:+UseParallelGC
        java version "1.8.0_121"
        Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
        Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

问题3:你平时工作用过的JVM常用基本配置参数有哪些

问题3:你平时工作用过的JVM常用基本配置参数有哪些
    代码如何获取内存大小呢?
        1.//返回java虚拟机的内存总量(默认是电脑内存的1/64)
            long totolMemory=Runtime.getRuntime().totalMemory()/(1024*1024);
            System.out.println("内存总量:"+totolMemory+"MB");
        2.//返回java虚拟机视图使用的最大内存量(默认是电脑内存的1/4)
            long maxMemory=Runtime.getRuntime().maxMemory()/(1024*1024);
            System.out.println("内存最大量:"+maxMemory+"MB");
常用参数:
    1. -Xms:初始大小内存,默认是物理内存的1/64,等价于-XX:InitialHeapSize
    2. -Xmx:最大分配内存,默认为物理内存大的1/4,等价于-XX:MaxHeapSize
    3. -Xss:设置单个线程栈的大小,一般默认是512k-1024k,等价于-XX:ThreadStackSize
            1.查进程号:
                >jps
                    43392 DeadLockDemo
                    40468 RemoteMavenServer36
                    47508
                    65156 KotlinCompileDaemon
                    26072 Launcher
                    31132 Jps
            2.查栈内存的初始值:发现是0??这是因为0代表使用的是栈的默认值(默认值是1024k,并且和jdk版本以及jvm部署环境有关),如果在JVM中更改了栈的大小,这里会显示更改后的值
                >jinfo -flag ThreadStackSize 43392
                    -XX:ThreadStackSize=0

    4. -Xmn:设置年轻代大小
    
    5. -XX:MetaspaceSize:设置元空间大小
            元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
            不过元空间域永久代最大的区别在于:
                元空间并不在虚拟机中,而是使用本地内存
                因此,在默认情况下,元空间的大小仅受本地内存限制
        示例:
            -Xms10m -Xmx10m -XX:MetaspaceSzie1024m -XX:+PrintFlagsFinal -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC(串行垃圾回收器)
            
    6.-XX:+PrintGCDetails:打印gc垃圾回收的细节
        例子:设置堆内存的大小为10m,代码中设置50m的占用,会触发OOM异常(java heap space),看gc的回收细节
            测试类
                public class GCRootDemo {
                public static void main(String[] args) {
                   Byte[] bytes=new Byte[50*1024*1024];
                }
            }
            配置jvm启动参数:-Xms10m -Xmx10m -XX:+PrintGCDetails
        输出:
            这些如何解读呢??看下面截图
            1.monior gc清理
            [GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->783K(9728K), 0.0245799 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
            [GC (Allocation Failure) [PSYoungGen: 765K->504K(2560K)] 1044K->859K(9728K), 0.0044273 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
            [GC (Allocation Failure) [PSYoungGen: 504K->504K(2560K)] 859K->883K(9728K), 0.0007878 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
            2.full gc清理(会清理老年代的内容)
            [Full GC (Allocation Failure) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 379K->738K(7168K)] 883K->738K(9728K), [Metaspace: 3494K->3494K(1056768K)], 0.0075299 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
            [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 738K->738K(9728K), 0.0003469 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
            [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 738K->719K(7168K)] 738K->719K(9728K), [Metaspace: 3494K->3494K(1056768K)], 0.0085254 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
            内存占用
            Heap
            1.年轻代
             PSYoungGen      total 2560K, used 100K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
              eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd19328,0x00000000fff00000)
              from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
              to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
            2.老年代
             ParOldGen       total 7168K, used 719K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
              object space 7168K, 10% used [0x00000000ff600000,0x00000000ff6b3f00,0x00000000ffd00000)
            3.元数据
             Metaspace       used 3527K, capacity 4502K, committed 4864K, reserved 1056768K
              class space    used 391K, capacity 394K, committed 512K, reserved 1048576K
          抛出的内存溢出
            Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
            	at deadLock.GCRootDemo.main(GCRootDemo.java:8)

    7.-XX:SurvivorRatio:设置新生代中eden和s0/s1空间比例
        默认比例伊甸园区:s0:s1=8:1:1
        如果改为:-XX:SurvivorRatio=4,Eden:s0:s1=4:1:1
        主要SurvivorRatio配置的是伊甸园占的比例
        
    8.-XX:NewRatio:配置年轻代和老年代在堆结构中的占比
        默认是:-XX:NewRatio=2新生代栈1,老年代占2,年轻代占整个队的1/3
        加入:
            -XX:NewRatio=4,新生代占1,老年代占4,年轻代占整个堆的1/5
        NewRatio值就是设置老年代的占比,剩下的1给新生代
        
    9.-XX:MaxTenuringThreshold:设置垃圾最大年龄(即年轻代到养老区需要经过gc扫描的次数)
    默认是15(只能设置在0-15之间),java8设置15以上会报错!

问题:强应用/软应用/弱引用/虚引用分别是什么

整体架构如图:

1.强引用:
    当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算出现了oom也不会对该对象进行回收,死都不收。
    
    强引用是我最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾收集器不会碰这种对象。
    在java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,他就处于可达状态,他是不可能被垃圾回收机制回收的。
    即使该对象以后永远都不会被用到,也不会被jvm回收。因此,强引用是造成java内存泄露的主要原因之一。
    
    对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域,或者显示的将相应(强)引用赋值为null;
    一般认为就是可以被垃圾回收机制回收的(当然具体回收时机还是要看垃圾收集的策略)
    代码:
        public class GCRootDemo {
            public static void main(String[] args) {
                //这样定义的默认就是强引用
                Object obj1 = new Object();
                //obj2引用赋值
                Object obj2 = obj1;
                //置空
                obj1 = null;
                //垃圾回收
                System.gc();
                System.out.println(obj2);
            }
        }
    输出:发现obj2并没有被回收
        java.lang.Object@4dc63996
        
        
2.软引用:内存足够,不收;内存不够时,回收
    软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集
    对于只有软引用的对象来说:
        1.当系统内存充足时,不会被回收
        2.当系统内存不足时,会被回收
    软引用通常用在对内存敏感的程序中,比如告诉缓存就有用到软引用,内存足够时就保留,不够用就回收
    具体演示代码如下:
        public class GCRootDemo {
            public static void main(String[] args) {
                //这样定义的默认就是强引用
                Object obj1 = new Object();
                SoftReference<Object> softReference = new SoftReference<>(obj1);
                //手动gc,此时空间没有占满,下列都可以正常输出
                System.gc();
                System.out.println(obj1);
                System.out.println(softReference.get());
        
                //分割线---设置内存不够时
                obj1 = null;
                try {
                    //设置一个大的对象占满内存,此时内存不够,gc需要来回收,会回收掉软引用,下列都输出为null
                    byte[] bytes = new byte[30 * 1024 * 1024];
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //都输出null:obj1强引用置为null,softReference.get()软引用已被回收
                    System.out.println(obj1);
                    System.out.println(softReference.get());
                }
            }
        }
        输出:
            java.lang.Object@4dc63996
            java.lang.Object@4dc63996
            null
            null
3.弱引用:不管内存是否足够,垃圾回收都会回收
    需要通过java.lang.ref.WeakReference类来实现,比软引用的生存周期更短
    对于弱引用的对象来说,只要垃圾回收机制一运行,不管JVM内存空间是否足够,都会回收该对象占用的空间
    演示代码如下:
        Object obj1 = new Object();
        WeakReference<Object> softReference = new WeakReference<>(obj1);
        //这里必须改为null,即强引用置为null
        obj1 = null;
        System.gc();
        System.out.println(obj1);
        System.out.println(softReference.get());
        输出:null null
        
    使用场景:
        1.假如有一个应用需要读取大量的本地图片
            * 如果每次都去图片都从硬盘中读取则会严重的影响性能
            * 如果一次性全部加载到内存中有可能会造成内存溢出
        此时可以使用软引用(内存足够时不回收,不够时回收)或者弱引用(垃圾回收时都回收,不管内存是否足够)解决这个问题
            设计思路:
                用一个HashMap来保存这些图片的路径和响应的图片对象关联的软引用之间映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效的避免了OOM的问题
                Map<String,SoftReference<Bitmap>> imageCache=new HashMap<String,SoftReference<Bitmap>>();
     
    知道弱引用,谈一下WeakHashMap?
        当WeakHashMap中key置为null时,垃圾回收时会回收掉这对kv
        代码示例如下:
            public class GCRootDemo {
                public static void main(String[] args) {
                    //普通map------------------------------------------
                    Map<Integer, String> map = new HashMap<>();
                    //重点1:key必须是对象才能体现出来
                    Integer key = new Integer(22);
                    String value = "吴孟达";
                    map.put(key, value);
                    System.out.println(map);//输出:{22=吴孟达}
                    key = null;
                    System.gc();
                    System.out.println(map);//输出:{22=吴孟达}
                    发现普通map并没有回收,因为map底层是Node节点,即将kv的值赋给Node节点中的属性中,跟原始key、value已然是没有任何关系了
                    
                    
                    //WeakHashMap----------------------
                    Map<Integer, String> weakHashMap = new WeakHashMap<>();
                    //Integer weak_key = new Integer(22);
                    Integer weak_key = new Integer(22);
                    String weak_value = "刘丹";
                    weakHashMap.put(weak_key, weak_value);
                    System.out.println(weakHashMap);//输出:{22=刘丹}
                    weak_key = null;
                    System.out.println(weakHashMap);//输出:{22=刘丹},此时虽然已经将key改为了null,但是还没有进行垃圾回收,内容还在
                    System.gc();//垃圾回收
                    System.out.println(weakHashMap);//此时输出:{}}
                }
            }

4.虚引用:
     虚引用需要java.lang.ref.PhantomReference类来实现
     顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
     如果一个对象仅只有虚引用,那么他就跟没有任何引用一样,在任何时候都可能被垃圾回收器回收,他不能单独使用,也不能通过它访问对象,虚引用必须与引用队列(ReferenceQueue)联合使用。
     
    虚引用 主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一确保对象被finalize以后,做某些事情的机制。
    PhantomReference的get方法返回总是null,因此无法访问对应的引用对象,其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作
    
    换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的请求
    java技术允许使用finalize()方法在垃圾收集器将独享从内存中清除出去之前做必要的清扫工作
    代码示例:
        public class GCRootDemo {
            public static void main(String[] args) {
                Object o1=new Object();
                ReferenceQueue<Object> referenceQue=new ReferenceQueue<>();
                PhantomReference<Object> phantomReference=new PhantomReference<>(o1, referenceQue);
                System.out.println(o1);//正常输出:java.lang.Object@4dc63996
                System.out.println(phantomReference.get());//输出null,因为虚引用任何时候get都是null
                System.out.println(referenceQue.poll());
                //将引用置为null:发现置为null,没有进行垃圾回收时都是null
                o1=null;
                System.out.println(o1);//输出null
                System.out.println(phantomReference.get());//输出null
                System.out.println(referenceQue.poll());//输出null
                //垃圾回收:垃圾回收后会将回收的虚引用存到引用队列:ReferenceQueue中
                System.gc();
                System.out.println(o1);//输出null
                System.out.println(phantomReference.get());//输出null
                System.out.println(referenceQue.poll());//输出:java.lang.ref.PhantomReference@d716361
            }
        }
总结如图:

OOM

1.java.lang.StackOverflowError:栈溢出异常,一般发生在递归调用里
    样例代码如下:
        方法自己调自己,形成递归死锁,容易将线程栈撑爆!报java.lang.StackOverflowError
        public static void main(String[] args) {
            diGui();
        }
        private static void diGui() {
            diGui();
        }
        
2.java.lang.OutOfMemoryError:java heap space:堆内存溢出
    样例代码:如果设置堆内存带下为50m,-Xms10m -Xmx10m,再创建一个80m的大对象,直接就会报错java.lang.OutOfMemoryError:java heap space
        Byte[] bytes = new Byte[80 * 1024 * 1024];
        
3.java.lang.OutOfMemoryError:gc overhead limit excped
    理论:
        gc回收时间过长会抛出OutOfMemoryError,过长的定义98%的时间用来GC并且回收了不到2%的堆内存,连续多次的gc都只能回收不到2%的极端情况下才会被抛出,
        假设不抛出GC overhead limit错误会发生什么情况呢?
        那就是gc回收的那么点内存很快会被填满,迫使gc再次执行,这样就形成了恶性循环
        cpu的利用率一致是100%,而gc却没有任何成果
    样例代码:写一死循环,不停的往list中放值,并且都是强引用,gc无法回收,空间满了会导致gc频繁运行,98%都运行gc了,但是只回收了不到2%的空间
        @Data
        @AllArgsConstructor
        class Person {
            private int age;
            private String name;
        }
        
        public class GcOverHeadDemo {
            public static void main(String[] args) {
                List<Person> personList = new ArrayList<>();
                int i = 0;
                try {
                    
                    while (true) {
                        Person person = new Person(i++, ("吴孟达"+i).intern());
                        personList.add(person);
                        System.out.println("添加到了:"+i);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("最终i:"+i);
                }
            }
        }
    打印出:
        发现fullgc也只能回收几kb的空间,还不如不回收,就会抛出异常
        [Full GC (Ergonomics) [PSYoungGen: 989K->989K(1024K)] [ParOldGen: 506K->504K(512K)] 1495K->1493K(1536K), [Metaspace: 3678K->3678K(1056768K)], 0.0156882 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 
        [Full GC (Ergonomics) [PSYoungGen: 989K->989K(1024K)] [ParOldGen: 504K->504K(512K)] 1493K->1493K(1536K), [Metaspace: 3678K->3678K(1056768K)], 0.0144500 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
        最终i:9258
        [Full GC (Ergonomics) [PSYoungGen: 989K->454K(1024K)] [ParOldGen: 507K->279K(512K)] 1496K->734K(1536K), [Metaspace: 3681K->3681K(1056768K)], 0.0071657 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
        Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
        	at java.lang.StringBuilder.toString(StringBuilder.java:407)
        	at OOM.GcOverHeadDemo.main(GcOverHeadDemo.java:24)
   
               
4.java.lang.OutOfMemoryError:Direct buffer memory:直接内存挂了
    理论:
        元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制
    导致原因:
        写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式
        它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作
        这样能在一些场景中显著提高性能,因此避免了在java堆和native堆中来回复制数据
        1.ByteBuffer.allocate(capability)是分配jvm堆内存,属于gc管辖范围,由于需要拷贝,所以速度相对比较慢
        2.ByteBuffer.allocateDirect(capability)是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝,所以速度相对较快
        但如果不断的分配本地内存,堆内存很少使用,jvm就不需要执行gc,DirectByteBuffer对象就不会被回收
        这时堆内存充足,但本地内存可能已经用完,再次尝试分配本地内存就会出现OutOfMemoryErrpr,那么程序就直接崩了
    
5.java.lang.OutOfMemoryError:unable to create new native thread(不能创建新的线程)
        高并发程序时可能会发生下面情况
        导致原因:
            1.应用创建了太多的线程,超过了系统承载极限
            2.服务器不允许创建那么多的线程,linux系统默认允许创建的线程数是1024个(不同平台的默认设置不同)
            如果创建的线程数超过这个数量就会报:java.lang.OutOfMemoryError:unable to create new native thread
        解决办法:
            1.想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建那么多的线程,如果不是,该代码降低线程数
            2.当有用线程超过linux系统默认的1024个线程的限制,可以通过更改linux服务器的配置,扩大linux的默认配置

6.java.lang.OutOfMemoryError:Metaspace
    理论:
        java8以后的版本使用Metaspace来代替永久代
        与永久代的区别在于:
            Metaspace并不在虚拟机内存中而是使用本地内存,即在java8中,class metadata被存储在叫做metaspace的本地内存中
        元空间存放了下列信息:
            1.虚拟机加载的类信息
            2.常量池
            3.静态变量
            4.即时编译后的代码
    原因:
        错误的主要原因, 是加载到内存中的 class 数量太多或者体积太大
    样例代码:不停的通过spring反射创建静态内部类,会被加载到元空间中
        public class MetaspaceDemo {
            static class OOM{}
            public static void main(String[] args) {
                int i = 0;//模拟计数多少次以后发生异常
                try {
                    while (true){
                        i++;
                        Enhancer enhancer = new Enhancer();
                        enhancer.setSuperclass(OOM.class);
                        enhancer.setUseCache(false);
                        enhancer.setCallback(new MethodInterceptor() {
                            @Override
                            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                                return methodProxy.invokeSuper(o,args);
                            }
                        });
                        enhancer.create();
                    }
                } catch (Throwable e) {
                    System.out.println("=================多少次后发生异常:"+i);
                    e.printStackTrace();
                }
            }
运行结果:

posted @ 2022-05-19 19:25  努力的达子  阅读(330)  评论(0编辑  收藏  举报