JVM面试FAQ分析

1 入门部分

1.1 为什么要学习JVM?

深入理解JVM可以帮助我们从平台角度提高解决问题的能力:

1.有效防止内存泄漏

2.优化线程锁的使用

3.科学进行垃圾回收

4.提高系统吞吐量

5.降低延迟,提高性能

1.2 你了解哪些JVM产品?

  1. HotSpot

  2. JRockit VM

  3. J9 VM

  4. TaobaoJVM

1.3 JVM的构成有哪几部分?

1.运行时方法区:负责存储数据信息(对象,方法等)

2.执行引擎:负责解释执行字节码,执行GC操作等

3.类加载系统:负责加载类到内存

4.本地方法接口:负责融合不同的变成语言为java所用

2 类加载部分

2.1 你知道哪些类加载器?

BootStrapClassLoader(根/引导类加载器):java的核心类库都是使用引导类加载器进行加载的

AppClassLoader(应用/系统类加载器):对于用户自定义的类来说,默认使用这个类加载器加载

ExtClassLoader(扩展类加载器):该类加载器负责加载java的扩展库或者java.ext.dirs路径下的内容

2.2 什么是双亲委派类加载模型?

java虚拟机对class文件采用的是按需加载的方式,也就是说需要使用该类时才会将他的class文件加载到内存生成class对象,而且加载某个类的class文件时,java虚拟机采用双亲委派模式,即如果一个类加载器收到了一个加载类的请求,不会先去加载这个类,而是会交给他的父类加载器加载,如果父类加载器还有父类,则再委托给父类,直至最终的启动器类加载器,即BootStrapClassLoader.

如果父类加载器可以加载这个类,就加载,若不能加载,则向下分派加载任务,由子类加载器进行加载.

向下分派的过程也和向上委托的过程一样,逐级向下,直到有一个类加载器可以加载这个类.

如果没有一个类加载器可以加载这个类的,那么程序就会抛出异常

2.3 双亲委派方式加载类有什么优势、劣势?

优点:

基于双亲委派模式机制实现了类加载时的优先级层次关系,同时可以保证同一个类只被一个加载器加载,这样更利于java程序的稳定运行

缺点:

1.无法扩展加载源

2.无法实现对字节码文件进行加密后再通过类加载器对其进行解密

3.无法实现隔离类的加载

2.4 描述一些类加载时候的基本步骤是怎样的?

  1. 通过一个类的全限定名来获取其定义的二进制字节流

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

  3. 在java堆中生成一个代表这个类的class对象,作为对方法区中这些数据的访问入口

  4. 加载过程大致可以分为加载,验证,准备,解析,初始化几大阶段,顺序:加载,验证,准备和初始化发生的顺序是确定的,而解析阶段则不一定,且加载,验证,准备和初始化这四个阶段按顺序开始不一定按顺序完成

  5. 另外,一个已经加载的类被卸载的几率很小,至少被卸载的时间是不确定的,假如需要卸载的话可以尝试System.exit(0)

2.5 什么情况下会触发类的加载?

java中的类加载方式主要有两种:隐式加载和显示加载

隐式加载:

1.访问类的静态成员(例如类变量,静态方法)

2.构建类的实例对象(例如使用new关键字构建对象或反射构建对象)

3.构建子类实例对象(构建类的对象时首先会加载父类类型)

显示加载:

1.ClassLoader.loadClass()

2.Class.forName()

2.6 类加载时静态代码块一定会执行吗?

不一定

通过ClassLoader对象的loadClass方法加载类不会执行静态代码块

2.7 如何理解类的主动加载和被动加载?

主动加载:是有目的性的,是显示加载,会执行加载,连接,初始化静态域

被动加载:是被触发的,是隐式加载,只执行加载,连接,不初始化类静态域

2.8 为什么要自己定义类加载器,如何定义?

有时候需要定制类的加载方式以满足某些特殊需求,例如:

1.扩展加载源(从数据库中加载类)

2.防止源码泄漏(对字节码文件进行加密,用时再通过自定义类加载器对其进行解密)

3.隔离类的加载(不同框架有完全相同全限定名的类)

如何定义?

一种简单的方式就是继承URLClassLoader,此类可以直接从指定目录,jar包,网络中加载指定的类资源

3 字节码增强部分

3.1 为何要学习字节码?

