学习:GC垃圾回收、JVM架构、运行时数据区

目录

前言:探索HotSpot JVM、Runtime Data Areas与GC

 一、运行时数据区域

1.1、 程序计数器Register pc

1.2、Java虚拟机栈Java Virtual Machine Stacks    

1.3、本地方法栈 Native Method Stacks     

1.4、堆heap

1.5、方法区Method Area     

1.6、运行时常量池Run-Time Constant Pool      

二、哪些内存要回收?

 2.1、回收堆

2.1.1、引用计数算法

2.1.2、可达性分析算法

 2.2、回收方法区

 2.3、引用

 三、如何回收?

3.1、垃圾收集算法

3.2、垃圾收集器 

3.2.1、经典垃圾收集器

3.2.2、G1收集器

3.3、选择合适的垃圾收集器

3.4、垃圾收集器参数与GC日志

3.4.1、常用参数

3.4.2、GC日志常用参数

四、内存分配和回收策略

4.1、对象优先在Eden上分配

4.2、大对象直接进入老年代

4.3、长期存活的对象将进入老年代

前言:探索HotSpot JVM、Runtime Data Areas与GC

在cmd窗口,我们可以看到"HotSpot",HotSpot JVM具有一种体系结构,该体系结构支持强大的特性和功能基础,并支持实现高性能和大规模可伸缩性的能力。


官方介绍HotSpot JVM主要组件包括类加载器,运行时数据区域和执行引擎。

hotSpot JVM

 从热点架构图中可以看到GC与运行时数据区域是相互联系的,并且在HotSpot虚拟机垃圾收集优化指南 中这样介绍的:

垃圾收集器(GC)自动管理应用程序的动态内存分配请求。 GC通过以下操作执行自动动态内存管理:

  •  从操作系统分配内存并将其还给操作系统
  •  根据请求将内存分发给应用程序
  •  确定应用程序仍在使用该内存的哪些部分
  •  回收未使用的内存,以供应用程序重新使用

根据热点架构图以及GC介绍,我们可以从这几个问题来思考垃圾GC,也是本篇文章的学习思路大纲

GC目录



 一、运行时数据区域


   Java虚拟机定义了在程序执行期间使用的各种运行时数据区域,这些区域有各自的用途、创建和销毁时间。根据Java虚拟机规范(JavaSE 15)的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图所示: 

JVM运行时数据区域

图解:
Java虚拟机可以一次执行多个执行线程,根据线程的角度可以分为:线程私有的数据区、线程共享的数据区。

线程私有:伴随线程的产生而产生,一旦线程终止,私有内存区也会自动消除

1.1、 程序计数器Register pc

  •  在任何时候,每个虚拟机线程都在执行单个方法的代码,即该线程的当前方法, PC可以当作当前线程的字节码的行号指示器 ,指示当前程序执行到了哪一行;
  • 方法如果不是native,PC寄存器包含正在执行的虚拟机指令的地址;
  • 方法是native,PC寄存器值未定义(undefined)
  • PC寄存器足够宽,唯一一个无oom的区域

1.2、Java虚拟机栈Java Virtual Machine Stacks
    

  •  每个Java虚拟机线程都有一个专用的Java虚拟机栈,该栈与线程同时创建,线程的生命周期相同;
  • 描述的是Java方法执行的内存模型;一个线程中,每调用一个方法创建一个栈帧,栈帧存储局部变量表,操作数栈,动态链接,方法返回地址和一些额外的符加信息;
  • 为Java方法服务;
  • 程序执行时入栈,执行完成后栈帧出栈;
  • 系统自动分配,速度快,连续的内存空间;
  • 如果线程请求的栈深度大于虚拟机所允许的深度,则将抛出StackOverflowError;
  • Java虚拟机栈容量可以动态扩展,但栈扩展时无法申请到足够的内存时,则将引发OutOfMemoryError

1.3、本地方法栈 Native Method Stacks
     

  • 与Java虚拟机栈作用类似
  • 为native方法服务
  • 线程私有
  • 与Java虚拟机栈一样会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常

     
 线程共享:
 

1.4、堆heap

     

  •  所有线程共享
  • 分配所有类实例和数组的内存
  • Java虚拟机管理的内存中最大的一块,GC主要就是在Java堆中进行的
  •  堆的大小可以是固定的,也可以根据计算要求进行扩展
  •  堆的内存不必是连续的,分配灵活
  • 为程序员或用户提供对堆的初始大小(-Xms)以及最大堆大小(-Xmx)的控制
  • 如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,将引发 OutOfMemoryError

