Java面试题(8)- 虚拟机

Java面试题(8)- 虚拟机

1 说说JVM的内存结构?

JVM内存结构

  • 程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

  • 虚拟机栈

线程私有,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。

动画是由一帧一帧图片连续切换结果的结果而产生的,其实虚拟机的运行和动画也类似,每个在虚拟机中运行的程序也是由许多的帧的切换产生的结果,只是这些帧里面存放的是方法的局部变量,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

  • 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。

堆的大小可以通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。

如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;

新生代:程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。

老年代:用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默 认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。2、大的数组对象,且数组中无引用外部对象。老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

  • 方法区

方法区在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。

简单说方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、连接(验证、准备、解析)和初始化。而装载(后的结果就是由.class文件转变为方法区中的一段特定的数据结构。

VM为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。

每个类的这些元数据,无论是在构建这个类的实例还是调用这个类某个对象的方法,都会访问方法区的这些元数据。

构建一个对象时,JVM会在堆中给对象分配空间,这些空间用来存储当前对象实例属性以及其父类的实例属性(而这些属性信息都是从方法区获得),注意,这里并不是仅仅为当前对象的实例属性分配空间,还需要给父类的实例属性分配,到此其实我们就可以回答第一个问题了,即实例化父类的某个子类时,JVM也会同时构建父类的一个对象。从另外一个角度也可以印证这个问题:调用当前类的构造方法时,首先会调用其父类的构造方法直到Object,而构造方法的调用意味着实例的创建,所以子类实例化时,父类肯定也会被实例化。

类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在JVM使用一个类之前,它必须在方法区中为每个non-final类变量分配空间

2 如何监控和诊断JVM堆内和堆外内存使用?

-可以使用综合性的图形化工具,如 JConsole、VisualVM(从 Oracle JDK 9 开 始,VisualVM 已经不再包含在 JDK 安装包中)等。这些工具使用起来相对比较直观,直接连接到 Java 进程,然后就可以在图形化界面里掌握内存使用情况。

以 JConsole 为例,其内存页面可以显示常见的堆内存和各种堆外部分使用状态。也可以使用命令行工具进行运行时查询,如 jstat 和 jmap 等工具都提供了一些选项,可 以查看堆、方法区等使用数据。也可以使用 jmap 等提供的命令,生成堆转储(Heap Dump)文件,然后利用 jhat 或 Eclipse MAT 等堆转储分析工具进行详细分析。

如果你使用的是 Tomcat、Weblogic 等 Java EE 服务器,这些服务器同样提供了内存管理相关的功能。
另外,从某种程度上来说,GC 日志等输出,同样包含着丰富的信息。这里有一个相对特殊的部分,就是是堆外内存中的直接内存,前面的工具基本不适用,可以使用 JDK 自带的 Native Memory Tracking(NMT)特性,它会从 JVM 本地内存分配的角度进行解读。

3 如何查看JVM的内存使用情况?

(1)进入Java安装的bin目录:cd /usr/local/java/bin
(2)查看CPU使用情况 :top -c
(3)查看jmap命令是否可以使用,如果不能使用,则使用全路径或者配置好环境变量
(4)查看堆内存使用情况 :/usr/local/java/bin/jmap -heap pid
(5)查看内存存活实例 :/usr/local/java/bin/jmap -histo:live pid|more
(6)导出当前线程的快照信息

/usr/local/java/bin/jmap -dump:live,format=b,file=153.dmp pid  
/usr/local/java/bin/jmap -dump:live,format=b,file=153.dmp 19950

(7)如果文件过大,进行压缩 :zip 153.zip 153.dump
(8)导出当前栈使用情况

./jstack 18649 >33.txt
jmap –heap PID > 20220613.TXT  
jmap -histo [pid] > pid.histo.txt  
jstack pid > stack.txt  
split -b 500m log.txt

4 常用的JVM配置和调优参数都有哪些?

-Xms2g:初始化堆大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+UseG1GC:设置使用G1垃圾回收器
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
-XX:+PrintHeapAtGC: 表示可以看到每次GC前后堆内存布局
-XX:UseTLAB:设置使用TLAB
-XX:+PrintTLAB: 表示可以看到TLAB的使用情况。 TLAB的全称是Thread Local Allocation Buffer 即线程本地分配缓存区,这是一个线程专用的内存分配区域。
-verbose:gc(-verbose:class可以输出类加载的信息)
-Xss:表示可以设置虚拟机栈的大小为128k
-Xoss:表示设置本地方法栈的大小为128k。不过HotSpot并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说这个参数是无效的
-XX:+TraceClassLoading: 表示查看类的加载信息
-XX:+TraceClassUnLoading: 表示查看类的卸载信息
-XX:+HeapDumpOnOutOfMemoryError: 表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照
-XX:HeapDumpPath:表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照存储地址
XX:OnOutOfMemoryError:当系统发生OOM错误时,虚拟机在错误发生时运行一段第三方脚本, 比如, 当OOM发生时,重置系统 -=c:\reset.bat
-XX:-UseGCOverheadLimit:取消outofmemory警告
-XX:PretenureSizeThreshold: 表示对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位
-XX:MaxTenuringThreshold: 表示对象年龄大于1,自动进入老年代,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代被回收的概率。
-XX:CompileThreshold: 表示一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译
-XX:+UseSpining:开启自旋锁
-XX:PreBlockSpin:更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁
-XX:MaxGCPauseMillis:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开
-XX:+DoEscapeAnalysis:开启逃逸分析(JDK8中,逃逸分析默认开启)
-XX:-DoEscapeAnalysis:关闭逃逸分析
-XX:+PrintEscapeAnalysis:逃逸分析结果展示
-XX:+EliminateLocks:JDK8中锁消除默认开启

5 如何用Java分配一段连续的1G的内存空间?

ByteBuffer.allocateDirect(1024*1024*1024);

6 Java为什么存在内存泄露呢?

JVM使用引用计数法和可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用,垃圾回收器会自动回收不再使用的对象。但是代码的实现不同就会出现很多种内存泄漏问题,JVM误以为对象还在引用中,无法回收造成内存泄漏。

(1)静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
(2)单例模式,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象引用,那么这个外部对象也不会被回收,就会发生内存泄漏。
(3)未关闭的资源类,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
(4)变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
(5)非静态内部类持有外部类的引用。如果有地方引用了这个非静态内部类,会导致外部类也被引用。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露
(6)改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。
(7)缓存泄漏,把对象引用放入到缓存中,如果遗忘了删除对象,很容易造成内存泄露。一般使用WeakHashMap装填缓存,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
(8)监听器和回调,如果客户端在实现的API中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存他的弱引用,例如将他们保存成为WeakHashMap中的键。

7 内存溢出是怎么回事,请举例说明?

内存溢出就是内存不够,又分栈内存溢出、堆内存溢出、永久代溢出。引起内存溢出的常见原因有以下几种:
(1)内存中加载的数据量过于庞大
(2)集合类中有对象的引用,使用完后未清空,使得 JVM 不能回收
(3)代码中存在死循环或循环产生过多重复的对象实体
(4)启动参数内存值设定的过小

栈内存可以分为虚拟机栈(VM Stack)和本地方法栈(Native Method Stack),除了它们分别用于执行Java方法(字节码)和本地方法,其余部分原理是类似的(以虚拟机栈为例说明)。Java虚拟机栈是线程私有的,当线程中方法被调度时,虚拟机会创建用于保存局部变量表、操作数栈、动态连接和方法出口等信息的栈帧(Stack Frame)。当线程执行某个方法时,JVM会创建栈帧并压栈,此时刚压栈的栈帧就成为了当前栈帧。如果该方法进行递归调用时,JVM每次都会将保存了当前方法数据的栈帧压栈,每次栈帧中的数据都是对当前方法数据的一份拷贝。如果递归的次数足够多,多到栈中栈帧所使用的内存超出了栈内存的最大容量,此时JVM就会抛出StackOverflowError。不论是因为栈帧太大还是栈内存太小,当新的栈帧内存无法被分配时,JVM就会抛出StackOverFlowError,通常栈内存可以通过设置-Xss参数来改变大小。以下代码演示栈内存溢出:

public class StackOverflowErrorDemo {
  private static int stackLength = 0;
  public static void main(String[] args) {
    StackOverflowErrorDemo demo = new StackOverflowErrorDemo();
    try {
      demo.pusStack();
    } catch (Throwable e){
      System.out.println("stack length is: " + demo.stackLength);
      throw e;
    }
  }
  public void pusStack(){
      stackLength++;
      pusStack();
  }
}

以上代码运行结果:

stack length is: 20315
Exception in thread "main" java.lang.StackOverflowError
at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
......

堆内存的唯一作用就是存放数组和对象实例,即通过new指令创建的对象,包括数组和引用类型,堆中对象实例所占的内存空间超出了堆内存的最大容量,JVM就会抛出OutOfMemoryError:java heap space异常。以下代码演示堆内存溢出:

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * 将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展。
 */
public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();

        while (true) {
            list.add(new OOMObject());
        }
    }
}

