深入理解java虚拟机(二)----垃圾收集策略与内存分配策略

程序计数器,虚拟机栈,本地方法栈的内存分配和回收具有确定性,每一个栈帧分配多少内存基本在类结构确定下来时就已知了。在这几个区域中也不需要过多的考虑回收的问题,因为方法结束或者线程结束时,内存也边便跟着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能也不一样,我们只有在程序处于运行期间时才知道会创建哪些对象,这部分内存的分配和回收是动态的,这部分是GC关注的主要区域。

下面先介绍几种GC判断对象存活状态的算法
1. 引用计数算法:即对象中添加一个引用计数器,每当一个地方引用它时,计数器值加1,当引用失效时。计数器值减1,任何时刻计数器为0的对象就是不可能再被使用的。但是主流的jvm没有使用这种算法,因为这种算法不能解决循环引用的问题(即两个对象相互引用形成的孤岛)。典型的情况如下面代码所示:

public class MyTest{
    int val;
    MyTest next;
    public static void main(String[] args){
        Mytest m1 = new MyTest();
        MyTest m2 = new MyTest();
        m1.next = m2;
        m2.next = m1;
        m1 = null;
        m2 = null;
        System.gc();
    }
}

2. 可达性分析算法:在主流的jvm中都是通过可达性分析来判定对象是否存活的。这个算法的基本思想是通过一系列的成为”GC Roots”的对象作为起始点,从这些结点开始向下搜索,搜索走过的路径成为引用链,当GC Roots到这个对象不可达的时候,则证明这个对象是不可用的。在java中,可作为GC Roots的对象包括下面几种:
– 虚拟机栈(局部变量表slot)中引用的对象
– 方法区中类静态属性引用的对象
– 方法区中常量引用的对象
– 本地方法栈中JNI引用的对象

其实从上面两种方法都可以看出,对象的存活与否都与”引用”有关。

  • 在JDK 1.2之前,引用的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。但这个定义太过狭隘,我们希望描述这样一类对象:当内存空间还足够时,则保留在内存之中;如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这一类对象
  • 在JDK1.2之后,java对引用的概念进行了扩充,将引用分为强引用,软引用,弱引用,虚引用。强引用:就是我们最经常见到的引用,比如Object o1 = new Object(); 只要强引用存在,gc永远不会回收被引用的对象;软引用:SoftReference o1 = new SoftReference<>(T ref); 一般是缓存,对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常;弱引用:WeakReference,被弱引用关联的对象只能生存到写一次gc发生之前;虚引用:PhantomReference,这种引用和对象的生存时间豪无关系,也不可能通过虚引用来获得对象实例,只是为了在这个对象被回收的时候收到一个系统通知。

即使在可达性分析算法中不可达的对象,也并非非死不可,它们暂时处于”缓刑”的阶段,要真正宣告一个对象的死亡,至少要经历两次标记过程:如果一个对象对于GC Roots不可达,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法当对象没有覆盖finalize()方法,或者finalize方法已经被虚拟机调用过,虚拟机将这两种枪口I昂视为没有必要执行。如果有必要执行,对象会进入F-Queue队列中,由虚拟机自动建立的低优先级的finalizer线程去执行对象的finalize方法。如果某个对象finalize方法死循环,将可能导致整个内存回收系统崩溃。如果对象没有在finalize方法中成功拯救自己(重写与引用链上的任何一个对象建立关联),那它将在第二次标记中被清除,一定要注意一点:一个对象的finalize方法只会被系统调用一次,还有很不建议将这个方法认为成析构函数,因为首先这个方法在F-Queue队列中执行,执行的时间不确定(在低优先级线程中执行),无法保证各个对象的调用顺序,并且执行了这个方法的对象可能并没有被回收。

回收方法区:回收方法区的主要回收两部分内容:废弃常量和无用的类
回收废弃常量和回收对象很相似,如果没有任何地方引用这个字面量将会被回收
回收无用的类需要满足以下三个条件才可能被回收:1.该类的所有实例已经被回收;2.加载该类的classloader已经被回收;3.该类对应的class对象没有在任何地方被引用。
在大量使用反射,动态代理,CGLib等字节码框架(这些会产生动态代理类(或者字节码修改后的类)对象)等场景都需要jvm具备类卸载功能,以保证永久代不溢出。