1.5、方法区Method Area
     

  • 线程共享
  • 存储每个类的结构,如运行时常量池、字段、和方法数据,以及方法和构造函数的代码等数据
  • 尽管在逻辑上是堆的一部分,但有一个别名为Non-heap(非堆)
  •  简单的实现可以选择不进行垃圾回收或压缩
  • 可以是固定大小的,也可以根据计算的需要进行扩展
  •  内存不必是连续的
  • 为程序员或用户提供对方法区的初始大小 -XX:NewSize以及最大方法区大小 -XX:MaxNewSize的控制
  • 如果方法区域中的内存无法满足分配请求,则 Java 虚拟机将引发 OutOfMemoryError


补充:永久代: 

在JDK8之前很多人把方法区也称为“永久代”,是因为当时把收集器的分代设计扩展至方法区,用永久代来实现永久代。    但这种设计导致Java应用更容易遇到OOM的问题(有持久代最大值参数设定:-XX:MaxPermSize,即使不设置也有默认为1/4大小),并且有极少数方法会因为永久代的原因而导致在不同虚拟机下有不同的表现。因此在JDK8,已经完全废弃了永久代的概念,而改用元空间(Meta-space),后文中不再对“永久代”进行解释。

1.6、运行时常量池Run-Time Constant Pool
      

  • 是方法区的一部分
  • 包含多种常量,从编译时已知的字面量到必须运行时解析的方法和字段的符号引用
  •  创建类或接口时,如果运行时常量池的构造需要比 Java 虚拟机的方法区域中可用的内存更多,则 Java 虚拟机将引发 OutOfMemoryError


二、哪些内存要回收?


从上一部分运行时数据区域,我们可以知道线程私有区域伴随线程的产生而产生,一旦线程终止,私有内存区也会自动消除;而Java堆和方法区这两个区域是线程共享的,这部分内存的分配和回收是动态的;所以GC关注的是这部分的内存如何管理,所以接下来讨论的“内存”也仅特指这部分的内存。如下图所示: 

JVM2

 2.1、回收堆


堆用于存储对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象中哪些是“存活”,哪些是“死亡”(不可能再被任何途径使用的对象),判断对象是否存活方法主要为以下两种:

2.1.1、引用计数算法

引用计数算法:每当一个地方引用它时,计数器+1;引用失效时,计数器-1;计数值=0,即不可能再被引用的对象。 

举个简单的例子:对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    // 用于占内存,好分析GC日志
    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        ReferenceCountingGC objA=new ReferenceCountingGC();
        ReferenceCountingGC objB=new ReferenceCountingGC();
        objA.instance=objB;
        objB.instance=objA;
        objA=null;
        objB=null;
        // 假设发生GC objA和objB会不会被GC?
        System.gc();
    }

}

查看运行结果(下图),会发现并没有因为两个对象互相引用就没有回收,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。

 主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
 引用计数

2.1.2、可达性分析算法

可达性分析:向图,树图,把一系列“GC Roots”作为起始点,从节点向下搜索,路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即不可达时,则证明此对象时不可用的。

举例:一颗树有很多丫枝,其中一个分支断了,跟树上没有任何联系,那就说明这个分支没有用了,就可以当垃圾回收去烧了。
 可达性分析


在Java中可作为GCRoots的对象:

1)虚拟机栈(栈帧中的本地变量表)中引用的对象
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象;
4)本地方法栈中JNI(即一般说的Native)引用的对象
5)Java虚拟机内部的引用,如基本类型对应的Class对象,一些常驻的异常对象
6)所有被同步锁(`synchronized`关键字)持有的对象
7)反映Java虚拟机内部情况的JMXbean、JVMTI中注册的回调、本地代码缓存等

 2.2、回收方法区


Java虚拟机规范(JavaSE 15)中提到过可以不要求虚拟机在方法区中实现垃圾收集,但如果平时会大量使用反射、动态代理等字节码框架,动态生成jsp的场景下,也可以了解一下类型卸载。方法区主要回收两部分内容:废弃的常量和不再使用的类型
 废弃的常量:常量池中有某常量,但已经没有任何地方引用该常量,如果这时发生GC,并且判断有必要清理的话,这个常量就会被清理出常量池,常量池中其它的符号引用也与此类似。
 不再被使用的类:

  •  该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  •  加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
  •  该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

 
2.3、引用

在引用计数与可达性分析算法中,判断对象是否存活都和“引用”离不开关系,在JDF1.2版后,Reference的使用(JDK11版) 对引用的概念进行了扩充:

 三、如何回收?

3.1、垃圾收集算法