以上代码运行结果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid9220.hprof ...
Heap dump file created [27717826 bytes in 0.160 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
   at java.util.Arrays.copyOf(Arrays.java:2245)
   at java.util.Arrays.copyOf(Arrays.java:2219)
   at java.util.ArrayList.grow(ArrayList.java:242)
   at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
   at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
   at java.util.ArrayList.add(ArrayList.java:440)
   at com.lindaxuan.outofmemory.HeapOOM.main(HeapOOM.java:19)
   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
   at java.lang.reflect.Method.invoke(Method.java:606)
   at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

8 如何避免内存溢出呢?

(1)修改JVM启动参数-Xms、-Xmx,增加内存
(2)检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误
(3)对代码进行走查和分析,重点排查以下几点:一次性加载数据是否过多;代码中是否有死循环或递归调用;是否有大循环重复产生新对象实体;检查集合对象是否有使用完后,是否未清除对象。

9 JVM为什么需要GC?

GC 是指垃圾收集(Gabage Collection)。内存处理是个比较复杂的地方,编程人员常常会忘记或者错误的内存回收,导致系统不稳定。JVM提供自动GC功能可以让编程人员专注业务开发,不用花精力在程序的内存管理上。随着应用程序越来越庞大,用户越来越多,没有GC就不能保证应用程序正常进行,而GC造成STW(stop the word)又跟不上实际的需求。Oracle一直不断地尝试对GC进行优化,减少应用程序发生停顿的可能性。

10 JVM如何确定垃圾,垃圾回收算法有哪几种?

主要有两种垃圾确定方式,分别是引用计数法与可访问性分析法,其原理分别如下:
(1)引用计数法:在 Java 中,引用与对象相关联,如果要操作对象,则必须使用引用。因此,可以通过引用计数来确定对象是否可以回收。实现原则是,如果一个对象被引用一次,计数器 +1,反之亦然。当计数器为 0 时,该对象不被引用,则该对象被视为垃圾,并且可以被 GC 回收利用。
(2)可达性分析:为了解决引用计数法的循环引用问题,Java采用了可达性分析的方法。其实现原理是,将一系列"GCroot"对象作为搜索起点。如果在"GCroot"和一个对象之间没有可达的路径,则该对象被认为是不可访问的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

垃圾回收算法有四种:标记清除算法、复制算法、标记整理算法和分代收集算法。

  • 标记清除算法

“标记-清除”算法是最基础的算法,分为_标记_和_清除_两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。实现过程如下图:
标记清除算法
这种算法存在两个缺点:
(1)效率问题:标记和清除过程的效率都不高
(2)空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

  • 复制算法

为了解决标记清除算法内存碎片化严重的缺陷,提出了复制算法。复制算法主要思想是,按内存容量将内存划分为大小相等的两块区域。每次只使用其中一块,当这一块内存满后将其中存活的对象复制到另一块上去,然后把该内存中的垃圾对象清理掉,实现过程如下图:

复制算法
复制算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。

  • 标记整理算法

结合了以上两个算法,为了避免缺陷而提出。标记阶段和标记清理算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。实现过程如下图:
标记整理算法

  • 分代收集算法

在结合以上三种算法的综合分析及 JVM 内存对象生命周期的特点,诞生了分代收集算法。其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老年代(Tenured/Old Generation)和新生代(Young Generation)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

(1)新生代采用复制算法
因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space/S0, To Space/S1),每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
JVM新生代
(2)老年代采用标记整理算法
老年代因为每次只回收少量对象,因而采用标记整理算法。
对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老年代;
当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,EdenSpace 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 FromSpace 进行清理;
如果 To Space 无法足够存储某个对象,则将这个对象存储到老年代;
在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环;
当对象在 Survivor 区躲过一次 GC 后,其年龄就会 +1。默认情况下年龄到达 15 的对象会被移到老年代中。