下面介绍几种垃圾收集算法思想 :
1. 标记-清除算法(Mark-Sweep)算法:算法分为标记和清除两个阶段,首先标记所有需要被回收的对象(一般采用可达性分析来标记),然后统一回收。它的主要不足有两个:1.效率问题,标记和清除两个过程的效率都不高;2.空间问题:标记清除后会产生大量不连续的内存碎片,碎片太多会导致需要分配较大对象时不得不提前触发另一次垃圾收集动作。
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》

2. 复制算法:为了解决效率问题,复制算法将内存按照容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存使用完了,就把存活着的对象复制到另一块上,然后再把一是用过的内存空间一次清理掉。这样效率很高,不需要考虑内存碎片,只要移动堆顶指针,按顺序分配内存即可但是这种算法的代价是将内存缩小为了原来的一半。
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》
这种算法的应用场合真的不少,IBM研究表明,新生代中的对象98%都是朝生夕死的,并不需要1:1划分空间,而是将内存分为一块较大的Eden空间和两个较小的Survivor空间,每次使用Eden空间和一块较小的Survivor。每次回收时将Eden和Survivor存活着的对象一次性的复制到另一个Survivor上,最后清理Eden和Survivor。Hotspot虚拟机默认Eden和Survivor的比例是8:1,也就是每次新生代中可用的内存空间为整个新生代容量的90%,只有10%会被浪费,当然我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor不够用时,需要依赖其他内存(老年代)进行分配担保(就和银行的担保人一样(用的时候不多,但要有一个))

3.标记-整理算法:在老年代中,对象的存活率较高,复制算法效率变低,并且8:1的比例不在适应老年代。所以根据老年代的特点,提出了一种标记-整理的算法,标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界之外的内存
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》

4.分代收集算法(综合2,3):这种算法根据对象存活周期的不同将内存划分为几块,一般将java堆划分为新生代和老年代,这样可以根据各个年代的特点采用最适当的垃圾收集算法。在新生代中,每次gc都有大批对象死区,存活率很低,故采用复制算法。而老年代因为对象存活率高,没有额外空间进行分配担保,故采用标记-清理或者标记-整理算法进行回收。具体对象如何在新生代和老年代分配将在后面的动态内存分配中解释。

HotSpot的算法实现:
枚举根结点:前面提到,在java中,可作为GC Roots的节点主要在全局性引用(常量或静态属性)与执行上下文(栈帧的局部变量表)中。但是现在有些大型应用仅仅方法区便有数百兆,一个一个的遍历效率会很低;除此之外,在可达性分析中,必须在一个能确保一致性的快照中进行,即GC进行时必须停顿所有的java执行线程(对象引用关系不变化),至少枚举根结点的时候必须停顿。在HotSpot的实现中,使用一组称为OopMap的数据结构来达到存储对象引用的目的。这样,GC在扫描时就可以直接得知这些信息。
安全点:在OopMap的协助下,HotSpot可以快速并且准确地完成GC Roots的枚举,jvm不可能在每条指令执行完都为其生成目前的OopMap,这样GC的空间成本就太大了,因为并不是每条指令执行都可以GC,所以只在安全点处生成OopMap并且GC。安全点的选择基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的,一般为方法调用,循环跳转,异常跳转等。大多数虚拟机采用主动式中断让所有线程在GC发生时可以跑到各线程最近的安全点上:当GC需要中断线程的时候,不直接对线程进行操作(抢先式中断会),而是设置一个标志,各个线程执行时主动地去轮询这个标志位,发现中断标志为真就自己中断挂起,各个线程轮询标志的地方是和安全点重合的,所以一定也是挂起在安全点。
安全区域:使用SafePoint似乎已经完美地解决了如何进入GC的问题,安全点机制保证了程序执行时在不太长的时间内就会遇到可进入GC的safepoiont。但是,程序不执行的时候呢?所谓不执行就是没有分配CPU时间,典型的例子就是县城处于Sleep状态或者Block状态,这个时候线程无法响应JVM的中断请求(没有分配CPU时间片去轮询),走到安全的地方中断,JVM也不可能等现成重新被分配CPU时间。这个情况就需要安全区域来解决。安全区域是指一个代码片段中,引用关系不会发生变化。在这个区域任何地方开始GC都很安全。当线程执行到安全区域的时候,就会标识自己进入了安全区域,JVM不需同步这些线程到安全点,当线程要离开安全区域的时候,要检查系统是否已经完成了根结点枚举(或者整个GC过程),如果没有,则要等待其完成才能接着往下走。

