Java总结之JVM

1、四种垃圾回收算法

如何确定垃圾

  1. 引用计数:引用与对象相关联,如果要操作对象,则必须使用引用。因此,可以通过引用计数来确定对象是否可以回收。实现原则是,如果一个对象被引用一次,计数器 +1,反之亦然。当计数器为 0 时,该对象不被引用,则该对象被视为垃圾,并且可以被 GC 回收利用,但是引用计数算法无法解决循环引用问题
  2. 可达性分析:为了解决引用计数法的循环引用问题,Java 采用了可达性分析的方法。其实现原理是,将一系列"GCroot"对象作为搜索起点。如果在"GCroot"和一个对象之间没有可达的路径,则该对象被认为是不可访问的
  • 引用计数(以上已有说明)
  • 复制:如下图复制算法会划分两片区域,一片区域是已使用,一片区域是备用复制,当触发GC时,会先进行可达性分析,为需要GC的区域进行标记,再进行GC时会把已使用的区域复制到备用区域,此时两片区域功能性对换,这就是复制算法的本质
    • 优点:实现简单内存效率高,不易产生碎片
    • 缺点:内存被压缩一半,当活跃对象过多,复制效率会大大降低

 

  • 标记清除:此算法分为两个阶段,首先标记出需要回收的对象,再标记完成后对已标记的对象统一清理回收
    • 缺点:在清楚之后会产生大量不连续的内存碎片,这样很容易导致在创建新的大对象时,无法分配足够的连续空间导致再次触发GC操作

  • 标记整理:此算法会先对内存中的对象进行分析,进行标记,此步骤和标记清除算法一致,在整理阶段会将把已使用的区域统一复制到内存的一段,再将剩余标记的对象进行清理
    • 优点:不会产生内存碎片
    • 缺点:耗时相对较长

分代收集算法是以上算法根据不同情况的回收区域进行最优选择,就不做赘述,后续在总结7种垃圾回收器会有分析算法使用情况

2、GCroot和四大引用类型

  • GCroot
    • 什么是GCroot:GCroot本质上是一个初始引用的集合,对象是否需要被回收,需要判断是否有一条路径从GCroot中出发,并指向目标对象,如果存在则表示该对象可达,不需要进行回收,反之则需要进行清理
    • 如何判断是否属于GCroot集合(4种)
      • 虚拟机栈,在栈帧中的本地变量表引用的对象
      • native方法引用的对象
      • 方法区中的静态变量
      • 方法区常量引用的对象
  • 四大引用
    软引用、弱引用、虚引用都有构造函数可以传入一个引用队列用于记录被GC回收的引用对象
    • 强引用(一般我们在方法里创建的对象即为强引用,这边就不做介绍)
    • 软引用:在GC时若内存充足时不会被GC回收,只有在内存不足时才会被GC回收
          /**
           * 内存充足时的软引用
           */
          private static void softReferenceEnoughDemo(){
              Object o1 = new Object();
              SoftReference<Object> softReference = new SoftReference<>(o1);
              System.out.println(o1);
              System.out.println(softReference.get());
              // 将o1引用置为null
              o1 = null;
              // 手动触发gc收集
              System.gc();
              System.out.println("==============================");
              System.out.println(o1);
              System.out.println(softReference.get());
          }
      
          /**
           * 内存不足时的软引用
           */
          private static void softReferenceNotEnoughDemo(){
              Object o1 = new Object();
              SoftReference<Object> softReference = new SoftReference<>(o1);
              System.out.println(o1);
              System.out.println(softReference.get());
              // 将o1引用置为null
              o1 = null;
              System.out.println("==============================");
              try {
                  byte[] bytes = new byte[20 * 1024 * 1024];
              } catch (Exception e) {
                  e.printStackTrace();
              } finally {
                  System.out.println(o1);
                  System.out.println(softReference.get());
              }
          }

      jvm参数配置下图,上图是执行控制台打印出的程序执行信息以及GC回收的详细信息

      -Xms10m -Xmx20m -XX:+PrintGCDetails //使用jdk8默认的垃圾回收器
    • 弱引用:弱引用只要触发GC就会被回收,与内存是否充足无关
          private static void weakReferenceDemo(){
              Object o1 = new Object();
              WeakReference<Object> weakReference = new WeakReference<>(o1);
              System.out.println(o1);
              System.out.println(weakReference.get());
              // 将o1引用置为null
              o1 = null;
              // 手动触发gc收集
              System.gc();
              System.out.println("==============================");
              System.out.println(o1);
              System.out.println(weakReference.get());
          }

    • 虚引用:虚引用的使用一般需要结合ReferenceQueue使用,查看源码可以看到,虚引用的构造函数入参需要传入引用队列,虚引用在创建后在任何时候都可能回收,回收时会被放入引用队列,可以用来做类似aop中的后置通知的功能,用于监听销毁,即在一个finalization阶段可以被gc回收,用来实现再次之前做一些必要的操作(虚引用的get操作返回的永远是null,所以无法获取到引用对象)
      /**
           * 虚引用
           */
          private static void phantomReferenceDemo(){
              Object o1 = new Object();
              ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
              PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);
              System.out.println(o1);
              System.out.println(phantomReference.get());
              System.out.println(referenceQueue.poll());
              o1 = null;
              System.gc();
      
              System.out.println(o1);
              System.out.println(phantomReference.get());
              System.out.println(referenceQueue.poll());
      
          }

