资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

白话说Java虚拟机原理系列【第五章】:内存结构之堆详解-创新互联

文章目录
    • 堆(Heap)
        • 对象在堆中的存储结构
        • 垃圾收集器详解
            • 对象存活判断算法
            • 引用的种类
            • 对象最终判定死亡之两次标记规则
            • `方法区`的垃圾回收原则
            • `垃圾收集算法`
            • 分代收集模型
            • 垃圾收集器
            • 对象分配原则
            • 垃圾收集触发方式
            • 垃圾收集器的参数
            • 异常


成都创新互联公司服务项目包括坊子网站建设、坊子网站制作、坊子网页制作以及坊子网络营销策划等。多年来,我们专注于互联网行业,利用自身积累的技术优势、行业经验、深度合作伙伴关系等,向广大中小型企业、政府机构等提供互联网行业的解决方案,坊子网站推广取得了明显的社会效益与经济效益。目前,我们服务的客户以成都为中心已经辐射到坊子省份的部分城市,未来相信会继续扩大服务区域并继续获得客户的支持与信任!

前导说明:
本文基于《深入理解Java虚拟机》第二版和个人理解完成,
以大白话的形式期望能用大家都看懂的描述讲清楚虚拟机内幕,
后续会增加基于《深入理解Java虚拟机》第三版内容,并进行二个版本的对比

堆(Heap)

线程共享此区域:即存与堆中的对象有并发隐患,存在缓存一致性问题。