对于开发人员,了解字节码可以更准确,直观地理解java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效.另外,字节码增强技术在SpringAOP,各种ORM框架,热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益.除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala,Groovy,Kotlin)一种契机,可以扩展java所没有的特性或者实现各种语法糖.理解字节码后再学习这些语言,可以"逆流而上".从字节码视角看它的设计思路,学习起来也"易如反掌"

3.2 如何解读字节码内容?

1.直接解读,可以通过notepad++(需要安装HEX-Editor插件),打开class文件,文件内容默认是一种16进制的格式

2.在class文件目录使用反编译,通过命令行内输入 javap -v xxx.class

3.jclasslib插件应用

IDEA插件,代码编译后,在菜单栏View中选择Show Bytecode With jclasslib就可以很直观地看到当前字节码文件的类信息,常量池,方法区等信息

3.3 字节码内容由哪几部分构成?

魔数:magic

次版本号:minor_version

主版本号:major_version

常量池计数器:constant_pool_count

常量池:constant_pool[constant_pool_count-1]

类的访问标识:access_flags

当前类名索引值:this_class

父类名索引值:super_class

接口计数:interfaces_count

接口数组:interface[interfaces_count]

成员变量计数:fields_count

成员变量数组:fileds[fields_count]

方法计数:methods_count

方法数组methods[methods_count]

属性计数:attributes_count

属性数组:attribute[attributes_count]

3.4 什么是字节码增强?

字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以实现对现有字节码进行修改或动态生成新的字节码,进而对运行中的程序做修改,实现热部署.也可以跟踪JVM运行中程序的状态,进行性能诊断等.

此外,我们平时使用的动态代理,AOP也与字节码增强密切相关,他们实质上还是利用各种手段生成符合规范的字节码文件

3.5 为什么要进行字节码增强?

可以高效的定位并快速修复一些棘手问题(如线上性能问题,方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率

3.6 你了解哪些字节码增强技术?

ASM技术:对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产.class字节码文件,也可以在类被加载入JVM之前动态修改类行为.ASM的应用场景有AOP(Cglib就是基于ASM),热部署,修改其他jar包中的类等

Javassist技术:Javassist是一个用于分析,编辑和创建Java字节码的类库,相比ASM在指令层次上操作字节码会更加简单直接,可以无需关注字节码刻板的结构,直接使用Java编码的形式,而不需要了解虚拟机指令

JavaAgent技术:Java Agent是Java Instrumentation API的一部分,它提供了向现有已编译的Java类添加字节码的功能,相当于字节码插桩的入口,可以侵入运行JVM上的应用程序,进而修改应用程序中的各类字节码

3.7 什么是热替换以及如何实现?

热替换:当一个程序在运行的时候,我们使用另外一个class文件对原有的一些class文件进行替换或者修改等操作.

实现热替换可以使用asm+javaAgent技术或者javassist+javaAgent技术

实现步骤:

第一步:创建业务service类,将此类作为字节码增强对象

第二步:创建Transformer对象,用于对目标对象进行功能增强:

(首先要添加Javassist的依赖)

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

 

第三步:创建Agent对象,用于调用DefaultClassTransformer对象执行字节码增强,在Agent中可以定义两个方法进行不同时间点进行增强:

1.premain方法,此方法在main方法执行之前执行(方法声明固定写法)

2.agentmain方法,此方法在main方法启动后,也就是程序运行时执行(方法声明固定写法)

 

第四步:添加maven插件用于对项目进行打包,例如:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>8</source>
                <target>8</target>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.2.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>
                    <manifestEntries>
                        <Premain-Class>com.java.jvm.bytecode.instrument.DefaultAgent</Premain-Class>
                        <Agent-Class>com.java.jvm.bytecode.instrument.DefaultAgent</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

第五步:创建CycleServiceTests类,对CycleService对象进行调用,也就是启动服务

 

第六步:添加tools依赖,打包项目

程序启动后,通过VirtualMachine的attach api加载Java Agent,这组api其实是JVM进程之间沟通的桥梁,底层通过socket进行通信,JVM A可以发送一些指令给JVM B,B收到指令后,可以执行对应的逻辑.

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
​

第七步:创建AgentInstrumentTests类

 

第八步:分别运行CycleServiceTests和AgentInstrumentTests类进行测试

4 JVM运行内存部分

4.1 JVM运行内存是如何划分的?

JVM运行时内存大致分为:

程序计数器

虚拟机栈

本地方法栈

方法区

4.2 JVM中的程序计数器用于做什么?

程序计数器是一块较小的内存空间,用来存储指向下一条指令的地址,也可以看做是当前线程执行的字节码的行号指示器

在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条要执行的字节码指令.分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖计数器来完成

4.3 JVM虚拟机栈的结构是怎样的?

1.局部变量表

2.操作数栈(或表达式栈)

3.动态链接(或指向运行时常量池的方法引用)

4.方法返回地址(或方法正常退出或者异常退出的定义)

5.一些附加信息

4.4 JVM虚拟机栈中局部变量表的作用是什么?

局部变量表,也称之为局部变量数组或本地变量表,用于存放方法参数和方法内部定义的局部变量信息.在java程序被编译为Class文件时,就已经确定了每个方法所需局部变量表的大小.

局部变量表以变量槽为最小单位,每个变量槽可以存放一个32位以内的数据类型,所以每个变量槽都应该能存放byte,short,int,char,float,boolean,refrence或returnAddress类型的数据,对于long和double两种,会占用两个变量槽.

4.5 JVM虚拟机栈中操作数栈的作用是什么?

每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出的操作数栈.

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈,用于存储和提供数据.

4.6 JVM堆的构成是怎样的?

1.年轻代:一个伊甸园区和两个幸存区

2.老年代

4.7 Java对象分配内存的过程是怎样的?

1.编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配

2.如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配

3.如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区)