1、分代收集理论

 收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储,放在现在商用的Java虚拟机里,一般会划分为新生代(Young generation)和老年代(Old generation)。

 在Java堆划分出不同的区域后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域,回收类型划分出“Minor GC”、“Major GC"、”Full GC"

 根据不同的区域安排与其对象存亡特征想匹配的垃圾收集算法,标记-清除、复制、标记-整理算法。

 Minor GC/Young GC:目标只是新生代的垃圾收集;
 Major GC/Old GC:指目标只是老年代的垃圾收集;
 Full GC:收集整个Java堆和方法区的垃圾收集;

2、标记-清除、复制、标记-整理算法

复制

3.2、垃圾收集器 

3.2.1、经典垃圾收集器


Serial, Parallel, CMS将堆分成三个部分: 固定内存大小的年轻代,老年代和永久代 。从堆划分方式来看,我将这部分的垃圾收集器称为经典垃圾收集器。
 

堆结构

1、Serial串行收集器

  •   使用单线程来执行所有的垃圾收集工作,使之相对高效,因为线程之间没有通信开销
  •   适合单处理器计算机,是HotSpot虚拟机运行在客户端模式下的默认新生代收集器 
  •  新生代收集器,采用复制算法,“stop the world":在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束;
  •  -XX:+UseSerialGC

2、ParNew收集器

  • Serial收集器的多线程并行版本 
  • -XX:UseParNewGC(JDK9之后不再起作用)
  • 在JDK9开始,ParNew+CMS不再是官方推荐的服务端模式下的收集器解决方案了,也取消了ParNew+SerialOld组合

 
3、Parallel Scavenge吞吐量优先收集器
 

  • 新生代收集器
  • 也是基于复制算法实现的
  • 并行收集的多线程收集器
  • 目标是达到一个可控制的吞吐量,吞吐量=运行用户代码的时间/ 运(行用户代码的时间+垃圾收集时间)
  • -XX:+UseParallelGC

4、Serial Old收集器

  Serial收集器的老年代版本,使用标记-整理算法

5、Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,基于标记-整理算法

 
6、CMS收集器

  •  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
  • 适合关注服务的相应速度的需求
  • 基于标记-清除算法实现
  • 1)初始标记(CMS initial mark):标记一下GC Roots能直接关联到的对象;
  • 2)并发标记(CMS concurrent mark) : 从GC Roots的直接关联对象开始遍历整个对象图的过程, 耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
  • 3)重新标记(CMS remark) :为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;
  • 4)并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象
  • -XX:+UseCMSCompactAtFullCollection,进行内存碎片合并整理,JDK9已废弃
  • -XX:+CMSFullGCsBeforeCompaction,在Full-GC前进行碎片整理, JDK 9已废弃
  • -XX:+UseCMSCollectionPassing,JDK9已废弃
  • -XX:+CMSIncrementalMode,JDK9已废弃

3.2.2、G1收集器

1、堆布局

经典收集器,所有内存都属于新生代、老年代、永久区这三部分,而G1收集器采用了不同的方法,如图所示:

g1收集器

图解:G1不再以固定大小以及固定数量的分代区域划分,而是将堆划分为一组大小相等的堆区域(Region)。每个区域都有一个连续的虚拟内存范围,根据需要,这些区域集分配了与经典的相同的角色(Eden、survivor、old),区域中还有一类特殊的Humongous区域,专门用来存储大对象。因为大小不固定,所以在内存使用方法提供了更大的灵活性。

2、G1特点

根据 HotSpot虚拟机垃圾收集优化指南 介绍,G1特点包括:

  •  堆大小最大为10 GB或更大,其中超过50%的Java堆占用实时数据。
  •  对象分配和升级的速率可能会随时间而显着变化。
  •  堆中有大量碎片。   
  •  可预测的暂停时间目标目标不超过几百毫秒,避免了长时间的垃圾收集暂停。

3、垃圾收集周期:

G1主要通过疏散来回收空间:在选定的内存区域中收集的活动对象将被复制到新的内存区域,并在此过程中对其进行压缩。疏散完成后,活动对象先前占用的空间将重新用于应用程序分配。
在较高级别上,主要包括两个阶段, G1收集器在两个阶段之间交替。如图所示:

 周期

 仅限年轻阶段:包含垃圾回收,这些垃圾回收逐渐将旧一代中的对象填充到当前可用的内存中;
 空间回收阶段:G1除了处理年轻一代外,还逐步回收老一代的空间。
 在进行空间回收之后,收集周期会从另一个仅年轻阶段开始。作为备份,如果应用程序在收集活动信息时内存不足,则G1会像其他收集器一样执行就地停止的全堆压缩(Full GC)

