JVM常见问题

第一章 类加载机制

1.1 Java运行时一个类是什么时候被加载的?

一个类在什么时候开始被加载,《Java虚拟机规范》中并没有进行强制约束,交给了虚拟机自己去自由实现,HotSpot虚拟机是按需加载,在需要用到该类的时候加载这个类;

1、Sun公司最早的 Classic虚拟机;

2、Sun/Oracle公司的HotSpot虚拟机;

3、BEA公司的JRockit虚拟机;

4、IBM公司的IBM J9虚拟机;

jvm监控类的加载参数:

-XX:+TraceClassLoading

1.2 JVM一个类的加载过程?

一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段:

1、加载(Loading)

2、验证(Verification)

3、准备(Preparation)

4、解析(Resolution)

5、初始化(Initialization)

6、使用(Using)

7、卸载(Unloading)

其中验证、准备、解析三个阶段统称为连接(Linking);

img

简化后就是:加载->连接->初始化->使用->卸载

加载:classpath、jar包、网络、某个磁盘位置下的类的class文件二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间(jdk1.8以后叫元空间、之前叫方法区或永久代),此阶段我们程序员可以干预,我们可以自定义类加载器来实现类的加载;

验证:验证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全;

准备:类变量(static)赋默认初始值,int为0,long为0L,boolean为false,引用类型为null;常量赋正式值;

解析:把符号引用翻译为直接引用;

初始化:当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化...... 那么这些都会触发类的初始化;

使用:使用这个类;

卸载:

卸载条件较为苛刻,jvm基于性能等各方面的考虑,一般不会取卸载类。

1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;

2.加载该类的ClassLoader已经被GC(类加载器也被回收了);

3.该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;

1.3 一个类被初始化的过程?

img

类的初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码;

进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,才真正初始化类变量和其他资源;

public class Test02 {

    // 静态常量  ==准备
    public static final String staticConstantField = "静态常量";

    // 静态变量 ==准备阶段赋值为 null,初始化阶段赋值为 静态变量
    public static String staticField = "静态变量";

    // 变量 == 创建对象的时候赋值
    public String field = "变量";

    // 静态初始化块 ==初始化阶段执行
    static {
        System.out.println(staticConstantField);
        System.out.println(staticField);
        System.out.println("静态初始化块");
    }

    // 普通初始化块 == 创建对象的时候执行
    {
        System.out.println(field);
        System.out.println("初始化块");
    }

    // 构造器 == 创建对象的时候执行
    public Test02() {
        System.out.println("构造器");
    }

    // java Test02
    public static void main(String[] args) {
        new Test02();

        // 1、rt.jar charset.jar
        // 2、InitialOrderTest
    }
}

img

1.4 继承时父子类的初始化顺序是怎样的?

//父类
public class ParentClass {

    // 静态变量
    public static String p_StaticField = "父类--静态变量";

    // 变量
    public String p_Field = "父类--变量";

    protected int i = 0;
    protected int j = 0;

    // 静态初始化块
    static {
        System.out.println(p_StaticField);
        System.out.println("父类--静态初始化块");
    }

    // 初始化块
    {
        System.out.println(p_Field);
        System.out.println("父类--初始化块");
    }

    // 构造器
    public ParentClass() {
        System.out.print("父类--构造器");
        System.out.println("i=" + i + ", j=" + j);
        i = 1; j = 1;
    }
}
//子类
public class ChildClass extends ParentClass {

    // 静态变量
    public static String s_StaticField = "子类--静态变量";

    // 变量
    public String s_Field = "子类--变量";

    // 静态初始化块
    static {
        System.out.println(s_StaticField);
        System.out.println("子类--静态初始化块");
    }

    // 初始化块
    {
        System.out.println(s_Field);
        System.out.println("子类--初始化块");
    }

    // 构造器
    public ChildClass() {
        System.out.print("子类--构造器");
        System.out.println("i=" + i + ",j=" + j);
    }

    // 程序入口 java ChildClass
    public static void main(String[] args) {
        new ChildClass();
    }
}

父类--静态变量

父类--静态初始化块

子类--静态变量

子类--静态初始化块

父类--变量

父类--初始化块

父类--构造器

子类--变量

子类--初始化块

子类--构造器

1.5 究竟什么是类加载器?

在类“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的“代码”被称为“类加载器”(Class Loader),这个动作是可以自定义实现的;这段代码可能是java可能是C++。

1.6 JVM有哪些类加载器?

站在Java虚拟机的角度来看,只存在两种不同的类加载器:

1、启动类加载器(Bootstrap ClassLoader),使用C++语言实现,是虚拟机自身的一部分;

2、其他所有的类加载器,由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader;

站在Java开发者的角度来看,自JDK 1.2开始,Java一直保持着三层类加载器架构;

img

1.7 JVM中不同的类加载器加载哪些文件?

1、启动类加载器(Bootstrap ClassLoader)

(根的类加载器)C++语言实现的

<JAVA_HOME>\jre\lib\rt.jar,resources.jar、charsets.jar等

被-Xbootclasspath参数所指定的路径中存放的类库;

2、扩展类加载器(Extension ClassLoader)

sun.misc.Launcher$ExtClassLoader,

<JAVA_HOME>\jre\lib\ext,

被java.ext.dirs系统变量所指定的路径中所有的类库;