4.如果Eden区无法存储对象,则执行Yong GC(Minor Collection)

5.如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代

6.新生代由Eden区和两个幸存区构成(s1,s2),任意时刻至少有一个幸存区是空的,用于存放下次GC时未被收集的对象

7.GC触发时Eden区所有"可达对象"会被复制到一个幸存区,假设是s1,当幸存区无法触发这些对象时会直接复制到老年代

8.GC再次触发时Eden区和s1幸存区中的"可达对象"会被复制到另一个幸存区s2,同时清空Eden区和s1幸存区

9.GC再次触发时Eden区和s2幸存区中的"可达对象"会被复制到另一个幸存区s1中,同时清空Eden区和s2幸存区,以此类推

10.当多次GC过程完成后,幸存区中的对象存活时间达到了一定阈值(可以用参数-XX:+MaxTenuringThreshold来指定上限,默认为15),会被看成是"年老"的对象,然后直接移动到老年代

简单来说:

1.new的对象先放Eden区,放的下就直接放,放不下就放到老年代中

2.当创建对象,Eden区被填满时,就会触发一次Minor GC,将Eden区中没有被其他对象引用的对象进行销毁,剩余没被销毁的对象转移到幸存区survive0,每个对象都有年龄计数器,当触发了一次GC之后,仍然存活的对象的年龄计数加1.

3.如果Eden区有空间,加载的新对象就放到Eden区(超大的对象放不下会直接放到老年代中)

4.当Eden区再次填满时,触发GC,GC就会回收Eden区和survive0区中没有引用指向的对象,并将剩余存活对象存放入survive1区中,清空survive0和Eden

5.再次触发GC的时候,将存活对象放入survive0区,清空survive1区和Eden,以此类推,若当有对象的年龄达到15的时候还存活着,那么就会将这个对象放入到老年代中

6.老年代满了或者放不下时,就会触发MajorGC,再放不下就OOM(OutOfMemory内存溢出)

4.8 JVM年轻代幸存区设置的比较小会有什么问题?

Eden区被回收时,对象要拷贝到幸存区,假如幸存区比较小,拷贝的对象比较大,对象就会直接存入老年代,这样会增加老年代GC的频率,而分代回收的思想就会被弱化

4.9 JVM年轻代伊甸园区设置的比例比较小会有什么问题?

伊甸园区设置的比较小,会增加GC的频率,可能会导致STW的时间变长,影响系统性能

4.10 JVM堆内存为什么要分成年轻代和老年代?

为了更好地实现垃圾回收

4.11 如何理解JVM方法区以及它的构成是怎样的?

方法区是一种规范,用于存储已被虚拟机加载的类信息,常量,静态变量,即编译后的代码等数据,不同jdk,方法区的实现不同,HotSpot虚拟机在JDK8中使用Native Memory来实现方法区构成:

1.类信息包括对每个加载的类型(类,接口,枚举,注解)以及属性和方法信息

2.常量信息可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型

4.12 什么是逃逸分析以及可以解决什么问题?