4、G1的推荐用例
G1的首要重点是为运行需要大堆且GC延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为6GB或更大,并且稳定且可预测的暂停时间低于0.5秒。

如果当前具有CMS或ParallelOldGC垃圾收集器的应用程序具有以下一个或多个特征,则将其切换到G1将非常有益。

  •  完整的GC持续时间太长或太频繁。
  • 对象分配率或提升率差异很大。
  • 不必要的长时间垃圾收集或压缩暂停(长于0.5到1秒)

 
 注意:G1取代了并发标记扫描(CMS)收集器,它也是默认的收集器(JDK9开始)。使用-XX:+UseG1GC显示启动。

3.3、选择合适的垃圾收集器


在选择收集器之前那可以 先调整堆大小 来满足目标,如果还是不能满足,可以从以下几个方面来选择合适的垃圾收集器。

数据集较小(最大约为100 MB),则选择串行收集器-XX:+UseSerialGC

  • 在单个处理器上运行,并且没有暂停时间要求,则选择-XX:+UseSerialGC
  • (a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或一秒或更长时间的暂停是可接受的,则选择并行收集器-XX:+UseParallelGC
  • 响应时间比整体吞吐量更重要,并且必须将垃圾收集暂停时间保持在大约一秒钟以内,则使用-XX:+UseG1GC或选择一个主要是并发的收集器-XX:+UseConcMarkSweepGC
  • 如果响应时间是高优先级,或您使用的堆非常大,请使用选择一个完全并发的收集器-XX:UseZGC。 

3.4、垃圾收集器参数与GC日志

3.4.1、常用参数

-Xms:初始堆大小

-Xmx:最大堆大小

-Xmn:年轻代大小

-Xss:每个线程堆栈大小

-XX:NewSize:设置方法区大小

-XX:MaxNewSize最大方法区

-XX:PermSize:持久代大小(JDK8已废弃,可用-XX:MetaSpaceSize)

-XX:MaxPermSize:持久代最大值(JDK8已废弃,可用-XX:MaxMetaSpaceSize)

-MinHeapFreeRatio:默认为40,如果某代中的可用空间百分比降到40%以下,则该代将被扩展以保持40%的可用空间,直到该代最大允许的大小

-MaxHeapFreeRatio:默认为70,如果可用空间超过70%,则将收缩该世代,以便只有70%的空间是空闲的,这取决于该世代的最小大小

-XX:SurvivorRatio:默认为8,表示Eden与一个Surivor区空间比例时8:1

3.4.2、GC日志常用参数

参数JDK9前日志JDK9后日志
查看GC基本信息-XX:+PrintGC-Xlog:gc
查看GC详细信息-XX:+PrintGCDetails-X-log:gc*
查看熬过收集后剩余对象的年龄分布信息-XX:+PrintTenuringDistribution-Xlog:gc+age=trace

举例说明(我用的JDK8,JDK9之后的只是将日志规范化了):

在这里插入图片描述

四、内存分配和回收策略

在文章前言部分,就提出“内存是怎么分配的?”,以及“什么时候回收”这两个问题,其实GC实现动态内存管理最终就是解决了两个问题,给对象分配内存以及回收分配给对象的内存。
对象的内存分配,从概念上讲,就是堆上分配(实际上也有可能为栈上分配)。在经典分代设计下,新生对象会分配在新生代中,少数情况下(对象大小超过一定的阈值)会直接分配在老年代,但具体的分配规则跟垃圾收集器有关。

注:以下有关增大和缩小堆以及默认堆大小的讨论不适用于并行收集器。 本章节使用HotSpot虚拟机,以服务端模式运行,收集器组合使用的是Serial+Serial Old。

4.1、对象优先在Eden上分配

对象优先在Eden上分配,当Eden区没有足够空间进行分配时,虚拟机将会发起一次Minor GC。
堆分布:
在这里插入图片描述

根据堆分布,设置VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC

代码示例:

 private static final int _1MB = 1024 * 1024;

    /*
     * VM参数:
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * desc:Java堆大小为20M不可扩展,新生代10M(可用空间为9M=Eden(8M)+1个Survivor区(1M)),老年代10M,
     * 使用Serial+SerialOld收集器组合,新生代使用复制算法,每次只使用其中一块Surivor
     */

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }

GC日志:
GC日志

分析:

[GC (Allocation Failure) [DefNew: 8142K->...] ]:这次发生的原因是因为在给allocation4分配内存的时候,发现Eden区已经占用了差不多8M,剩余的空间(1M)已不足以分配allocation4所需的4M内存,因此发生Minor GC。