3、应用程序类加载器(Application ClassLoader):系统的类加载器

sun.misc.Launcher$AppClassLoader

加载用户类路径(ClassPath)上所有的类库;

1.8 JVM三层类加载器之间的关系是继承吗?

Bootstrap ClassLoader是C++实现的

Extension ClassLoader和Application ClassLoader是launch下的静态内部类,都基础自ClassLoader。

image-20220919142420268

image-20220919142535549

自定义的类加载器也继承自ClassLoader(抽象类)

1.9 你了解JVM类加载的双亲委派模型吗?

img

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,下一层类加载器才会尝试自己去加载;

1.10 JDK为什么要设计双亲委派模型,有什么好处?

1、确保安全,避免Java核心类库被修改(核心类库由Bootstrap ClassLoader加载);

2、避免重复加载(自己写的类库不会被加载);

3、保证类的唯一性;

如果你写一个jaa.lang.String的类去运行,发现会抛出如下异常;

image-20220919144106630

1.12 如何自定义自己的类加载器?

1、继承ClassLoader

2、覆盖findClass(String name)方法 或者 loadClass() 方法;

findClass(String name)方法 不会打破双亲委派;

loadClass() 方法 可以打破双亲委派;

1.13 ClassLoader中的loadClass()、findClass()、defineClass()区别?

loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;

findClass() 根据名称或位置加载.class字节码;(findClass() 在双亲委派之后执行,loadClass()中有findClass() )

definclass() 把字节码转化为java.lang.Class;

1、当我们想要自定义一个类加载器的时候,并且想破坏双亲委派模型时,我们会重写loadClass()方法;

2、如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?可以可以重写findClass方法(),findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法,这个方法只抛出了一个异常,没有默认实现;

JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中;

所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass()中实现你自己的加载逻辑即可;

1.14 你了解Tomcat 的类加载机制吗?

img

可以看到,在原来的Java的类加载机制基础上,Tomcat新增了3个基础类加载器和每个Web应用的类加载器+JSP类加载器;

3个基础类加载器在 conf/catalina.properties 中进行配置:

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar" 
server.loader= 
shared.loader=

Tomcat自定义了WebAppClassLoader类加载器,打破了双亲委派的机制,即如果收到类加载的请求,首先会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类,我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat打破了这个规则,重写了loadClass方法,我们可以看到WebAppClassLoader类中重写了loadClass方法;

1.15 为什么Tomcat要破坏双亲委派模型?

Tomcat是web容器,那么一个web容器可能需要部署多个应用程序;

1、部署在同一个Tomcat上的两个Web应用所使用的Java类库要相互隔离(Spring版本不一致);

2、部署在同一个Tomcat上的两个Web应用所使用的Java类库要互相共享(都需要JDBC、都需要servlet等,用于共享);

3、保证Tomcat服务器自身的安全(Tomcat本身是java写的,不能影响Tomcat的类加载)不受部署的Web应用程序影响;

4、需要支持JSP页面的热部署和热加载;

1.16 有没有听说过热加载和热部署,如何自己实现一个热加载?

热加载 是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于Java的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境;

热部署 是指可以在不重启服务的情况下重新部署整个项目,比如Tomcat热部署就是在程序运行时,如果我们修改了War包中的内容,那么Tomcat就会删除之前的War包解压的文件夹,重新解压新的War包生成新的文件夹;

1、热加载是在运行时重新加载class,后台会启动一个线程不断检测你的class是否发生改变;

2、热部署是在运行时重新部署整个项目,耗时相对较高;

如何实现热加载呢?

在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的class文件,然后重新进行加载;

1、实现自己的类加载器;

2、从自己的类加载器中加载要热加载的类;

3、不断轮训要热加载的类class文件是否有更新,如果有更新,重新加载;

第二章 JVM内存结构

2.1 Java代码到底是如何运行起来的?

image-20220919201622578

1、Mall.java -->javac(命令) --> Mall.class --> java Mall (jvm进程,也就是一个jvm虚拟机)

2、Mall.java -->javac-->Mall.class 打jar包-->Mall.jar --> java -jar Mall.jar

3、Mall.java --> javac --> Mall.class 打war包 -->Mall.war --> Tomcat --> startup.sh --> org.apache.catalina.startup.Bootstrap (jvm进程,也就是一个jvm虚拟机)

其实运行起来一个Java程序,都是通过D:\dev\Java\jdk1.8.0_251\bin\java 启动一个JVM虚拟机,在虚拟机里面运行Mall.class字节码文件;

2.2 来,画一下JVM整个运行原理图?

img

2.3 请介绍一下JVM的内存结构划分?

加载进来的.class字节码文件、代码执行创建的对象、代码执行调用方法,方法中有变量等数据需要一个地方存放,所以JVM划分出了几个区域,用于存放这些信息;hotspot

img

在JDK1.8之前,元空间就是原来的方法区(永久代);

2.4 JVM哪些区域是线程私有的,哪些区域是线程共享的?

左边线程私有,右边线程共享

1、堆、元空间(方法区)是线程共享的;

2、其他区域是线程私有的;

2.5 从JVM角度剖析如下程序代码如何执行?

img

img