所有的对象实例以及数组都分配在堆内存上。

  • 类的对象的存储:堆中的对象内部也会存有一个引用,指向该类的class字节码文件在方法区中的位置,这个的目的是实现向上转型和instanceof功能的需要。
  • 数组的存储:相同类型和维度的数组都是同一个类的实例,而不管数组的长度是多少。
    数组类的名称2部分组成:
    • 1.每一维用一个方括号“[”表示。
    • 2.用字符或字符串表示元素类型。
    • 3.比如:
      一维数组对象int[] a所属类型名为"[I",
      二维数组对象byte[] b所属类型名为"[[B"。
    • 类比:和class字节码文件中的常量池中的描述符的表示方法是一样的。
对象在堆中的存储结构

在这里插入图片描述
由图可知每个部分的存储空间大小,图中是64位虚拟机的存储结构,如果是32位虚拟机,那么Mark Word等标黄的部分都将会是32bit,即4个字节。

对象实例存储结构分为:对象头对象体

  • 对象头:由3部分组成
    • Mark Word:这部分用来存储哈希码(hashcode)、对象锁或GC标记,具体存什么要根据当前对象的锁的状态而定。
      在这里插入图片描述
      由图可知,当对象没有加锁时,存储的是哈希码、对象分代年龄。当对象状态是轻量级锁定时,存储的是指向锁记录的指针,此时的指针则是指向获取了当前轻量级锁的线程的工作内存中拷贝的该对象的地址位置(因为该对象作为了锁,首先它就是共享变量,所以线程要访问他,所以就肯定要拷贝到工作内存)。当状态是重量级锁时,内容是指向了重量级锁的指针。当状态是GC标记时(即垃圾回收时进行的GC标记就是在对象的Mark Word这里做的标记),此时内容为空,因为马上要被回收了,所以有没有内容也不重要了。当对象状态是可偏向时,此时保存的内容是偏向的线程的id、时间戳、分代年龄,其中的偏向线程id,即得到锁的那个线程的id,只不过只有当第一个线程访问同步块时得到的锁才会使用偏向锁,因为偏向锁是想进一步优化轻量级锁,就是连CAS操作都不想用了,因为只有一个线程嘛,所以此时只是将Mark Word的状态改为偏向锁,并把当前线程id标记在Mark Word中,后续此线程再访问此共享变量时,就直接访问了,不需要加锁什么的,但是如果有另一个线程要进入同步块时会检查Mark Work的标记状态,由于没有上锁,此时JVM就会终止偏向锁,膨胀成为轻量级锁,先通过CAS设置Mark Word,如果成功,说明之前那个线程已经不使用了,但是因为这不是首次,所以不会再用偏向锁了,而是设置成轻量级锁,如果CAS失败,说明之前那个线程还在用,此时将会直接膨胀成重量级锁,进行同步,即阻塞挂起,直到等到锁释放。这样的一个从低消耗到高消耗的膨胀升级很有必要,能节省很大的性能。(暂时了解即可,在将线程安全时会详细讲解)
    • Klass word:该部分存储的是该对象指向方法区中该对象的类信息的指针。
    • 数组长度:这部分是只有当前对象是数组时才会有,不是数组则没有此区域。
  • 对象体:就是用来存储对象实例的具体属性数据的。
垃圾收集器详解

堆又称为GC堆,因为他是垃圾收集器管理的主要内存区域。

对象存活判断算法
  • 引用计数算法:给对象实例中添加一个引用计数器,每当有一个地方引用他时,计数器就加1,当引用失效时,计数器就减1,当计数器为0时,说明没有使用这个对象的地方了,可以回收了。无法解决循环引用带来的问题,此方案JVM没有采用。
  • 可达性分析算法:通过一个对象作为基准(成为Gc Roots),从这个对象向下搜索,搜索走过的路径称为引用链,当一个对象到Gc Roots没有任何引用链相连通时,则证明此对象是不可用的。
    在这里插入图片描述
    什么对象可作为Gc Roots?
    思路:首先数据都在运行时数据区,堆可以排除,因为这个区域保存的对象没有什么标记,所以无法作为Gc Roots,寄存器这个也不会有引用,剩下的就是方法区、java栈、本地方法栈了。所以可作为Gc Roots的对象包括以下几种:
    • 1.Java栈(栈帧中的局部变量表)中引用的对象。
      就是说运行着的方法中的变量肯定是存活的,跟他相连通的肯定也是正在用的,如果没有连通着的说明可以回收了。
    • 2.方法区中类静态属性引用的对象。
      类静态属性随类被载入初始时就存在了,可以作为参照标准。
    • 3.方法区中常量引用的对象。
    • 4.本地方法栈中JNI(即一般说的native方法)引用的对象。
引用的种类
  • 强引用
    就是指在代码程序中普遍存在的,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。平时我们基本都是这么用的。
  • 软引用
    用来描述一些还有用但并不是必须的对象,对于软引用关联着的对象,在系统将要发生内存溢出抛异常之前,将会把这些对象标记为回收范围内,当进行垃圾回收时,这些对象将会被释放,如果释放这些后还是内存溢出,此时才会抛出内存溢出异常。
    可以通过SoftReference类来实现软引用。
    比如:
    String value = new String("sy");
    SoftReference sfRefer = new SoftReference (value);
    sfRefer.get();//可以获得引用对象值
  • 弱引用
    也是用来描述非必须对象的,但它比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,即当一个对象只存在弱引用时,只要发生一次垃圾回收,不论内存是否充足,都会回收掉该对象。
    可以通过WeakReference类实现弱引用。
    比如:
    String value = new String("sy");
    WeakReference weakRefer = new WeakReference(value);
    System.gc(); //直接运行gc垃圾收集
    weakRefer.get();//返回null,垃圾收集后什么都没了
  • 虚引用
    又称幽灵引用或幻影引用,这种引用基本没有意义了,因为他比弱引用还要弱,所以这种引用就相当于不存在了,如果标记了虚引用,那目的只有一个就是这个对象被垃圾回收掉的时候可以得到一个通知。可以通过PhantomReference来实现虚引用。
对象最终判定死亡之两次标记规则
  • 首次标记
    当可达性算法判定一个对象不可达时,这时该对象并非直接会被垃圾回收,而是当要进行垃圾收集时,对这些不可达对象进行一次标记,并且进行一个判断,判断该对象是否要执行进一步回收。①如果对象重写了finalize方法且之前没有被调用过,则将该对象存入F-Queue队列。JVM会在启动时自建一个低优先级的线程(Finalizer线程)去获取该队列中的对象,并执行对象的finalize()方法,但是不保证该线程会运行完所有的F-Queue队列中的每个对象的finalize()方法,也不能保障单独一个对象的finalize()方法能运行完成(避免对象的finalize()方法内部执行缓慢或死循环,而导致垃圾回收机制崩溃)。②如果对象没有重写finalize()方法或者重写了该方法但是已经被调用过了,则不执行其他动作,即只是做了一个标记。

    注意:一个对象的finalize()方法只会被执行一次,执行完后会标记,不会再执行第二次。
    还有上边所说的检查那都是系统进行垃圾回收时或手动调用System.gc()时才进行,也即入口都是在要进行垃圾回收时才有的动作。
  • 第二次标记
    第二次标记将会对F-Queue中的对象进行第二次标记,如果没有被finalize()自救的对象将会还在F-Queue队列,如果被自救的(即又存在引用了的对象)则移出F-Queue队列。也即第二次标记将会把在F-Queue中所有对象都回收掉,因为通过finalize()方法自救的对象会被移出F-Queue队列。

    注意:书中说是至少两次标记,所以有可能在队列中的会有被标记多次的还没有回收的,并不是绝对就2次标记后对没有引用的对象进行回收,注意理解一下。

注意:这里的2次标记是在对象在堆中的对象头Mark Word部位,前边已经讲过了,即GC标记。

推测:因为没有覆盖finalize()方法的对象只是将堆中对象的对象头进行了一次GC标记,而不涉及放入F-QUEUE队列,所以第二次标记时也就自然而然被直接回收了。

方法区的垃圾回收原则
  • 1.方法区或说成是永久代的垃圾收集性价比不好,虽说不好也有必要去收集。
  • 2.收集的内容包括废弃常量和无用的类。
  • 3.判断常量的无用条件
    这个判断比较简单,
    只要常量池中的数据不再被使用或者对应的字面量没有被其他引用就可以了,
    如String na = "aa"; 或 String b = na; 
    如果aa在常量池只要na不使用它就可以收回,
    如果na变量在常量池,那么只要b不在使用它就可以回收。
  • 4.判定一个类无用的条件
    1.该类所有的实例都已被回收,也就是java堆中不存在该类的任何实例。
    2.加载该类的ClassLoader已经被收回。
    3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

回收策略:以上只是可以回收的标准,这里不像堆,只要能回收就可以回收了,还需要通过参数配置,是否进行回收。JVM提供了-Xnoclassgc或-verbose:class来控制是否回收无用的类;
以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading来查看类加载和卸载的信息。当然前提是需要JVM的支持,不是所有JVM都支持这些配置策略。

垃圾收集算法
  • 标记-清除算法
    在这里插入图片描述
    概念:如同名字一样,算法分为标记、清除两部分,首先标记所有需要回收的对象,标记完后统一回收所有对象。缺点:会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。注意:算法是没有阻塞不阻塞的,此处说的是算法,所以无需关注阻塞不阻塞全部线程,阻塞不阻塞是垃圾收集器的实现方式该关注的。
  • 复制算法
    在这里插入图片描述
    概念:为了提高效率,该算法将按照容量大小将此容量一分为二,使用时只使用其中的一块,另一块选择空闲,当使用的一块没有空间时,触发垃圾收集,运行此算法将存活的对象复制到另一个空闲的区域,然后清空之前满了的区域,这样下来解决了内存碎片问题。缺点:比如一个500M内存,实际只用了250M,导致只能使用一半,资源浪费。应用:堆内存的新生代的垃圾收集算法就是复制算法。因为新生代的对象存活率较低,比较适合这个算法。
  • 标记整理算法
    在这里插入图片描述
    概念:由于老年代的对象存活率较高,所以复制算法也不合适了(因为如果采用复制算法,将会每次复制大量数据,并且因为存活率高,几乎不会回收多少数据,这样来回复制并不会回收多少数据,反而会浪费一半空间,会使空间利用率极低。),而标记清除算法又会产生大量碎片,于是该算法产生了,标记阶段和标记清除算法一样,标记完存活对象后,不是清除,而是让对象向内存空间的一端移动,移动完成后将另一端空间清理掉,这样既不浪费空间也不会产生碎片了。优点:不浪费空间、也不产生碎片。
  • 分代收集算法(或叫分代收集策略)
    其实这个就不是算法了,只是为了利用各个算法的优点,将堆内存区域分开管理的方案,就是把堆分为新生代和老年代,新生代因为死亡率比较高,每次都只有少量对象存活,所以采用复制算法,而老年代的存活率较高,所以采用标记-清除或标记-整理比较合适;当然也少不了作为永久代的方法区。
分代收集模型
  • 新生代:对象new创建时会优先进入新生代的Eden和其中一个Survivor空间,当Eden和一个Survivor占满后将进行一次新生代的垃圾回收,它会将Eden和一个Survivor中的存活对象复制到Survivor中的另一个空闲区域,比如From Survivor是被使用的,那么ToSurvivor就是空闲的,然后将Eden和From Survivor清空;之后再new就是放入Eden和ToSuvivor,然后重复之前的过程,直到当空闲的一个Suvivor不能容纳存活的对象时,这批存活对象将直接进入老年代,之后再将新生代对应的空间清除。默认的新生代内存分配比例 Eden:From Survivor:To Survivor = 8 : 1 : 1,也即默认被使用的是9/10比例的空间,有1/10的空间是站岗作用。一般新生代的对象存活率较低,所以该算法很合适,而老年代对象存活率较高,这个复制算法就不合适了,因为没有那么多空间还要腾出一半空间来浪费。
    • 1.Eden
    • 2.Survivor
      • 1.From Survivor
      • 2.To Survivor
  • 2.老年代:老年代的数据几乎不会GC,直到空间不足时,会进行Major GC或叫Full GC,会根据使用的垃圾收集器的算法将死亡对象清除。如果最终Full GC后仍然空间不足,此时将会抛出内存溢出OutOfMemoryError异常。
  • 3.永久代:HotSpod的JVM把永久代实现成了方法区,以此来让GC也能收集方法区的垃圾,只不过方法区(永久代)的垃圾收集方案不太理想,主要是想回收常量池和类型的卸载(加载的class文件),但是类型卸载要求严苛,基本没有达到目的。
垃圾收集器

注意图中上半部分为新生代常用算法,下半部分为老年代常用算法

  • serial 收集器
    • 线程:单线程运行
    • 阻塞:一旦运行全程阻塞全部运行线程。
    • 算法:复制算法
    • 适用:新生代
  • parnew 收集器(就是serial的多线程版本)
    • 线程:多线程运行
      默认线程数与cpu数一样多,可通过-XX:ParallelGCThreads参数控制线程数。
    • 阻塞:一旦运行全程阻塞全部运行线程。
    • 算法:复制算法
    • 适用:新生代
  • parallel scavenge 收集器
    • 线程:多线程运行
    • 阻塞:一旦运行全程阻塞全部运行线程。
    • 算法:复制算法
    • 适用:新生代
    • 关注点:程序的吞吐量
      • 1.吞吐量=cpu运行程序的时间/(cpu运行程序的时间+垃圾回收时间)
      • 2.其他收集器都是关注尽量缩短程序阻塞停顿的时间,但此收集器关注的是一个平衡。
      • 3.参数-XX:MaxGCPauseMillis,设置一个大于0的毫秒数,表示大的GC时间,但是这个时间设置太小不代表GC的效率高,因为他是回收新生代,为了提高这个GC的时间,他只能是提高GC的频率,也即每次回收的量小了当然GC时间就短了,但是这样会导致整个GC的时间消耗增多,即薄利多销的道理,最终会导致吞吐量降低。
      • 4.参数-XX:GCTimeRatio,设置一个0到100的整数,表示垃圾收集时间占总运行时间的比例。是吞吐量的倒数,比如设置19,那么允许大GC时间5%(1/(1+19)),默认值为99,就是允许大1%(1/(1+99))的垃圾收集时间。
      • 5.参数-XX:UseAdaptiveSizePolicy,设置自适应策略,即不需要我们自己关注新生代的大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象的年龄(-XX:PretenureSizeThreshold)等,JVM都会为我们自动设置。只需要关注3、4两个参数来搭配吞吐量设置即可。
  • serial old 收集器
    • 线程:单线程运行
    • 阻塞:一旦运行全程阻塞全部运行线程。
    • 算法:标记整理算法
    • 适用:老年代
  • parallel old 收集器
    • 线程:多线程运行
    • 阻塞:一旦运行全程阻塞全部运行线程。
    • 算法:标记整理算法
    • 适用:老年代
    • 搭配parallel scavenge使用比较合适,都是关注吞吐量的收集器。
  • cms 收集器
    • 目标:尽量让总程序阻塞停顿最短时间。
    • 线程:并发执行
    • 算法:标记清除算法
    • 执行过程
      • 1.初始标记(CMS initial mark)
        • 0.单线程进行初始标记。
        • 1.阻塞所有线程运行
        • 2.只标记Gc Roots能直接关联到的对象,即存活对象,速度很快。
      • 2.并发标记(CMS concurrent mark)
        • 1.单线程进行并发执行,不会阻塞任何线程。
        • 2.对初始标记的所有"直接"关联对象进行全部的查找、标记。速度较长,好在是并发。
      • 3.重新标记(CMS remark)
        • 0.多线程进行重新标记。
        • 1.阻塞所有线程运行
        • 2.主要修正并发标记时同时运行的用户线程改变了引用关系的对象,对他们进行重新标记,也比较快。但一般比初始标记要长一些,但远比并发标记时间短。
      • 4.并发清除(CMS concurrent sweep)
        单线程进行并发执行,不会阻塞任何线程。
    • 适用:老年代
    • 缺点
      • 1.CPU敏感问题
        因为cms在并发执行阶段是会占用cpu的上下文资源,这就相当于跟用户线程争抢cpu,导致吞吐量下降,cms默认启动的线程数=(cpu数量+3)/4,比如cpu=2,此时线程数将是1.x,占比一半多,所以他的线程数还是很影响用户线程的。
        icms增强型cms收集器:变种了cms,就是让cms在并发执行时,采用与用户线程交替运行而不是并行,先不讨论怎么实现,但是这种方案实际效果不好,已经废弃了,了解一下即可。
      • 2.浮动垃圾
        因为是并行收集,所以在收集过程中,肯定多多少少会产生一些收集不到的垃圾,即为浮动垃圾,因为收集的是老年代,所以收集时机如果控制不好,比如马上要满了才进行收集,很容易就导致收集过程中内存就崩溃了,此时将会导致收集失败(Concurrent Mode Failure),jdk 1.5 cms默认当老年代达到68%时就会触发垃圾回收,jdk 1.6比例为92%,如果出现失败,此时虚拟机启动后备方案,临时启用serial old收集器对老年代重新进行收集,这样将导致停顿更长的时间。
        参数-XX:CMSInitiatingOccupancyFraction可以调整回收触发的老年代对象占比
      • 3.标记清理算法内存碎片
        标记清理导致会出现内存碎片,这个问题之前就说了,所以无可厚非这里也有,此处是老年代,所以即便空间很大,但是创建对象时不能够提供一个足够大的连续空间时,此时将会直接导致一次垃圾回收(主要这个垃圾收集的触发是一个算法弊端造成的不必要收集,也即提前收集),即Full Gc,其实老年代只要产生垃圾回收就是Full Gc。
        参数-XX:UseCMSCompactAtFullCollection是一个开关,默认是开启,意思是当老年代使用cms收集器时要进行垃圾回收(Full Gc)时先进行内存碎片的合并整理,即先不垃圾回收,不过碎片整理也是需要阻塞的,阻塞全部线程,当然整理完后还是要紧跟着这行这次垃圾回收,所以停顿时间更长,只不过碎片也能整理了,解决的是标记清除算法的bug问题。
        参数-XXCMSFullGCsBeforeCompaction,可以设置执行多少次不整理碎片的Full Gc后,执行一次带有整理碎片功能的Full Gc。默认值为0,表示每次进行Full Gc时都进行碎片压缩整理。
  • .g1 收集器
    • 目标:同cms相同,让停顿时间尽量短。
    • 线程:并发+并行都有。
    • 算法:整体是标记整理算法,局部(两个region之间)是复制算法,就是标记整理+复制算法。
    • 适用:整个堆
      • 1.虽然g1保留了分带收集的新生代、老年代概念,但是他自己也相对弱化了这个概念,他增加了region的概念,这是一个把堆看成一个整体,然后均分成多个region区块,如果保留新生代和老年代的话,就是他会把新生代和老年代拼在一起再均分成若干个region区域,然后对region进行垃圾收集。
      • 2.可预测的垃圾收集时间:g1提供了cms没有的一个参数,可以设置一个长度毫秒级的数值,用来指定垃圾收集时间不超过这个值。之所以g1能进行可控时间的收集,主要就是因为region这个概念,因为他可以不全区域的对堆进行垃圾收集,而是将region排优先级,在指定的时间内,先对性价比高的region进行垃圾回收。这也是g1,garbage first的由来。
      • 3.问题:
        把堆分成region小块,那么就能对单个region进行单独回收吗?
        把堆分成新生代、老年代,那么就能单独对新生代和老年代回收吗?
        其实是同一个问题,因为明显多个region可能都有某一个region中的对象的引用,新生代、老年代也是这个道理,只不过新生代、老年代不明显罢了就是回收新生代时还要扫描老年代有没有引用,那这样就会降低性能了。那么回归region的话题,既然有引用,那根据可达性算法,肯定是要检索整个堆才能最终确定哪些对象有没有引用吧?对的,没有问题。
        • 1.使用RememberedSet解决:不管是g1还是其他收集器,这个问题都是使用RemeberedSet来避免全堆扫描的。
        • 2.g1中每个region都会有一个RememberedSet与之对应。相应其他收集器的新生代和老年代当然也有一个对应的RememberedSet对应。JVM发现有关对应的region内的对象的引用发生变化时,他将会把引用相关的信息都记录到RememberedSet中。这样在垃圾回收时检查可达性引用时,通过RememberedSet即可得知了,而不必全堆检索。
    • 执行过程
      • 1.初始标记(Initial Marking)
        • 0.单线程进行初始标记。
        • 1.阻塞所有线程运行
        • 2.只标记Gc Roots能直接关联到的对象,即存活对象,速度很快。
      • 2.并发标记(Concurrent Marking)
        • 1.单线程进行并发运行,不会阻塞任何线程。
        • 2.对初始标记的所有"直接"关联对象进行全部的查找、标记。速度较长,好在是并发。
      • 3.最终标记(Final Marking)
        • 0.多线程进行重新标记。
        • 1.阻塞所有线程运行
        • 2.主要修正并发标记时同时运行的用户线程改变了引用关系的对象,对他们进行重新标记,并跟合并标记的数据合并在一起。
      • 4.筛选回收(Live Data Counting and Evacuation)
        • 1.多线程进行执行,阻塞所有线程运行。根据sun公司透漏,此处也可以并发非阻塞运行。
        • 2.首先对各个region回收价值和成本排序,根据用户所期望的GC停顿时间制定回收计划,然后进一步执行回收。

概念区分:

  • 1.并行parallel
    指多个垃圾收集线程同时执行,但是执行时仍然是阻塞全部运行线程的。
  • 2.并发Concurrent
    指垃圾收集线程与用户线程同时进行,但是垃圾收集的线程与用户线程不一定是并行,可能是错开执行的。注意理解一下。
  • Minor GC:
    新生代内存空间不足时将会发起一个Minor GC来进行垃圾收集。因为新生代的存活率低的缘故即大量数据一回收就都移出了,所以Minor GC的执行会比较频繁,但每次的速度很快。
  • Major GC/Full GC:
    发生在老年代的垃圾回收。一般出现Major GC都会伴随一次Minor GC但这不是绝对。Major GC一般比Minor GC慢10倍以上。Major GC就是对堆内存全盘整理,即新生代、老年代和永久代都会垃圾回收,只不过永久代可以忽略,因为性价比太低。
对象分配原则
  • 1.对象优先分配新生代,即分配到Eden+(1/2)Survivor
  • 2.大对象分配到老年代
    默认一般对象只要新生代放得下就会放到新生代,但是可以通过参数-XX:PretenureSizeThreshold参数设置一个整数,代表上限,即超过这个大小的对象被创建时,直接分配到老年代。该参数只对serial和parnew收集器有效。
  • 3.长期存活的对象进入老年代
    为了能让新、老对象能体现智能分配,JVM为每个对象都定义了一个age计数器,当对象被分配到新生代后,每经历一次Minor GC它的age计数器都会+1,如果新生代空间一直够用,那么这个age将一直计算下去?不是的,有一个参数-XX:MaxTenuringThreshold可以设置age多大后就直接转入到老年代,默认=15,即经过15次Minor GC后,这些对象将会移入到老年代。
  • 4.动态对象年龄判断
    JVM还提供了另一个可以进入老年代的潜在规则,就是不必等到age达到指定的数值也会进入老年代,就是在Survivor或说成是在新生代中的同龄age对象的总大小如果大于Survivor区(单个Survivor的一半,不是2个Survivor的一半)的一半,那么他们都将被移入老年代。
  • 5.空间分配担保
    就是在发生Minor GC之前,JVM会先检查老年代大连续可用空间是否大于新生代所有对象的总空间,如果成立,那么Minor GC可以顺利完成。
    如果不成立,JVM会再检查HandlePromotionFailture设置的值是否允许冒险执行Minor GC,如果允许,JVM还会检查老年代大连续可用空间是否大于历次晋升老年代对象的平均大小,如果大于,则进行一次Minor GC,如果小于,或者HandlePromotionFailture设置不允许,那么就直接取消Minor GC,而改为执行一次Major GC即Full GC。
垃圾收集触发方式
  • 1.JVM触发新生代Minor GC
  • 2.JVM触发老年代Major GC/Full GC
  • 3.System.gc()手动触发Full GC和调用Runtime.getRuntime().gc()一样。
垃圾收集器的参数

在这里插入图片描述
在这里插入图片描述
①client运行时,默认使用Serial+Serial Old组合;
②server运行时,默认使用Parallel Scavenge+Serial Old组合;
③可通过参数设置出来的组合还有:
ParNew+Serial Old
ParNew+CMS+Serial Old
Parallel Scavenge+Parallel Old
G1
即常用的共有6中组合,其中包含两种默认组合。其实就是图中线段连接出来的组合。
④其他常用参数:
-Xms 最小堆内存
-Xmx 大堆内存
-XX:PermSize 非堆内存(方法区/永久代)大小
-XX:MaxPermSize 非堆大内存
-Xmn 新生代内存大小
-XX:SurvivorRatio eden和survivor比例
-XX:+UseSerialGC 串行收集器
-XX:+UseParallelGC 并行收集器
-XX:+UseParallelGCThreads=8 并行收集器线程数,同时有多少个线程进行垃圾回收,一般与CPU数量相等
-XX:+UseParallelOldGC 指定老年代为并行收集
-XX:+UseConcMarkSweepGC CMS收集器(并发收集器)
-XX:+UseCMSCompactAtFullCollection 开启内存空间压缩和整理,防止过多内存碎片
-XX:CMSFullGCsBeforeCompaction=0 表示多少次Full GC后开始压缩和整理,0表示每次Full GC后立即执行压缩和整理
-XX:CMSInitiatingOccupancyFraction=80% 表示老年代内存空间使用80%时开始执行CMS收集,防止过多的Full GC
-XX:+UseG1GC G1收集器
-XX:MaxTenuringThreshold=0 在年轻代经过几次GC后还存活,就进入老年代,0表示直接进入老年代

异常

堆中如果内存不够用但无法扩展时,将会抛出OutOfMemoryError异常。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


分享文章:白话说Java虚拟机原理系列【第五章】:内存结构之堆详解-创新互联
文章位置:http://cdkjz.cn/article/ceehii.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220