[GC... [DefNew: 8142K->607K(9216K)...]8142K->6752K(19456K)...]:GC期间虚拟发现allocation1、allocation2、allocation3三个对象都是存活的,3个2MB大小的对象无法全部放入survivor空间(survivor空间只有1M大小可使用),所以只好通过分配担保机制提前转移到老年代去,所以Java堆可用内存GC后的占用量由8142KB变为6752KB,Eden的对象被清空,所以由8142KB变为607KB。

这次GC结束后,程序执行完的结果是def new generation total 9216K, used 4869K 新生代占用了4869KB(被allocation4占用),tenured generation total 10240K, used 6144K老年代占用了6144KB(被allocation1、allocation2、allocation3占用)。

从GC日志我们也可以看到设置VM参数是起作用了的,def new generation total 9216K... eden space 8192K...from space 1024K...to space 1024K... :新生代总可用空间为9M,其中Eden:survior为8:1,survior1和survior2被分为相等的两个内存;tenured generation total 10240K...:老年代总可用空间为10M;Metaspace...:JDK1.8废弃了永久代,而使用元空间。

注意: 这里分析的在给allocation4分配内存时发生的GC,也是根据程序最后执行结果打印的堆空间内存分布占用情况来综合分析的;也有可能遇到在给allocation3分配内存的时候,发生GC,最后老年代占用4M,新生代占用6M的;用并行收集器的话,也可能不会有GC等,数据可能会因为各种因素而改变的情况。因此我这里贴的GC日志图是最贴近我们理想中分析的内存分配情况,着重点应该是理解对象怎么分配的流程策略,以及学习怎么分析GC日志的方法。

4.2、大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象,最典型的大对象便是很长的字符串,或者元素数量很庞大的数组。在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区 之间来回复制,产生大量的内存复制操作。但此参数只对Serial和ParNew两款新生代收集器有效,HotSpot 的其他新生代收集器,如Parallel Scavenge并不支持这个参数。

根据引用提示,添加VM参数:-XX:PretenureSizeThreshold=3145728
代码示例:

  private static final int _1MB = 1024 * 1024;

    /*
     * VM参数:
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * -XX:PretenureSizeThreshold=3145728  
     * desc: 大对象阈值,这个参数不能直接写3MB,设置了此参数之后,超过3MB的对象都会直接在老年代进行分配
     */
    public static void main(String [] args) {
        byte[] allocation;
        allocation = new byte[4 * _1MB]; //  直接分配在老年代中
    }

GC日志:

GC大对象日志

分析: 可以看到没有发生GC,新生代的Eden空间被占用26%(<4M),而老年代被占用了40%,所以allocation对象直接就分配在老年代中,因此可以验证大对象直接进入老年代

4.3、长期存活的对象将进入老年代

对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置

根据引用引用提示,添加VM参数:-XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
代码示例:

  private static final int _1MB = 1024 * 1024;

    /*
     * VM参数:
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
     * desc:年龄设置 设置了此参数后,对象在第二次GC发生时进入老年代 打印
     */
    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4]; //  注意大小,survivor区能够容纳
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];

    }

GC日志:
在这里插入图片描述

分析: 可以从日志看到,进行了一次GC后,age=1,老年代占用内存4M,Eden区大致为4M,survivor区占用内存84%,即allocation1在进入了survivor区;


我们再修改一下代码,让进行第二次GC:

  private static final int _1MB = 1024 * 1024;

    /*
     * VM参数:
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
     * desc:年龄设置 设置了此参数后,对象在第二次GC发生时进入老年代 打印
     */
    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4]; //  注意大小,survivor区能够容纳
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null; //  不可达,其所占内存将会被回收
        allocation4 = new byte[4 * _1MB];  

    }

GC日志:
在这里插入图片描述

分析: 可以看到进行了两次GC,第一次GC后,survivor有占用空间,age=1;第二次GC后,survivor无占用空间,老年代比预期的增加了,即长期存活对象allocation1进入到了老年代。

补充1: HotSpot虚拟机并不是永远要求对象的年龄必须达到最大年龄才到老年代,如果survivor空间中相同年龄多有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代

/*
     * VM参数:
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
     * desc:最大年龄设置
     */
    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4]; //  注意大小,survivor区能够容纳
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];

    }

GC日志:

在这里插入图片描述

分析:比如在上面的代码示例中再添加一个256KB的对象,查看GC日志,会发现survivor区占用为0%,而老年代币预期增加了,也就是说allocation1、allocation2对象都直接进入到了老年代。

补充2:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行Full GC。


学习参考:
《深入理解Java 虚拟机(第三版)》
HotSpot虚拟机垃圾收集优化指南
https://blog.csdn.net/antony9118/article/details/51375662

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值