先加载Application类,放到元空间里面。main ()方法先压入虚拟机栈,然后load()方法压入,load()方法中有一个局部变量,存在虚拟机栈,局部变量指向的对象存储在堆中,指向的对象Config要通过加载器从元空间加载进来。创建Config对象时候,有一个静态变量manager存储在元空间里面,创建的对象在堆里面。loadData()方法再压入栈。

img

2.6 JVM运行时数据区 程序计数器 的特点及作用?

1、程序计数器是一块较小的内存空间,几乎可以忽略;

2、是当前线程所执行的字节码的行号指示器;

3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;

4、该区域是“线程私有”的内存,每个线程独立存储;

5、该区域不存在OutOfMemoryError;

2.7 JVM运行时数据区 虚拟机栈的特点及作用?

1、线程私有;

2、方法执行会创建栈帧,存储局部变量表等信息;

3、方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)

4、栈深度大于虚拟机所允许StackOverflowError;(递归调用 造成栈溢出)

5、栈在启动线程时就申请好了,不会动态扩展。 OutOfMemoryError(比较少见 创建大量线程会出现);hotspot虚拟机没有OutOfMemoryError;

6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值(常量值在元空间中)、对象值等)都存放到堆上的;

7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;

8、随线程而生,随线程而灭;

9、该区域不会有GC回收(线程结束就销毁了);

image-20220919210633047

一个方法创建一个栈帧

2.8 JVM运行时数据区 本地方法栈的特点及作用?

1、与虚拟机栈基本类似;

2、区别在于本地方法栈为Native方法(Thread类中就有很多Native方法)服务;

3、HotSpot虚拟机将虚拟机栈和本地方法栈合并;

4、有StackOverflowError和OutOfMemoryError(大量创建线程会出现,比较少见);

5、随线程而生,随线程而灭;

6、GC不会回收该区域;

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;

2.9 JVM运行时数据区 Java堆的特点及作用?

image-20220919213805329

假设堆占300M

  1. 线程共享的一块区域;

  2. 虚拟机启动时创建;

  3. 虚拟机所管理的内存中最大的一块区域;

  4. 存放所有实例对象或数组;

  5. GC垃圾收集器的主要管理区域;

  6. 可分为新生代、老年代;

  7. 新生代更细化可分为Eden、From Survivor、To Survivor,Eden:Survivor = 8:1:1

  8. 可通过-Xmx、-Xms调节堆大小;

  9. 无法再扩展java.lang.OutOfMemoryError: Java heap space

    OOM(原因不断往堆中放数据)

  10. 如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率(先放私有的TLAB,满了再往公共区域放);

2.10 JVM中对象如何在堆内存分配?

1、指针碰撞(Bump The Pointer):内存规整的情况下,排列在内存中整齐;

2、空闲列表(Free List):空闲列表记录堆中空闲位置,内存不规整的情况下;

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定

因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;

而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存;

3、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):对象创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;

那么解决方案有两种:

(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;

(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(以空间换时间),称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB(+使用,-不使用)参数来设定;

-XX:TLABSize=512k 设置大小;

2.11 JVM堆内存中的对象布局?

在 HotSpot 虚拟机中,一个对象的存储结构分为3块区域:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)

对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit,官方称为 ‘Mark Word’;

img

第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;

实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);

对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot虚拟机,任何对象的大小都是8字节的整数倍

2.12 JVM什么情况下会发生堆内存溢出?

Java堆中用于储存对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径(引用链路,不允许回收)来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制后就会产生内存溢出;

image-20220920102712596

MAT工具分析xxx.hprof文件,排查溢出的原因;

  • -Xms3072M
  • -Xmx3072M
  • -Xmn1536M
  • -Xss1M
  • -XX:-UseCompressedClassPointers
  • -XX:MetaspaceSize=256M
  • -XX:MaxMetaspaceSize=256M
  • -XX:SurvivorRatio=8
  • -XX:MaxTenuringThreshold=5
  • -XX:PretenureSizeThreshold=1M
  • -XX:+PrintGCDetails
  • -XX:+PrintGCDateStamps
  • -Xloggc:d:/dev/gc.log
  • -XX:+HeapDumpOnOutOfMemoryError
  • -XX:HeapDumpPath=d:/dev/heapdump.hprof

2.13 JVM如何判断对象可以被回收?

在JVM堆里面存放着所有的Java对象,垃圾收集器在对堆进行回收前,首先要确定这些对象之中哪些还“存活”着,哪些已经“死去”;

Java通过 可达性分析(Reachability Analysis)算法 来判定对象是否存活的;

该算法的基本思路:通过一系列称为“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收器回收;

img

对象object 5、object 6、object 7虽然有关联,但它们到GC Roots是不可达的,所以它们将会被判定为可回收的对象;

哪些对象可以作为GC Roots呢?

image-20220920103741254

1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等所引用的对象;

2、方法区/元空间中的类静态属性引用的对象;

3、方法区/元空间中的常量引用的对象;

4、在本地方法栈中JNI(即通常所说的Native方法)引用的对象;

5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;

6、所有被同步锁(synchronized关键字)持有的对象;

7、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;

8、其他可能临时性加入的对象;

2.14 谈谈Java中不同的引用类型?

Java里有不同的引用类型,分别是强引用、软引用、弱引用 和 虚引用;

强引用:Object object = new Object();(即便内存溢出,也不能被回收)

软引用:SoftReference 内存充足时不回收,内存不足时则回收;