逃逸分析是一种数据分析算法,基于此算法可以有效减少java对象在堆内存中的分配.

HotSpot虚拟机的编译器能够分析出一个新的对象的引用范围,然后决定是否要将这个对象分配到堆内存中,例如:

1.当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有逃逸

public void random(){
    Random random = new Random();
    random.nextInt();//该对象没有发生逃逸
}

2.当一个对象在方法中被定义后,它被外部方法所引用,则认为发生了逃逸

public Random random(){
    return new Random();//该对象就是逃逸对象
}

4.13 何为内存溢出以及导致内存溢出的原因?

内存溢出:内存中剩余的空间不足以分配给新的内存请求就会内存溢出,内存溢出可能直接导致系统崩溃

导致内存溢出的原因:

1.内存泄漏是导致内存溢出的一种原因,但内存溢出不全是内存泄漏引起的

2.创建的对象太大导致内存溢出

3.创建的对象太多导致内存溢出

4.方法出现了无限递归调用导致栈内存溢出

5.方法区内存空间不足导致内存溢出

4.14 何为内存泄漏以及内存泄漏的原因是什么?

内存泄漏:动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序结束.这个现象称之为内存泄漏

内存泄漏的原因:

1.大量使用静态变量(静态变量与程序声明周期一样)

2.IO/连接资源用完没有关闭

3.内部类的使用方式存在问题(实例内部类或默认引用外部类对象)

4.缓存应用不当(尽量不要使用强引用)

5.ThreadLocal应用不当(用完没有remove)

4.15 JAVA中的四大引用你知道多少?

1.强引用(Strong Reference)

java中的引用默认就是强引用,任何一个对象的赋值操作就产生了对这个对象的强引用,例如:

public class StrongReferenceUsage{
    @Test
    public void stringReference(){
        Object obj = new Object();
    }
}

new了一个Object对象,并将其赋值给obj变量,这个obj就是new Object()的强引用

强引用的特征是只要有强引用存在,被引用的对象就不会被垃圾回收

2.软引用(Soft Reference)

软引用在java中有个专门的SoftReference类型,软引用的意思是只有在内存不足的情况下,被引用的对象才会被回收

例如:

@Test
public void softReference(){
    Object obj = new Object();
    SoftReference<Object> soft = 
        new SoftReference<>(obj);
    obj = null;
    System.out.println("{" + soft.get() + "}");
    System.gc();
    System.out.println("{" + soft.get() + "}");
}

输出结果:

{java.lang.Object@7d4991ad}
{java.lang.Object@7d4991ad}

可以看到,在内存充足的情况下,SoftReference引用的对象是不会被回收的

3.弱引用(weak Reference)

弱引用和软引用很类似,不同的是弱引用引用的对象只要垃圾回收执行,就会被回收,而不管是否内存不足

例如:

public class WeakReferenceTests{
    @Test
    public void WeakReference(){
        Object obj = new Object();
        WeakReference<Object> weak =
                new WeakReference<>(obj);
        obj = null;
        System.out.println("{" + weak.get() + "}");
        System.gc();
        System.out.println("{" + weak.get() + "}");
    }
}

输出结果:

{java.lang.Object@7d4991ad}
{null}

可以看到,在内存充足的情况下,弱引用的对象还是被回收了

4.虚引用(PhantomReference)

虚引用的作用是跟踪垃圾回收器收集对象的活动,在GC的过程中,如果发现有虚引用,GC则会将引用放到ReferenceQueue(用于存储封装待回收的Reference对象的,ReferenceQueue中的对象是由Reference类中的ReferenceHandler内部类进行处理的)中,由程序员自己处理,当程序员调用ReferenceQueue.poll()方法,将引用ReferenceQueue移除之后,Reference对象会变成Inactive状态,意味着被引用的对象可以被回收了

和软引用与弱引用不同的是,虚引用只有一个构造函数,必须传入ReferenceQueue

例如:

public class PhantomReferenceTests{
    @Test
    public void usePhantomReference(){
        ReferenceQueue<Object> rq =
                new ReferenceQueue<>();
        Object obj = new Object();
        PhantomReference<Object> phantomReference =
                new PhantomReference<>(obj,rq);
        obj = null;
        System.out.println("{" + phantomReference.get() + "}");
        System.gc();
        Reference<Object> r = (Reference<Object>) rq.poll();
        System.out.println("{" + r + "}");
    }
}

输出结果:

{null}
{null}

get的值为null,因为PhantomReference引用的是需要被垃圾回收的对象,所以在类的定义中,get的值一直是返回null

5 JVM垃圾回收部分

5.1 何为GC以及为和要GC?

GC称之为垃圾回收,是对内存中的垃圾对象,采用一定的算法进行内存回收的一个动作.比如,java中的垃圾回收会对内存中的对象进行遍历,对存活的对象进行标记,未标记对象可认为是垃圾对象,然后基于特定算法进行回收.

为何要学习GC

深入理解GC的工作机制,可以帮助我们写出更好的java应用,提高开发效率,同时也是进军大规模应用开发的一个前提

为何要GC

如果没有垃圾回收机制,那么我们在程序运行过程中创建出来的对象过多,程序运行的时间过长之后,会导致内存空间不足,系统性能会下降,如果自己不手动执行垃圾回收,那么就会导致系统崩溃

5.2 你知道哪些GC算法?

1.标记-清除算法(Mark-Sweep)

标记清除算法分为"标记"和"清除"阶段,首先会标记出内存中所有不需要回收的对象,然后从内存中清除所有未标记的对象,如图所示:

该算法的优点是简单直接,缺点是效率低,并且可能产生大量的不连续的碎片.

效率低是因为标记和清除两个过程都需要扫描内存空间(第一次:标记存活对象,第二次:清除没有标记的对象)

清除后产生大量的不连续的内存碎片空间,无法满足较大对象的存储需求,这样就可能会再次触发垃圾回收.

所以此垃圾回收算法,适用于对象存活率较高的内存区域(例如JVM中的老年代)

2.标记-复制

标记-复制算法(Mark-Copy)是将内存分为大小相同的两块区域,当这一块区域使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区域,如图所示:

该算法的缺点就是内存空间利用率低,适用于对象生命周期短,回收频率高的内存区域(例如JVM中的年轻代)

3.标记-整理-清除算法

标记整理清除算法(Mark-Sweep-Compact)结合了"标记-清除"和"复制"两个算法的优点,第一阶段从根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把存活对象"压缩"复制到堆的其中一块空间中,按顺序排放.第三阶段清理掉存活边界外的全部内存空间.如图所示:

系统GC时,每次执行清除操作,JVM都必须保证"不可达对象"占用的内存能被回收然后重用.内存是被回收了,但这有可能会产生大量的内存碎片,进而引发两个问题:

1.对象创建时,执行写入操作越来越耗时,因为寻找一块足够大的空闲内存会变得更加麻烦

2.对象创建时,JVM需要在连续的内存块中为对象分配内存.如果碎片问题很严重,直至没有空闲片段能存放新创建的对象,就会发生内存分配错误(allocation error).

为了解决碎片问题,JVM在启动GC执行垃圾收集的过程中,不仅仅是标记和清除,还需要执行"内存碎片整理".这个过程会让所有可达对象进行依次移动.进而可以消除(或减少)内存碎片,并为新对象提供更大并且连续的内存空间.

标记整理算法避免了"标记-清除"的碎片问题,同时也避免了"复制"算法的空间问题.由于需要向一侧移动等一系列操作,其效率相对低一些,但对内存空间管理上十分有益,适用于哪些生命周期长,回收频率低,但注重回收一次内存空间得到足够释放的场景.

5.3 JVM中有哪些垃圾回收器?

1.Serial收集器(串行收集器)

Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5和JAVA SE6中客户端虚拟机采用的默认配置

应用特点:

1.内部只使用一个线程执行垃圾回收(不能充分利用CPU的多核特性),无法并行化

2.GC时所有正在执行的用户线程暂停并且可能会产生较长时间的停顿

应用场景:

1.一般可工作在JVM的客户端模式

2.适用于CPU个数或核数较少且内存空间较少(越大可能停顿时间越长)的场景

算法应用:

1.新生代使用标记-复制算法(新生代存活对象较少)

2.老年代使用标记-清除-整理算法(老年代对象回收较少,容易产生碎片)

2.Parallel收集器(并行收集器)

可以利用多个或多核CPU优势实现多线程并行GC操作,其目标是减少停顿时间,实现更高的吞吐量

应用特点:

1.可利用CPU的多核特性执行多线程下的并行化GC操作

2.GC期间,所有CPU内核都在并行清理垃圾,所以暂停时间较短

3.最大优势是可实现可控的吞吐量与停顿时间