垃圾收集器:是收集算法的具体实现
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》
上图展示了7中作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,上面的几种收集器各有优劣,没有一个是完美收集器。
1. Serial收集器:它曾经(在JDK1.3.1之前)是jvm新生代收集的唯一选择,这是一个单线程收集器,它在进行垃圾收集时,必须暂停其他所有的工作线程去完成垃圾收集工作,直到其收集结束。下图是Serial/Serial Old收集器运行示意图
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》
实际上这个收集器依然是Client模式在的默认新生代收集器。它有着优于其他收集器的地方:简单,高效,对于单个CPU,没有线程交互的开销,一般停顿时间也只有一百毫秒左右,只要不是频繁发生,这些停顿是可以接受的。对于桌面应用场景而言(大型服务器确实有点不能接受)
2. ParNew收集器:它是Serial收集器的多线程版本,下图是ParNew/Serial Old收集器运行示意图
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》
它是许多运行在server模式下的虚拟机中首选的新生代收集器,其中一个与性能无关但是很重要的原因是,除了Serial之外,只有它能和CMS收集器配合工作。ParNew也是使用-XX : +UseConcMarkSweepGC选项后的默认新生代加载器,也可以使用-XX : UseParNewGC选项来强制使用它。ParNew在单CPU下绝对不比Serial好(多线程在单CPU下只是交替串行),甚至还存在线程交互的开销,它默认开启的收集线程数和CPU的数量相同(PS:超线程的未来发展,是提升处理器的逻辑线程,英特尔有计划将8核心的处理器,加以配合超线程技术,使之成为16个逻辑线程的产品。),下面还有必要解释一下并发并行的概念。并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;并发:是指用户线程与垃圾收集线程同时执行(但不一定并行,可能是交替执行)。
下面是另一个并发和并行的区别解释:并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
3. Parallel Scavenge收集器:它是一个新生代的收集器,它也是使用复制算法的收集器,也是一个并行的多线程收集器。它的特别之处在于它的关注点和其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程停顿的时间,而Parallel Scavenge收集器得目标则是达到一个可控制的吞吐量(Throughput),所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。
停顿时间越短就越适合ui,提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不是需要太多交互的任务。
Parallel Scavenge收集器有两个参数来控制吞吐量:最大垃圾收集停顿时间:-XX : MaxGCPauseMills以及直接设置吞吐量大小的-XX : GCTimeRatio参数。最大停顿时间将以牺牲新生代空间和吞吐量为代价(空间小了,次数多了,单次时间就短了,但总的时间可能更长了)。由于和吞吐量关系密切,Parallel Scavenge收集器一般也被称为吞吐量优先的收集器。-XX : UseApdativeSizePolicy来开关收集器的自适应调节策略(新生代大小-Xmn,Eden/Survivor比例-XX : SurvivorRatio,晋升老年代年龄等细节参数-XX : PreTenureSizeThreshold,其他堆参数还要自己设)
4. Serial Old收集器:它是Serial收集器的老年版本,同样是一个单线程收集器,使用”标记-整理”算法,在Client中经常使用,在Server中,一般作为CMS的后备预案,在并发收集发生Concurrent Mode Failure时使用。
5. Parallel Old收集器:它是Parallel Scavenge收集器的老年代版本,采用多线程和”标记-整理”算法,在没有Parallel Old收集器之前,Parallel Scavenge收集器的处境很尴尬,只能和Serial Old收集器搭配使用,由于老年代的Serial Old收集器在性能上的拖累,使得Parallel Scavenge收集器也未必能在整体引用上获得吞吐量最大化的效果,直到Parallel Old收集器出现后,“吞吐量优先”收集器有了名副其实的组合,在注重吞吐量和对CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old的组合。下图是Parallel Scavenge加Parallel Old的组合的运行示意图。
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》
6. CMS(Concurrent Mark Sweep)收集器:重视系统停顿时间,重视系统响应速度。整个运作过程分为四个步骤:初始标记→并发标记→重新标记→并发清除。其中初始标记和重新标记两个步骤仍然需要”stop the world”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是进行GC Roots Tracing的过程;重新标记则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿要比初始标记稍长一些,但远比并发标记短。由于整个过程中最耗费时间的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》
但是CMS会有以下的一些不足之处
并发的思想对CPU资源敏感,会降低用户进程的执行速度,导致CPU吞吐量降低,虽然减小了停顿时间,但是整体也慢了下来。
CMS无法处理“浮动垃圾”(即在并发标记时程序运行产生的新的垃圾)。所以为了给浮动垃圾留有空间,CMS一般不是在老年代快满的时候才GC,JDK 1.5默认老年代使用了68%之后就会激活,如果在应用中老年代增长的不是很快,可以通过调节-XX : CMSInitializingOccupancyFraction来提高触发百分比。如果CMS运行期间预留的内存无法满足程序需要,就会出现一次”Concurrent Mode Failure”,会改用Serial Old收集器,性能降低。
CMS基于标记-清除,会导致大量的内存碎片产生。空间碎片过多时,大对象的分配会很麻烦。CMS提供了一个-XX :UseCMSCompactAtFullCollection开关参数开启整理过程(默认开启),内存整理的过程是无法和用户进程并发的,所以碎片没有了,停顿的时间变长了。还有一个参数 -XX : CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后,执行一次带压缩的。默认为0.
7. G1收集器:是JDK 1.7中HotSpot虚拟机的一个重要进化特征。G1是一款面向服务端应用的垃圾收集器,未来可能会替换掉CMS收集器,与其他GC相比,G1具备以下特点:
– 并行与并发:G1能充分地利用多CPU环境下的硬件优势,使用多个CPU来减小Stop-The-World的停顿时间。
– 分代收集:分代概念得以保留,G1可以不需要其他收集器而管理整个GC堆。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过了多次GC的旧对象以获取更好的收集效果。
– 空间整合:与CMS的标记-清理不同,G1从整体看来是标记-整理的收集器,从局部(两个Region)上看是基于复制算法实现的。但无论如何,这两种算法都不会产生内存碎片,有利于程序长时间运行和大对象内存分配。
– 可预测的停顿:G1除了追求低停顿之外,还能建立可预测的停顿模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不超过N毫秒。
在G1之前的其他收集器收集对的范围都是新生代或者老年代。使用G1时,java的内存布局有较大的差别,它将整个java堆划分为多个大小相等的域(region),虽然还保留新生代和老年代的概念,但新生代和老年代已经不是物理隔离的了,他们都是一部分Region的集合
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
G1收集器的流程和CMS大致相同,只是最后的筛选回收并没有与用户进程并发执行,其实可以,但是因为只回收一部分Region,时间是用户可控的, 而且停顿用户进程将大幅提高收集效率。
《深入理解java虚拟机(二)----垃圾收集策略与内存分配策略》