弱引用:WeakReference 不管内存是否充足,只要GC一运行就会回收该引用对象;

image-20220920104602361

虚引用:PhantomReference这个其实暂时忽略也行,因为很少用,它形同虚设,就像没有引用一样,其作用就是该引用对象被GC回收时候触发一个系统通知,或者触发进一步的处理;

2.15 JVM堆内存分代模型?

JVM堆内存的分代模型:新生代、老年代;

大部分对象朝生夕死(放新生代),少数对象长期存活(放老年代);

img

  • minorGC 是清理整合新生代的过程, eden 的清理,S0\S1的清理都由于MinorGC
  • MajorGC:清理整合老年代的内存空间
  • Full GC为一次特殊GC行为的描述,这次GC会回收整个堆的内存,包含老年代,新生代,metaspace等

2.16 请介绍一下JVM堆中新生代的垃圾回收过程?

img

先在新生区Eden中分配,满了之后触发垃圾回收,不能回收的放入空闲S0、S1复制来复制去。经历15次仍没有被垃圾回收,就送入老年代中。

JVM里垃圾回收针对的是 新生代,老年代,还有元空间/方法区(永久代),

不会针对方法的栈帧进行回收,方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉,也就是虚拟机栈不存在垃圾回收;

代码里创建出来的对象,一般就是两种:

1、一种是短期存活的,分配在Java堆内存之后,迅速使用完就会被垃圾回收;

2、一种是长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用;

第一种短期存活的对象,是在Java堆内存的新生代里分配;

第二种长期存活的对象,通过在新生代S0区和S1区来回被垃圾回收15次后,进入Java堆内存的老年代中,这里的15次,我们也称为对象的年龄,即对象的年龄为15岁

java -XX:+PrintFlagsFinal 打印jvm默认参数值;

2.17 JVM对象动态年龄判断是怎么回事?

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold=15才能晋升老年代;存在一种动态年龄判断机制

结论-->动态年龄判断:Survivor区的对象年龄从小到大进行累加,当累加到X年龄(某个年龄)时占用空间的总和大于50%(可以使用-XX:TargetSurvivorRatio=? 来设置保留多少空闲空间,默认值是50),那么比X年龄大的对象都会晋升到老年代;

1、Survivor区分布如下图:

img

1-3岁总和小于50%

2、此时新生代GC后,有6%的对象进入Survivor区,Survivor区分布如下图:

img

这时从1岁加到4岁时,总和51% 大于50%,但此时没有大于四岁的对象,即没有对象晋升

3、又经过一次新生代GC后,有40%的对象进入Survivor区,Survivor区分布如下图:

img

Survivor区的对象年龄从小到大进行累加,当累加到 3 年龄时的总和大于50%,那么比3大的都会晋升到老年代,即4岁的20%、5岁的20%晋升到老年代;

2.18 什么是老年代空间分配担保机制

image-20220920162354294

image-20220920162709921

Eden:800m -->300m

S0:100m

S1:100m

老年代:1000m,剩350m、200m

新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?

1、执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;

2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;

3、如果执行Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC”;

所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC

  1. 如果Full GC之后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致“OOM”内存溢出 ;

在JDK6的时候有一个参数-XX:+HandlePromotionFailure用于开启是否要进行空间担保;

2.19 什么情况下对象会进入老年代?

1、躲过15次GC之后进入老年代,可通过JVM参数“-XX:MaxTenuringThreshold”来设置年龄,默认为15岁;

2、动态对象年龄判断;

3、老年代空间担保机制;

4、大对象直接进入老年代;

大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合,大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们,而当复制对象时,大对象又会引起高额的内存复制开销,为了避免新生代里出现那些大对象,然后屡次躲过GC而进行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代

我们可以通过JVM参数“-XX:PretenureSizeThreshold”设置多大的对象直接进入老年代,该值为字节数,比如“1048576”字节就是1MB,该参数表示如果创建一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放入老年代,而不会经过新生代;

-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,其他新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑 ParNew+CMS的收集器组合;

2.20 JVM运行时数据区 元空间的特点及作用?

1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代(在堆中);

2、元空间与Java堆类似,是线程共享的内存区域;

3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据;

4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;

-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;

6、元空间内存不足时,将抛出OutOfMemoryError;

2.21 JVM本机直接内存的特点及作用?

1、直接内存(Direct Memory)不属于JVM运行时数据区,是本机直接物理内存;

2、像在JDK 1.4中新加入了NIO(New Input/Output)类,一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;

3、可能导致OutOfMemoryError异常出现; netty

2.22 JVM本机直接内存溢出问题?

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,该参数表示设置新I / O(java.nio程序包)直接缓冲区分配的最大总大小(以字节为单位);默认情况下,大小设置为0,这意味着JVM自动为NIO直接缓冲区分配选择大小;

由直接内存导致的内存溢出,无法生成Heap Dump文件,如果程序中直接或间接使用了NIO技术,那就可以重点考虑检查一下直接内存方面的原因;

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/dev/heapdump.hprof

2.23 说几个与JVM内存相关的核心参数?

  • -Xms Java堆内存的大小;
  • -Xmx Java堆内存的最大大小;
  • -Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小;
  • -XX:MetaspaceSize 元空间大小;
  • -XX:MaxMetaspaceSize 元空间最大大小;
  • -Xss 每个线程的栈内存大小;
  • -XX:SurvivorRatio=8 设置eden区 和survivor 区大小的比例,默认是8:1:1;
  • -XX:MaxTenuringThreshold=5 年龄阈值;
  • -XX:+UseConcMarkSweepGC 指定CMS垃圾收集器;
  • -XX:+UseG1GC 指定使用G1垃圾回收器