应用场景:

1.GC操作仍需暂停应用程序(也有可能暂停时间比较长,因为GC阶段不能被打断),所以不适合要求低延迟的场景

2.因其高吞吐GC量的特性,适用于后台计算,后台处理的弱交互场景而不是web交互场景

算法应用:

1.在年轻代使用标记-复制算法,对应的是Parallel Scavenge收集器

2.在老年代使用标记-清除-整理算法,对应的是Parallel Old收集器

CMS收集器(并发收集器)

其设计目标是追求更快的响应时间

应用特点:

1.使用空闲列表管理内存存放空间的回收,不对老年代进行碎片整理,减少用户线程暂停时间

2.在标记-清除阶段的大部分工作和用户线程一起并发执行

3.最大优点是可减少停顿时间(可提高服务的响应速度),最大的缺陷是老年代的内存碎片

应用场景:

1.应用于多个或多核处理器,目标降低延迟,缩短停顿时间,响应时间优先

2.CPU受限场景下,因与用户线程竞争CPU,吞吐量会减少

算法应用:

1.年轻代采用并行方式的标记-复制算法

2.老年代主要使用并发的标记-清除算法

关键步骤分析:

1.初始标记阶段:标记一下GC Roots能直接关联到的对象,速度很快

2.并发标记阶段:进行GC Roots Tracing的过程,从直接关联对象遍历所有可达对象,然后进行标记

3.重新标记阶段:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录

4.并发清除阶段:与应用程序并发执行,不需要STW停顿.目的是删除未使用的对象,并收回他们占用的空间

5.并发重置阶段:与应用程序并发执行,重置CMS算法相关的内部数据,同时GC线程切换到用户线程

G1收集器

这是一种工作于服务端模式的垃圾回收器,主要面向多核,大内存的服务器.在实现高吞吐的同时,也最大限度满足了GC停顿时间的可控目标.G1收集器主要为有如下需求的程序设计:

1.可以像CMS收集器一样能同时和应用线程一起并发的执行

2.减少整理内存空间时的停顿时间

3.要满足可预测的GC停顿时间需求

4.不能牺牲太多的吞吐性能

特点:

1.将java堆均分成大小相同的多个区域(1M-32M,最多2000个,最大支持堆内存64G)

2.内存应用具备极大地弹性(一个或多个不连续的区域共同组成Eden,survivor和old区,但大小不再固定)

3.相对CMS有着更加可控的暂停时间和更大的吞吐量以及更少的碎片(标记整理)

4.支持并行与并发,可充分利用多CPU,多核优势,降低延迟,提高响应速度

应用场景:

1.FullGC发生相对比较频繁或小号的总时长过长

2.对象分配率或对象升级至老年代的比例波动较大

3.较长时间的内存整理停顿

说明:如果现在用CMS或者ParallelOldGC,并且程序运行的很好,没有经历长时间垃圾回收停顿,建议不用迁移

算法应用:

1.年轻代标记复制算法

2.老年代标记清除整理算法

关键步骤应用分析:

1.初始标记:属于Young GC范畴,是stop-the-world活动,对持有老年代对象引用的Survivor区进行标记

2.根区扫描:并发执行,扫描那些对old区有引用的Survivor区,在Young GC发生之前该阶段必须完成

3.并发标记:并发执行,找出整个堆中存活的对象,将空区域标记为"X",此阶段也可能会被Young GC中断

4.再次标记:完全完成对堆存活对象的标记,采用snapshot-at-the-beginning(SATB)算法完成,比CMS用的算法更快.

5.清理:并发执行,统计小堆区中所有存活的对象,并对小堆区进行排序,优先清理垃圾多的小堆区,释放内存

6.复制/清理:对小堆区未被清理对象进行复制,然后再清理

5.4 服务频繁fullgc,younggc次数较少,可能原因?

1.系统承载高并发请求,或者处理数据量过大,导致Young GC很贫乏,而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区过小,导致对象频繁进入老年代,频繁触发Full GC

2.系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年代,必然频繁触发Full GC

3.系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收,一直占用在老年代里,必然频繁触发Full GC

4.Metaspace(永久代)因为加载类过多触发Full GC

5.误调用System.gc()触发Full GC

解决方法:

原因一的解决方法是合理分配内存,调大Survivor区

原因二,三,dump出内存快照,用MAT工具进行分析

上述解决方案都不奏效,必然是最后两种原因

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值