3、JVM参数配置详解和常见的几种OOM类型

  • 参数配置
    • X类型(简称分类)
    • XX类型(简称分类)
  • 常见OOM类型(前两种相对常见,此次不贴代码)
    • java.lang.StackOverflowError:栈内存溢出,常出现在错误的递归调用种
    • java.lang.OutOfMemoryError:Java heap space:堆内存异常,对象创建超出了堆的最大值
    • java.lang.OutOfMemoryError:GC overhead limit exceeded
      /**
           * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
           * 指定堆外最大内存,不设置默认是64m如果超出设置值,这会多次触发FullGC
           */
          public static void main(String[] args) {
              Integer i = 0;
              List<String> list = new ArrayList<>();
              try {
                  while(true){
                      list.add(String.valueOf(++i).intern());
                  }
              } catch (Throwable e) {
                  System.out.println("*************i:" + i);
                  e.printStackTrace();
                  throw e;
              }
          }

      GC回收时间过长则会抛出错误,GC时间过长的实际是指,超过98%的时间做GC并且回收了不到2%的堆内存,连续多次触发FullGC都只回收不到2%的极端情况下才会抛出;以上设置的参数(-XX:MaxDirectMemorySize=5m)限制对外内存5m,当对外内存达到上限则触发FullGC操作,而每次GC的效果不明显,再次触发FullGC,恶心循环,频繁触发GC操作,此时CPU使用率一直偏高,故抛出错误

    • java.lang.OutOfMemoryError:Direct buffer memory:本地内存错误
          public static void main(String[] args) {
              //-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
              System.out.println(VM.maxDirectMemory() / 1024 / 1024 + "MB");
              try {
                  Thread.sleep(3000);
              }catch (Exception e){
                  e.printStackTrace();
              }
              ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6*1024*1024);
          }

      在NIO程序经常使用ByteBuffer来读取或者写入数据其中使用ByteBuffer.allocateDirect()方法可以直接分配本地内存,而本地内存不属于GC管辖范围,如果不断使用本地内存,而少量使用堆内存,由于堆内存充足,JVM不会触发GC操作那DirectByteBuffer对象们就不会回收,而此时再次分配本地内存就出现了OOM错误

    • java.lang.OutOfMemoryError:unable to create new native thread:线程数量超出最大值,无法再创建新的线程(在linux环境中默认每个用户(root用户外)最大开启的线程数为1024,如果超出)
    • java.lang.OutOfMemoryError:Metaspace
          /**
           * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
           */
          static class  test{}
          public static void main(final String[] args) {
              int i = 0;
              try {
                  while(true){
                      i++;
                      Enhancer enhancer =new Enhancer();
                      enhancer.setSuperclass(test.class);
                      enhancer.setUseCache(false);
                      enhancer.setCallback(new MethodInterceptor() {
                          public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                              return methodProxy.invoke(o, args);
                          }
                      });
                      enhancer.create();
                  }
              }catch (Throwable e){
                  System.out.println("多少次之后发生了异常: " + i);
                  e.printStackTrace();
              }
          }

       永久代(java8之后元空间取代了永久代)存放一下信息:1、虚拟机加载的类信息 2、常量池 3、静态变量 4、即时编译后的代码,如果元空间不断创建改大小以上信息则会出现OOM错误