11 GC收集器有哪些,常见的调优方法有哪些?

  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel old
  • 并发回收器:CMS、G1

(1)Serial:最基本的垃圾收集器,使用复制算法,单线程,虽然收集垃圾时需要暂停其他所有的工作线程,但简单高效,虚拟机运行在 Client 模式下默认的新生代垃圾收集器在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial Old GC。
(2)ParNew:是 Serial 收集器的多线程版本 ,除了多线程进行GC外,其他与Serial一样,默认开启和 CPU 数目相同的线程数 。很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
(3)Parallel Scavenge:关注程序的吞吐量,即吞吐量优先。主要适用于在后台运算而不需要太多交互的任务。 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
(4)Parallel old:Parallel Scavenge的老年代版本
(5)CMS:采用标记清除算法,低延迟,主要目标为获取最短垃圾回收停顿时间,可以为交互较高的程序提高用户体验。另外,标记清除算法会产生大量的空间碎片,可能出现空间足够大,但是没有足够的连续空间用来分配对象。由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
(6)G1:垃圾收集器理论发展最前沿的成果,对比CMS收集器有两点改进:一是采用标记整理算法,不产生内存碎片,二是可以精确控制停顿时间,在不牺牲吞吐量的前提下,减少垃圾回收停顿。使用-XX:+UseG1GC来启用。G1收集器将堆内存划分为大小固定的几个区域(局部压缩),以分区为单位进行回收,将存活对象复制到另一个空闲空间。并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。内存分区上不存在老年代和新生代的区别,只是在逻辑上有分代的概念,随着G1的运行,每个分区都可能在不同代之间切换。G1的收集都是STW,属于混合收集的方式,每次收集时可能只收集年轻代的区域,也可能收集年轻代的同时,也包含部分老年代区域。

12 常用的GC策略,什么时候会触发YGC和FGC?

YGC :对新生代堆进行GC。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小
FGC :全堆范围的GC。默认堆空间使用到达80%(可调整)的时候会触发FGC
YGC的时机:edn空间不足
FGC的时机:
(1)old空间不足
(2)perm空间不足
(3)显示调用System.gc() ,包括RMI等的定时触发
(4)YGC时的悲观策略
(5)dump live的内存信息时(jmap –dump:live)

参考
https://www.zhihu.com/question/58943470?sort=created
https://blog.csdn.net/u013773608/article/details/126044448
https://www.cnblogs.com/weechang/p/12489045.html
https://blog.csdn.net/weixin_46115362/article/details/122383160

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注