内存分配与回收策略(传统分代机制):对象的内存分配,往大方向上讲,就是堆上分配,对象主要分配到新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程有现在TLAB上分配(JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。)。少数情况也可能直接分配到老年代中。PS:这里有一个将java对象分配的博客写的很好,java的对象一定分配到堆上吗?Java的逃逸分析和TLAB
下面将介绍几条最普遍的内存分配规则,采用默认Serial/Serial Old收集器。
1. 对象优先在Eden区分配:大多数情况下,对象优先在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一起Minor GC。代码如下,虚拟机参数设置:-XX : PrintGCDetails打印内存回收日志,并在进程退出的时候打印内存各区域分配情况。-Xms20M, -Xmx20M, -Xmn10M, -XX : SurvivorRatio = 8。java代码如下:

private static final int _1MB = 1024*1024;
/* VM arguments : -verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8 */
public static void testAllocation(){
    byte[] alloc1, alloc2, alloc3, alloc4;
    alloc1 = new byte[2*_1MB];
    alloc2 = new byte[2*_1MB];
    alloc3 = new byte[2*_1MB];
    alloc4 = new byte[4*_1MB]; //Minor GC
}

因为Eden不够了,所以触发一起Minor GC,而在GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间,所以只有通过分配担保机制提前转移到老年代中去。运行结果如下:

java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC AllocTest 

there is a test
Heap
 def new generation   total 9216K, used 4681K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  53% used [0x00000000fec00000, 0x00000000ff051838, 0x00000000ff400000)
  from space 1024K,  25% used [0x00000000ff500000, 0x00000000ff540e60, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 2474K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 267K, capacity 386K, committed 512K, reserved 1048576K

2.大对象直接进入老年代:所谓的大对象指,需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。大对象分配对虚拟机是一个噩耗,将会直接进入老年代,如果运气不好,还要进行一次压缩以减小内存碎片。比遇到一个大对象更加坏的消息就是遇到一群”朝生夕灭”的”短命大对象”,写程序的时候应当避免。虚拟机提供了参数(参数之间千万不要忘记加空格)-XX:PreTenureSizeThreshold参数,另大于这个设置值的对象直接在老年代分配。这样做的目的是为了避免Eden区和Survivor区之间的大量的内存复制这个参数只对Serial和ParNew有效。

private static int _1MB = 1024 * 1024;
public static void main(String[] args){
    byte[] a1;
    a1=new byte[4*_1MB]; //直接分配到老年代
} 

运行结果:

java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:XX:PretenureSizeThreshold =3145728 AllocTest 
there is a test
Heap
 def new generation   total 9216K, used 672K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   8% used [0x00000000fec00000, 0x00000000feca8048, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 2474K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 267K, capacity 386K, committed 512K, reserved 1048576K

//可以看到对象接进入老年代

**3.长期存活的对象进入老年代:**jvm给每个对象都定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC仍然存活,并且能够被Survivor容纳的话,将会被移动到Survivor空间中,并将对象的年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加一岁,当它的年龄增长到一定程度的时候(默认15岁),就将会晋升到老年代中。对象晋升到老年代的年龄阈值可以通过参数-XX:MaxTenuringThreshold设置。

private static int _1MB = 1024 * 1024;
public static void main(String[] args){
    byte[] a1,a2,a3;
    a1=new byte[_1MB/4]
    a2=new byte[4*_1MB];
    a3=new byte[4*_1MB];
    a3=null;
    a3=new byte[4*_1MB];
}

执行结果:

//-XX:MaxTenuringThreshold=1 两次GC后新生代大小为0
[GC (Allocation Failure) [DefNew: 4859K->515K(9216K), 0.0037286 secs] 4859K->4611K(19456K), 0.0038285 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 4611K->0K(9216K), 0.0008652 secs] 8707K->4610K(19456K), 0.0009258 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4610K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  45% used [0x00000000ff600000, 0x00000000ffa80b40, 0x00000000ffa80c00, 0x0000000100000000)
 Metaspace       used 2473K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 267K, capacity 386K, committed 512K, reserved 1048576K

4. 对象动态年龄的判定:虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代。如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于或等于该年龄的对象将直接进入老年代。
5. 空间分配担保:在发生Minor GC之前,虚拟机会检查老年代的最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么才可以担保,Minor GC才是安全的。如果不成立,jvm会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历代晋升到老年代的对象大小的平均值,如果大于,虽然有风险,但还是会进行一次Minor GC,如果小于或者HandlePromotionFailure不允许担保失败,那这时要改为进行一次Full GC。

下面介绍几个比较重要的概念:

新生代GC(Minor GC):指发生在新生代的垃圾回收动作,因为Java对象大多具备朝生夕灭的特性,所以Mixor GC非常频繁,采用copy算法,存活率低,所以速度快。
老年代GC(Major GC,Full GC):指发生在老年代的GC,出现Major GC,经常会伴有至少一次的Minor GC。Major GC速度很慢,存活率高,一般不采用copy算法。
动态:在运行时根据实际情况决定!所以动态的灵活性比较大。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/zx64881926/article/details/54748062
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