4、6大垃圾回收器(JDK8,G1收集器单独总结)

  • 参数预先说明
    • DefNew-----Default New Generation
    • Tenured-----Old
    • ParNew-----Parallel New Generation
    • PsYongGen-----Parallel Scavenge
    • ParOldGen-----Parallel Old Generation
  • Server/Client模式:在32位windows操作系统固定是Client模式,在32位骑它操作系统2g内存且同时有两个cpu上则位Server模式,低于该配置Client模式,64位only server模式
  • 新生代使用的GC收集器(可以查看相应源码,年轻代和老年代的收集器匹配关系,这里就不贴相应源码)
    • 串行GC:(Serial)/(Serial Copying) 使用参数-XX:UseSerialGC,则表示开启Serial(Young区使用)+Serial Old(Old区使用)的收集器组合,新生代、老年代都会使用串行回收器,新生代使用复制算法,老年代使用标记-整理算法
    • 并行GC(ParNew)使用参数-XX:UseParNewGC,表示启用ParNew收集器,只影响新生代,老年代的收集器不发生改变,ParNew(Young区)+Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法,但是,ParNew+Tenured这样的搭配,java8不在推荐使用(注意:可以使用-XX:ParallelGCThreads 限制线程数量默认是cpu数量)
    • 并行回收GC(Parallel)/(Parallel Scavenge):Parallel Scavenge类是ParNew,也是复制算法,吞吐量优先收集器,它能自适应调节策略,根据运行情况收集性能监控信息,动态调整这些参数提供最佳的停顿时间(-xx:MaxGCPauseMills),若使用参数-XX:UseParallelGC则在老年代收集器默认使用-XX:UseParallelOldGC,同ParNew一样也可以通过参数设置并线线程数
  • 老年代(Serial Old 和 Parallel Old在年轻代介绍,以下就不展开)
    • 串行(Serial Old)/(Serial MSC)
    • 并行GC(Parallel Old)/(Parallel MSC)
    • 并发标记清除GC(CMS):并发标记清楚最大的优点低停顿,可以与用户线程一起执行;在使用参数-XX:+UseConcMarkSweepGC后默认使用ParNew(Young区)+CMS(Old区)+Serial Old,其中Serial Old将作为CMS出错的备用收集器
      • 4步过程:
        • 初始标记(CMS initial mark):初始标记对象
        • 并发标记(CMS concurrent mark):可以和用户线程一起并发执行
        • 重新标记(CMS remark):二次确认由于用户线程执行而没有被标记的对象
        • 并发清楚(CMS concurrent sewwp):可以和用户线程一起并发执行
      • 优缺点
        • 优势:并发收集且低停顿
        • 缺点:并发执行cpu资源压力较大、采用标记清楚算法会导致大量内存碎片
  • 总结
    • 多cpu高吞吐使用:ParallelGC
    • 多cpu低停顿,低延时响应快:CMS

5、G1垃圾回收器的深入理解

  • 配置:-XX:+UseG1GC配置参数后发现Heap没有在像之前的GC收集器配置一样划分年轻代、老年代
  • 简介说明:在官网描述中G1是一种服务器端的辣鸡收集器,主要应用在多处理器和大容量的内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停的时间的要求,在jdk9中默认使用G1替代CMS
    • G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW(停顿)
    • G1整体上采用标记整理算法,局部通过复制算法,不会产生内存碎片
    • 宏观意义上G1不在划分年轻代和老年代,而是把内存划分为多个独立的子区域(Region),类比于hdfs的存储系统
    • 在每个Region中保留了年轻代和老年代,每个Region组件不再是物理隔离,而是一些Region的集合且Region之间不需要连续,即不同的Region可以在执行不同的GC操作
    • G1的分区也不存在老年代和年轻代的区别,在G1的运行下每个分区可能在不同代的状态下进行切换
  • 底层原理:它并没有把在宏观意义上Heap划分位年轻代、老年代,取而代之的是将Heap区域划分为多个Region
    • Region区域化:可以通过-XX:G1HeapRegionSize=32m,可以指定分区大小(1MB-32MB,必须是2的幂),默认将整堆划分为2048个分区
    • 回收步骤:小区域收集+区域移动+筛选回收
    • G1收集运行图(后续补充)
  • 与CMS比较
    • 不会产生内存碎片
    • 保证高吞吐的同时,也保证了低停顿

6、扩展(JDK11中的zGC)

借鉴一款收费的GC收集器

后续学习再做补充。。。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值