--查看默认的堆大小及默认的垃圾收集器

java -XX:+PrintCommandLineFlags -version

2.24 如何计算一个对象的大小

使用lucene进行计算

第三章 垃圾回收

3.1 堆为什么要分成年轻代和老年代?

因为年轻代和老年代的特点不同,需要用不同的垃圾回收算法;

老年代的对象,他的特点是需要长期存活,所以需要另外一种垃圾回收算法;

所以需要分为两个区域去存放不同的对象;

1、绝大多数对象都是朝生夕死的;

如果一个区域里面的对象大多都是朝生夕死的,你们就把他们集中在一起,每次回收的时候只关注如何保留少量存活对象,而不是去标记哪些大量将要被回收的对象,就能以较低的待机回收大量的空间;

2、熬过了很多次垃圾收集的对象就越难回收;

如果是需要长期存活的对象,就把他们集中放在一起,虚拟机就可以使用较低的频率来回收这个区域,这样就兼顾了垃圾收集的时间开销和内存的空间有效利用;

3、JVM划分出新生代和老年代之后,垃圾收集器可以每次只回收其中某一个部分或者某些部分的区域,同时有了 Minor GC、Major GC、 Full GC这样的回收类型划分;

  • Minor GC:新生代收集
  • Major GC:老年代收集
  • Full GC:整堆收集:收集新生代、老年代以及元空间(方法区)的垃圾收集;
  • Mixed GC:混合收集,收集整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;

4、根据不同区域对象的存亡特征采用不同的垃圾收集算法:

标记-复制算法

标记-清除算法

标记-整理算法

3.2 JVM堆的年轻代为什么要有两个Survivor区?

1、如果没有Survivor区

这个时候当Eden区域满了之后,每出发一次Minor GC就会把Eden区的对象复制到老年代,这样当老年代满了之后就会出发Major GC\Full GC,频繁出发Full GC会影响性能;

2、如果只有一个Survivor区

创建的对象在Eden中,一旦Eden满了,出发一次Minor GC,Eden中存活的对象就会被移动到Survivor区,下一次Eden满了之后,此时进行Minor GC,Eden和Survivor区域就会将存活的对象存放到老年区,但是这样的垃圾清除会导致Survivor区域会出现内存碎片的问题,当下一次Eden中有一个大对象的时候,可能导致没有一个连续的空间存放,就会触发Minor GC,影响性能;结果不想出现内存碎片还可以进行标记-整理算法,但是标记整理算法又会耗费开销,影响性能;

3.3 Eden区与Survivor区的空间大小比值为什么默认是8:1:1?

这是个经验值

一个eden区 ,新生代对象出生的地方;

两个survivor区,一个用来保存上次新生代GC存活下来的对象,还有一个空着,在新生代GC时把eden+survivor中存活对象复制到这个空的survivor中;

统计和经验表明,90%的对象朝生夕死存活时间极短,每次gc会有90%对象被回收,剩下的10%要预留一个survivor空间去保存;

3.4 请介绍下JVM中的垃圾回收算法?

img

3.4.1 标记-清除算法

标记-清除算法是最基础的收集算法,后续的很多垃圾回收算法是基于该算法而发展出来的,它分为‘ 标记 ’和‘ 清除 ’两个阶段;

1、标记

标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记出所有存活的对象,在标记完成后,统一回收所有未被标记的对象,标记过程就是对象是否属于垃圾的判定过程,基于可达性分析算法判断对象是否可以回收;

2、清除

标记后,对所有被标记的对象进行回收;

img

优点:基于最基础的可达性分析算法,实现简单,后续的收集算法都是基于这种思想实现的;

缺点:

1、执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;

2、内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集;

3.1.4 复制算法

复制算法是标记-复制算法的简称,将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉;

img

优点:实现简单,效率高,解决了标记-清除算法导致的内存碎片问题;

缺点:

1、代价太大,将可分配内存缩小了一半,空间浪费太多了;

2、对象存活率较高时就要进行较多的复制操作,效率将会降低(老年代就是因为对象存活率高,复制算法因此不能适用于老年代);

一般虚拟机都会采用该算法来回收新生代,但是JVM对复制算法进行了改进,JVM并没有按照1:1的比例来划分新生代的内存空间,因为通过大量的统计和研究表明,90%以上的对象都是朝生夕死的,所以JVM把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间,HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有另外一个Survivor空间即10%的新生代会被“浪费”;

当然,90%的对象可被回收仅仅是大部分情况下,我们无法百分百保证每次回收都只有不多于10%的对象存活,因此JVM还有一个空间担保机制的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其它内存区域(实际上就是老年代)进行空间分配担保(Handle Promotion,也就是冒险Minor GC一下);

3.1.5 标记-整理算法

标记-整理算法是根据老年代的特点而产生的;

1、标记

标记过程与上面的标记-清理算法一致,也是基于可达性分析算法进行标记;

2、整理

和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理,让存活对象都向一端移动,然后直接清理掉边界以外的内存;

而标记-清除算法不移动存活对象,导致有大量不连续空间,即内存碎片,而老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新所有引用这些对象的引用,这是一种比较耗时的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿我们也称为“Stop The World”即STW;

但是即便是移动存活对象是耗时的操作,但是如果不这么做,那么在充满内存碎片的空间中分配对象,又影响了对象的分配和访问的效率,所以JVM权衡两者之后,还是采用了移动存活对象的方式,也就是对内存进行了整理

另外像cms垃圾收集器,平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间,所以像基于标记-清除算法的CMS收集器面临空间碎片过多时就会进行一次整理;

img

优点:

1、不会像复制算法那样划分两个区域,提高了空间利用率;

2、不会产生不连续的内存碎片;

缺点:效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率变低;

3.1.6 分代收集算法

现在一般虚拟机的垃圾收集都是采用“ 分代收集 ”算法;

根据对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,JVM根据各个年代的特点采用不同的收集算法;

新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;

老年代中,因为对象存活率较高,采用标记-清理、标记-整理算法来进行回收;

3.5 请介绍一下JVM垃圾收集器?

img

如上图,一共有7种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用,垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器;

新生代收集器:Serial、ParNew、Parallel Scavenge [ˈpærəlel] [ˈskævɪndʒ]

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

垃圾收集器的最前沿成果:ZGC 和 Shenandoah

3.5.1 Serial收集器

新生代收集器,最早的收集器,单线程的,收集时需暂停用户线程的工作(“Stop The World” STW),所以有卡顿现象,效率不高,致使java语言的开发团队一直在改进垃圾收集器的算法和实现,但Serial收集器简单,不会有线程切换的开销,是Client模式下默认的垃圾收集器,-client, -server

jdk8默认是Server模式

参数: -XX:+UseSerialGC

java -XX:+PrintFlagsFinal -version 打印jvm默认的参数值;

img

3.5.2 ParNew收集器

它是新生代收集器,就是Serial收集器的多线程版本,大部分基本一样,单CPU下,ParNew还需要切换线程,可能还不如Serial;

Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,后者CMS收集老年代,"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代垃圾收集器;
"-XX:+UseParNewGC":强制指定使用ParNew;
"-XX:ParallelGCThreads=2":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU核心数量相同;

img

3.5.3 Parallel Scavenge收集器

img

简称Parallel,它是新生代收集器,基于复制算法,并行的多线程收集器(与ParNew收集器类似),侧重于达到一个可控的吞吐量,虚拟机运行100分钟,垃圾收集花1分钟,则吞吐量为99%,有时候我们也把该垃圾收集器叫吞吐量垃圾收集器或者是吞吐量优先的垃圾收集器;而且这个垃圾收集器是jvm默认的新生代的垃圾收集器;

它提供一个参数设置吞吐量:

-XX:MaxGCPauseMillis 该参数设置大于0的毫秒数,每次GC的时间将尽量保持不超过设置的值,但是这个值也不是设置得越小就越好,GC暂停时间越短,那么GC的次数会变得更频繁;

-XX:+UseAdaptiveSizePolicy 自适应新生代大小策略,默认这个参数是开启的,当这个参数被开启之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间获得最大的吞吐量,这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics);

如果我们不知道怎么对jvm调优,我们可以使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(最大停顿时间)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成,自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性;

参数:-XX:+UseParallelGC 指定使用Parallel Scavenge垃圾收集器

java -XX:+PrintCommandLineFlags -version 打印jvm默认初始堆和最大堆大小以及垃圾收集器

java -XX:+PrintFlagsFinal -version 打印jvm所有的默认的参数值;

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:d:/dev/gc.log

Parallel Scavenge垃圾收集器中的Ergonomics负责自动的调节gc暂停时间和吞吐量之间的平衡,自动优化虚拟机的性能;

3.5.4 Serial Old收集器

img

它是Serial收集器的老年代版本,同Serial一样,单线程,可在Client模式下使用,也可在Server模式下使用,采用标记-整理算法,Serial Old收集器也可以作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用;

img

3.5.5 Parallel Old收集器

img

是Parallel Scavenge的老年代版本,多线程,标记整理算法,它是在jdk1.6开始才提供;

在注重吞吐量和CPU资源的情况下, Parallel Scavenge新生代+ Parallel Old老年代是一个很好的搭配;

参数:-XX:+UseParallelOldGC 指定使用Parallel Old收集器;

img

3.5.6 CMS收集器

img

CMS全称Concurrent Mark Sweep,是一款老年代的垃圾收集器,它是追求最短回收停顿时间为目标的收集器,互联网B/S结构的服务器端特别适合此收集器;

我们知道垃圾回收会带来Stop the World(stw)的问题,会导致系统卡死时间过长,很多响应无法处理,所以CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的,基于标记-清除算法;

参数:-XX:+UseConcMarkSweepGC 指定使用CMS垃圾收集器;

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:D:/dev/gc.log

CMS垃圾收集器的运作过程,分为4个阶段:

1、初始标记(stw,标记一下GC Roots能直接关联到的对象,那么这些对象也就是需要存活的对象,速度很快);

2、并发标记(不会stw,追踪GC Roots的整个链路,从GC Roots的直接关联对象开始遍历整个对象引用链路,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行)

3、重新标记(stw,修正并发标记期间,因用户程序继续运行而导致标记产生变化的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短,它其实就是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快的)

4、并发清除(不会stw,清理删除掉标记阶段判断的已经死亡的对象,这个阶段其实是很耗时的,但由于不需要移动存活对象,并且这个阶段也是与用户线程同时并发执行的)

其中初始标记和重新标记需要暂停用户线程(Stop The World),其它阶段都是并发执行,所以总体上暂停时间更短;

CMS垃圾收集器的缺点:

1、并发收集会占用CPU资源,特别是cpu数量小的服务器下,会占用用户线程,导致性能下降,CMS默认启动的回收线程数是(处理器核心数量 + 3)/ 4;

2、会产生浮动垃圾,因为你并发清除的时候用户线程可能还在产生垃圾,这些垃圾没有清除,而且你不能让老年代填满了再清除,你要给用户线程留一定空间,所以jdk1.5默认是老年代68%了就触发回收,jdk1.6则提升到92%;

通过-XX:CMSInitiatingOccupancyFraction参数设置;

默认现在是取 -XX:CMSTriggerRatio 的值,默认是80%;

-XX:CMSInitiatingOccupancyFraction设置得更高,可以降低内存回收频率,获取更好的性能,但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次并发清除失败“并发失败”(Concurrent Mode Failure),如果预留老年代不够用户线程使用,则启用Serial Old收集,这就会暂停用户线程,导致性能下降;

3、CMS基于标记-清除算法,清理后会产生碎片空间,空间碎片过多时,将会导致大对象无法分配,往往会出现老年代还有很多剩余空间,但没有足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC,CMS垃圾收集器有一个

-XX:+UseCMSCompactAtFullCollection开关参数(默认是开启的),用于在CMS收集器进行Full GC时开启内存碎片的整理过程,由于这个内存整理必须移动存活对象,整理过程是无法并发的,就导致停顿时间变长,因此虚拟机设计者们还提供了另外一个参数

-XX:CMSFullGCsBeforeCompaction,即CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)

img

java -XX:+PrintCommandLineFlags -version 打印jvm默认初始堆和最大堆大小以及垃圾收集器

java -XX:+PrintFlagsFinal -version 打印jvm所有的默认的参数值;

3.5.7 G1收集器

3.5.7.1 G1垃圾收集器的基本介绍

img

G1全称Garbage First,G1垃圾回收器可以同时回收新生代和老年代,不需要两个垃圾回收器配合起来使用;

G1垃圾收集器是目前可用于生产环境的最前沿最先进的垃圾收集器,从JDK1.6u14开始试验,到JDK1.7u4达到成熟,直到JDK1.8u40才正式完成,开始可以使用;

JDK 9发布时,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则被声明为不推荐使用(Deprecate)的收集器,如果对JDK 9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃,CMS后续将退出历史舞台;

3.5.7.2 G1垃圾收集器的基本原理

G1是一款可以让我们设置垃圾回收的预期停顿时间的垃圾收集器,设置参数是-XX:MaxGCPauseMillis,默认值是200ms;

其实我们对内存合理分配,优化jvm参数,就是为了尽可能减少新生代(Minor GC),或者是整个老年代(Major GC),或者是整个Java堆(Full GC),尽量减少GC带来的系统停顿,避免影响系统处理请求,G1可以指定垃圾回收导致的系统停顿时间不能超过多久,不管垃圾的多与少,垃圾回收的时间都不要超过我们设置的值(并不是绝对的),G1全权给你负责,保证达到这个目标,这相当于我们就可以直接控制垃圾回收对系统性能的影响了;

所以G1垃圾收集器是尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象,这就是G1垃圾收集器的核心原理;

3.5.7.3 G1垃圾收集器如何做到可预测的停顿时间?

1、这与G1垃圾收集器独特的设计有关,它最大的特点就是把Java整个堆内存拆分为多个大小相等的Region [ˈridʒən] ;区域、分区

img

1、G1它会追踪每个Region的回收价值,即它会计算每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?

3、G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为垃圾回收的最小单元,即每次可以选择一部分Region进行收集,避免在整个Java堆中进行全区域的垃圾收集,让G1收集器去跟踪各个Region里面的垃圾的“回收价值”,然后根据用户设定的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),然后在后台维护一个优先级列表,优先处理回收价值大的那些Region,这也是“Garbage First”名字的由来,这种使用Region划分堆内存空间,基于回收价值的回收方式,保证了G1收集器在有限的时间内尽可能收集更多的垃圾;

比如:G1通过追踪发现,1个Region中的垃圾对象有10MB,回收它需要耗费500毫秒,另一个Region中的垃圾对象有20MB,回收它需要耗费100毫秒,那么G1垃圾收集器基于回收价值计算会选择回收20MB只需要100毫秒的Region;

3.5.7.4 G1垃圾收集器是否还有年代的划分?

G1也有新生代和老年代的概念,但只不过是逻辑上的概念,也就是说一个Region此时是属于新生代的Eden空间,过一会儿可能就属于老年代空间,也就是一个Region在运行过程中动态地扮演着新生代的Eden空间、Survivor空间,或者老年代空间,每个Region并不是固定属于某一个空间,另外新生代、老年代也不一定是连续空间,可能是分开的;

img

img

刚开始Region是空的,可能谁都不属于,然后系统创建对象就分配给了新生代,这个Region被新生代对象放满之后,后续垃圾回收了这个Region,然后下一次同一个Region可能又被分配了老年代,用来放老年代的长时间需要存活的对象,所以Region随时会属于新生代也会属于老年代;

新生代和老年代各自的内存区域在不停地变动,由G1自动控制,也就是Region动态分配给新生代或者老年代,按需分配,然后触发垃圾回收的时候,可以根据设定的预期系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收,保证GC对系统停顿的影响在可控范围内,同时还能尽可能回收最多的对象;

3.5.7.5 G1垃圾收集器中的大对象?

Region中有一类特殊的Humongous [hjuːˈmʌŋgəs]区域,专门用来存储大对象;

img

G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待;

3.5.7.6 G1垃圾收集器内存大小如何设置?

img

每个region = 1m ~32m,最多有2048个region;

G1对应的是一大堆的Region内存区域,最多可以有2048个Region,比如说堆大小是4G(4096MB),那么每个Region的大小就是2MB,Region的取值范围是1M-32M,可以通过参数“-XX:G1HeapRegionSize”指定每个Region是多少兆;

比如说堆大小是4G(4096MB),刚开始,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”来设置新生代初始占比,一般默认值即可,因为在系统运行中,JVM会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”设置,并且一旦Region进行了垃圾回收,此时新生代的Region数量就会减少,这些都是动态的;

新生代 :老年代 = 60% :40%

3.5.7.7 G1垃圾收集器新生代还有Eden和Survivor吗?

img

G1垃圾收集器依然有新生代、老年代的概念,新生代里依然有Eden和Survivor的划分,G1是从CMS发展过来的,以后是要完全取代CMS垃圾收取器的,从jdk9开始G1已经是默认的垃圾收集器,之前的很多技术原理在G1中依然可用,我们知道新生代有一个参数“-XX:SurvivorRatio=8”,所以G1还是可以区分出来属于新生代的Region里哪些属于Eden,哪些属于Survivor;

比如新生代刚开始初始化时有100个Region,那么可能有80个Region是Eden,10个Region分别是两个Survivor,所以G1中依然有Eden和Survivor的概念,它们会各自占据不同的Region;

只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加;

5% -- 60%

3.5.7.8 G1垃圾收集器的新生代垃圾回收

img

G1的新生代也有Eden和Survivor,其触发垃圾回收的机制也是类似的,随着不停在新生代Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%;

假设堆 4G,最大2048个region,每个region为2M,新生代最大60%=2.4G;

一旦新生代达到了设定的占据堆内存的最大大小60%,按照上面的数据大概就是有1200个Region,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区满了,此时触发新生代的GC,G1就会依然用复制算法来进行垃圾回收,进入一个“Stop the World”状态,然后把Eden对应的Region中的存活对象复制到S0对应的Region中,接着回收掉Eden对应的Region中的垃圾对象;

但这个过程与之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms,那么G1就会通过对每个Region追踪回收它需要多少时间,可以回收多少对象来选择回收一部分Region,保证GC停顿时间控制在指定范围内,尽可能多地回收对象;

3.5.7.9 G1垃圾收集器的老年代垃圾回收

1、初始标记,需要Stop the World,不过仅仅标记一下GC Roots直接能引用的对象,这个过程速度很快,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿;

2、并发标记,不需要Stop the World,这个阶段会从GC Roots开始追踪所有的存活对象,初始标记阶段仅仅只是标记GC Roots直接关联的对象,而在并发标记阶段,就会进行GC Roots追踪,从这个GC Root对象直接关联的对象开始往下追踪,追踪全部的存活对象,这个阶段是很耗时的,但可以和系统程序并发运行,所以对系统程序的影响不大;

3、重新标记(最终标记),需要Stop the World,用户程序停止运行,最终标记一下有哪些存活对象,有哪些是垃圾对象;

4、筛选回收,需要Stop the World,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的;

从整体上看,G1垃圾收集像是一种标记-整理算法,它不存在内存碎片问题,实际上它是一种复制算法,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 所以它并不是纯粹地追求低延迟,而是给它设定暂停目标,使其在延迟可控的情况下获得尽可能高的吞吐量;

3.6 什么时候使用G1垃圾收集器?

-XX:+UseGlGC

  1. 针对大内存、多处理器的机器推荐采用G1垃圾收集器,比如堆大小至少6G或以上:
  2. 超过50%的堆空间都被活动数据占用;
  3. 在要求低延迟的场景,也就是GC导致的程序暂停时间要比较少,0.5-1秒之间;
  4. 对象在堆中分配频率或者年代升级频率变化比较大,防止高并发下应用雪崩现象的场景;

3.7 ZGC收集器

-XX:+UseZGC

ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃I圾收集器,它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器,在JDK11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms;

3.8 Shenandoah收集器

Shenandoah作为第一款不由Oracle(包括Sun)公司的虚拟机团队所领导开发的HotSpot垃圾收集器,Oracle官方明确拒绝在OracleJDK12中支持Shenandoah收集器,它是一款只有OpenJDK才会包含的收集器最初由RedHat公司创建的,在2014年的时候贡献给了OpenJDK,Shenandoah收集器能实现在任何堆内存大小下都把垃圾停顿时间限制在10ms以内:

posted @ 2022-09-20 23:38  王陸  阅读(328)  评论(0编辑  收藏  举报