JVM快速入门学习参考资料第一部

JVM快速入门

学习路线:

在这里插入图片描述

一、内存结构

在这里插入图片描述

1.程序计数器

1.1 定义

Program Counter Register 程序计数器(寄存器)

  • 作用:是记住下一条jvm指令的执行地址

  • 特点

    • 线程私有的,即每个线程有一个自己的专属寄存器,随着线程创建而创建,随着线程销毁而销毁

      例如:线程一在程序执行到jvm指令步骤10时,CPU时间片被线程二抢走了,那么寄存器就会记录下下一处执行哪一条jvm指令,即记录下一次执行步骤11。

    • 不会存在内存溢出

1.2 作用

在这里插入图片描述

2.虚拟机栈

2.1 定义

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈。
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法,也就是处于栈顶部的方法

在这里插入图片描述

  • 栈:线程运行所需的内存。
  • 栈帧:每个方法所需的内存。(代码的执行,就是一个一个方法的执行)

在这里插入图片描述

栈帧的内部结构

每个栈帧中存储着:

  • 局部变量表(Local Variables)

  • 操作数栈(operand Stack)(或表达式栈)

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

  • 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

  • 一些附加信息

局部变量表(Local Variables)

局部变量表也被称之为局部变量数组或本地变量表

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。

  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。

  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

操作数栈(Operand Stack)

每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)

  • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈

  • 比如:执行复制、交换、求和等操作

public void testAddOperation() {
    byte i = 15;
    int j = 8;
    int k = i + j;
}

在这里插入图片描述

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间


问题辨析

  1. 垃圾回收是否涉及栈内存?

    不需要,我们方法调用完毕后,对应的栈帧会弹出栈。

  2. 栈内存分配越大越好吗?

    不是,栈的内存大小确实可以自定义:

在这里插入图片描述

但首先我们给栈分配的内存是物理空间,是固定大小的,那么这个空间越大,线程数目就越少,物理空间扩大一倍,线程数就少一倍,一般采用默认。
  1. 方法内的局部变量是否线程安全?

    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的,但是,如果是共享的,例如由static修饰,那就会存在线程安全问题。
    • 如果是局部变量引用了对象,并逃离方法的作用方法(return了),需要考虑线程安全。(new 出来的都在堆里,而堆是共享的!)

2.2 栈内存溢出StackOverflowError

  • 栈帧过多导致栈内存溢出,例如:递归调用,如果没有设置正确的结束条件。
  • 栈帧过大导致栈内存溢出

2.3 线程运行诊断

案例1: cpu占用过多

定位

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack进程id
    • 可以根据线程id(十进制 --> 十六进制)找到有问题的线程,进一步定位到问题代码的源码行号

案例2:程序运行很长时间没有结果

死锁问题,可以使用jstack查看:

在这里插入图片描述

2.4 栈相关面试题

  • 举例栈溢出的情况?(StackOverflowError)

    • 通过 -Xss设置栈的大小。
  • 调整栈大小,就能保证不出现溢出么?

    • 不能保证不溢出。
  • 分配的栈内存越大越好么?

    • 不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。
  • 垃圾回收是否涉及到虚拟机栈?

    • 不会。
  • 方法中定义的局部变量是否线程安全?

    • 具体问题具体分析。如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。

3.本地方法栈

给native方法提供的内存空间。

4.堆

4.1 定义

Heap 堆

  • 通过 new 关键字,创建对象都会使用堆内存。

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题。
  • 有垃圾回收机制

堆被细分成了好几块区域,目的只是为了更好的回收内存,或更快的分配内存

在这里插入图片描述

4.2 堆内存溢出OutOfMemoryError

设置堆空间大小:-Xmx 例如,-Xmx8M。

当堆当中不断new对象时,内存总会被消耗完,虽然有GC,但是只有没被引用的对象才会被GC,

例如如下代码,会因为s = s + s不断在对空间中创建String对象导致内存溢出。

String s = "hello";
List list = new ArrayList();
while(true){
    list.add(s);
    s = s + s;
}

4.3 堆内存诊断

  1. jps 工具
    1. 查看当前系统中有哪些 java 进程
  2. jmap 工具
    1. 查看堆内存占用情况 jmap - heap 进程id
  3. jconsole 工具
    1. 图形界面的,多功能的监测工具,可以连续监测

举例使用:

public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

查看堆内存的使用:

查找进程

PS D:\code\jvm-test\src\main\java\chapter01> jps
12464 RemoteMavenServer36
14192
10164 Demo1_4
8628 Launcher
7548 Jps

第一次查看

PS D:\code\jvm-test\src\main\java\chapter01> jmap -heap 10164
Attaching to process ID 10164, please wait... 
Debugger attached successfully. 
Server compiler detected.
JVM version is 25.102-b14

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4261412864 (4064.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1420296192 (1354.5MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB) 

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 6660248 (6.351707458496094MB)
   free     = 59924328 (57.148292541503906MB)
   10.00268891101747% used
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 177733632 (169.5MB)
   used     = 0 (0.0MB)
   free     = 177733632 (169.5MB)
   0.0% used

3141 interned Strings occupying 257720 bytes.

我们关注Eden Space就是伊甸区,其中的used就是堆内存的大小。

第二次查看

PS D:\code\jvm-test\src\main\java\chapter01> jmap -heap 10164
Attaching to process ID 10164, please wait... 
Debugger attached successfully. 
Server compiler detected.             
JVM version is 25.102-b14             
                                      
using thread-local object allocation. 
Parallel GC with 8 thread(s)          

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4261412864 (4064.0MB) 
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1420296192 (1354.5MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 17146024 (16.351722717285156MB)
   free     = 49438552 (47.148277282714844MB)
   25.75074443666954% used
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 177733632 (169.5MB)
   used     = 0 (0.0MB)
   free     = 177733632 (169.5MB)
   0.0% used

3142 interned Strings occupying 257768 bytes.

从这里可以看到,堆内存扩大了10M(6.351707458496094MB --> 16.351722717285156MB),也就是我们创建了一个数组。

第三次查看

PS D:\code\jvm-test\src\main\java\chapter01> jmap -heap 10164
Attaching to process ID 10164, please wait... 
Debugger attached successfully. 
Server compiler detected.
JVM version is 25.102-b14

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4261412864 (4064.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1420296192 (1354.5MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB) 

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 1331712 (1.27001953125MB)
   free     = 65252864 (62.22998046875MB)
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 177733632 (169.5MB)
   used     = 979600 (0.9342193603515625MB)
   free     = 176754032 (168.56578063964844MB)
   0.5511618645141961% used

3128 interned Strings occupying 256776 bytes.

当我们不再y引用该数组时,且执行了GC,可以看到堆内存占用减少了(16.351722717285156MB --> 1.27001953125MB)。

下面使用一下jconsole:

PS D:\code\jvm-test\src\main\java\chapter01> jconsole

实时监控,界面如下

在这里插入图片描述

案例:垃圾回收后,内存占用仍然很高。

介绍一个更好用的工具:jvisualvm

在这里插入图片描述

由此我们可以分析这一时刻堆空间的占用情况

在这里插入图片描述

5.方法区

5.1定义

java8官方规范:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4

Java虚拟机拥有一个方法区,该区域在所有Java虚拟机线程之间共享。该方法区类似于传统语言的编译代码存储区域,或类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池字段方法数据以及方法构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法

方法区在虚拟机启动时创建。虽然方法区在逻辑上属于堆的一部分,但简单的实现可能选择不进行垃圾回收或压缩。本规范不要求方法区的位置或管理编译代码所使用的策略。方法区可能是固定大小的,也可能根据计算需要扩展,并且如果不再需要更大的方法区,则可以收缩。方法区的内存不需要是连续的。

Java虚拟机实现可以为程序员或用户提供对方法区初始大小的控制,以及在方法区大小可变的情况下,对最大和最小方法区大小的控制。

以下异常条件与方法区相关:

  • 如果方法区内存无法提供满足分配请求的空间,则Java虚拟机会抛出 OutOfMemoryError

2.5.4. Method Area

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

The following exceptional condition is associated with the method area:

  • If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

方法区在哪里

《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开

在这里插入图片描述

在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。

本质上,方法区和永久代并不等价。仅是对hotspot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit / IBM J9 中不存在永久代的概念。

现在来看,当年使用永久代,不是好的idea。导致Java程序更容易OOM(超过-XX:MaxPermsize上限)

在这里插入图片描述

而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存

永久代、元空间二者并不只是名字变了,内部结构也调整了,根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。

5.2 内部结构

在这里插入图片描述

存储什么?

它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
在这里插入图片描述

类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  1. 这个类型的完整有效名称(全名=包名.类名)
  2. 这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
  3. 这个类型的修饰符(public,abstract,final的某个子集)
  4. 这个类型直接接口的一个有序列表
域(Field)信息

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

方法(Method)信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  1. 方法名称

  2. 方法的返回类型(或void)

  3. 方法参数的数量和类型(按顺序)

  4. 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)

  5. 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)

  6. 异常表(abstract和native方法除外)

    每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

non-final的类变量
class Order {
    public static int count = 1;
    public static void hello() {
        System.out.println("hello!");
    }
}
  • 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分

  • 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它

补充说明:全局常量(static final)

被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。


5.3 方法区内存溢出

如果加载的类数量过多,就会导致方法区内存溢出,但元空间默认是采用的系统内存,一般很难溢出,除非手动设置内存变小。

  • 1.8 以前会导致永久代内存溢出

    java.lang.OutOfMemoryError: PermGen space

  • 1.8 之后会导致元空间内存溢出

    java.lang.OutOfMemoryError: Metaspace

为什么永久代要被元空间替代?
  • 为永久代设置空间大小是很难确定的

    在某些场景下,如果动态加载类过多,容易产生Perm区的oom。比如某个实际Web工 程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误,如:java.lang.OutOfMemoryError: PermGen space。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。

  • 对永久代进行调优是很困难的

    有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)。 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

方法区的主要垃圾回收两部分内容:

  • 常量池中废弃的常量
  • 不再使用的类型

5.4 运行时常量池

运行时常量池 VS 常量池

  • 方法区,内部包含了运行时常量池字节码文件,内部包含了常量池
  • 要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区。
  • 要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池。

在这里插入图片描述

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外,还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。

为什么需要常量池?

一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池,之前有介绍。

常量池中有什么?

击中常量池内存储的数据类型包括:

  • 数量值

  • 字符串值

  • 类引用

  • 字段引用

  • 方法引用

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

运行时常量池
  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。

  • 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

  • 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。

  • JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。

  • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址

  • 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性

  • 运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些。

  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常。

5.5 StringTable

StringTable 位置

在这里插入图片描述

String的基本特性
  • String:字符串,使用一对""引起来表示。

  • String声明为final的,不可被继承。

  • String实现了Serializable接口:表示字符串是支持序列化的。

  • String实现了Comparable接口:表示string可以比较大小。

  • String在jdk8及以前内部定义了final char[ ] value用于存储字符串数据。JDK9时改为byte[ ]

为什么改JDK9改了呢?String再也不用char[ ] 来存储了,改成了byte [ ] 加上编码标记,节约了一些空间

  • 常量池中的字符串仅是符号,第一次用到时才变为对象,就是所谓的懒加载
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。因此,调用 intern() 方法后,变量指向的对象可能会改变,取决于字符串池中是否已经有相同的字符串。
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回。
intern()的使用

前提:jdk8下;当调用intern方法时,如果池子里已经包含了一个与这个String对象相等的字符串,正如equals(Object)方法所确定的,那么池子里的字符串会被返回否则,这个String对象被添加到池中,并返回这个String对象的引用

看下面的案例:

        String s = new String("a") + new String("b"); //new String("ab")
        String s3 = s.intern();
        String s2 = "ab";

        System.out.println(s3 == s2); //true

在这里插入图片描述

由于intern时,串池中没有"ab",s对象会进入串池,所以s,s2,s3怎么比都是true。

        String s2 = "ab";
        String s = new String("a") + new String("b"); //new String("ab")
        String s3 = s.intern();

        System.out.println(s3 == s2);

在这里插入图片描述

StringTable 垃圾回收
public class StringGCTest {
    /**
     * -Xms15m -Xmx15m -XX:+PrintGCDetails
     */
    public static void main(String[] args) {
        
        for (int i = 0; i < 100000; i++) {
            String.valueOf(i).intern();
        }
    }
}
//***************************************************************************************
[GC (Allocation Failure) [PSYoungGen: 4096K->504K(4608K)] 4096K->1689K(15872K), 0.0581583 secs] [Times: user=0.00 sys=0.00, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 4600K->504K(4608K)] 5785K->2310K(15872K), 0.0015621 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4600K->504K(4608K)] 6406K->2350K(15872K), 0.0034849 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 4608K, used 1919K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 34% used [0x00000000ffb00000,0x00000000ffc61d30,0x00000000fff00000)
  from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e010,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 11264K, used 1846K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 11264K, 16% used [0x00000000ff000000,0x00000000ff1cd9b0,0x00000000ffb00000)
 Metaspace       used 3378K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 361K, capacity 388K, committed 512K, reserved 1048576K
StringTable 性能调优

StringTable底层采用的是hashTable,因此当有很多字符常量需要存入串池中时,桶的个数越多,元素就越分散,查找的时间就越短(空间换时间),否则就会链化+树化,降低效率。

  • 调整 -XX:StringTableSize=桶个数

在这里插入图片描述

  • 考虑将字符串对象是否入池

    例如:有大量重复的字符被加载,可以调用intern方法,从串池中获取,不再全部加载到堆当中。

方法区相关面试题

  • 说一下JVM内存模型吧,有哪些区?分别干什么的?
  • Java8的内存分代改进 JVM内存分哪几个区,每个区的作用是什么?
  • JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区?
  • Eden和survior的比例分配
  • jvm内存分区,为什么要有新生代和老年代
  • Java的内存分区
  • 讲讲vm运行时数据库区 什么时候对象会进入老年代?
  • JVM的内存结构,Eden和Survivor比例。
  • JVM内存为什么要分成新生代,老年代,持久代。
  • 新生代中为什么要分为Eden和survivor。
  • Jvm内存模型以及分区,需要详细到每个区放什么。
  • JVM的内存模型,Java8做了什么改
  • JVM内存分哪几个区,每个区的作用是什么?
  • java内存分配 jvm的永久代中会发生垃圾回收吗?
  • jvm内存分区,为什么要有新生代和老年代?

6.对象实例化及直接内存

6.1 对象实例化

美团:对象在JVM中是怎么存储的?对象头信息里面有哪些东西?

蚂蚁金服:Java对象头有什么?

在这里插入图片描述

1. 判断对象对应的类是否加载、链接、初始化

虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化(即判断类元信息是否存在)。

如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的 .class文件;

  • 如果没有找到文件,则抛出ClassNotFoundException异常

  • 如果找到,则进行类加载,并生成对应的Class对象

2. 为对象分配内存

首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小

如果内存规整:虚拟机将采用的是指针碰撞法(Bump The Point)来为对象分配内存。

  • 意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。

如果内存不规整:虚拟机需要维护一个空闲列表(Free List)来为对象分配内存。

  • 已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表来为对象分配内存。意思是虚拟机维护了一个列表,记录上那些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。

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

3. 处理并发问题
  • 采用CAS失败重试、区域加锁保证更新的原子性

  • 每个线程预先分配一块TLAB:通过设置 -XX:+UseTLAB参数来设定

4. 初始化分配到的内存

所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用

5. 设置对象的对象头

将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

6. 执行init方法进行初始化

在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

因此一般来说(由字节码中跟随invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。

给对象属性赋值的操作

  • 属性的默认初始化

  • 显式初始化

  • 代码块中初始化

  • 构造器中初始化

6.2 对象内存布局

在这里插入图片描述

对象头(Header)

对象头包含了两部分,分别是运行时元数据(Mark Word)和类型指针如果是数组,还需要记录数组的长度

运行时元数据

  • 哈希值(HashCode)

  • GC分代年龄

  • 锁状态标志

  • 线程持有的锁

  • 偏向线程ID

  • 偏向时间戳

类型指针

指向类元数据InstanceKlass,确定该对象所属的类型。

实例数据(Instance Data)

它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)

  • 相同宽度的字段总是被分配在一起

  • 父类中定义的变量会出现在子类之前

  • 如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙

在这里插入图片描述

6.3 对象的访问定位

在这里插入图片描述

JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?

在这里插入图片描述

句柄访问

在这里插入图片描述

reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只改变句柄中实例数据指针即可,reference本身不需要被修改。

直接指针(HotSpot采用)

在这里插入图片描述

直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。

6.4 直接内存(Direct Memory)

二、垃圾回收

2.1.什么样的对象会被GC?

引用计数法

引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性

缺点:

  • 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销

  • 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。

  • 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

在这里插入图片描述

Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。

可达性分析算法

  • 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。

  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)

  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。

  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象

在这里插入图片描述

MAT是Memory Analyzer的简称,它是一款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。

MAT是基于Eclipse开发的,是一款免费的性能分析工具。可以在 http://www.eclipse.org/mat/ 下载并使用MAT

java中的四种引用

在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)这4种引用强度依次逐渐减弱

  • 强引用

    最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

  • 软引用

    在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收,即内存不足即回收。如果这次回收后还没有足够的内存,才会抛出内存流出异常。

  • 弱引用

    被弱引用关联的对象只能生存到下一次垃圾收集之前,即发现即回收。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。

    但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象还是可以存在较长的时间

    面试题:你开发中使用过WeakHashMap吗?

    WeakHashMap对key保留了弱引用,用来存储图片信息,可以在内存不足的时候,及时回收,避免了OOM。

  • 虚引用

    一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

  • 终结器引用

    它用于实现对象的finalize() 方法,也可以称为终结器引用。无需手动编码,其内部配合引用队列使用。

    在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象调用它的finalize()方法,第二次GC时才回收被引用的对象

2.2.垃圾回收算法

标记清除

  • 阶段一:标记

    将需要回收的对象进行标记

在这里插入图片描述

  • 阶段二:清除

    将标记的对象GC

    在这里插入图片描述

优点:GC速度快;缺点:产生内存碎片(内存空间不连续),当一个“大”对象来的时候,无法分配内存。

标记整理

为了解决上述内存碎片的问题,就有了标记整理

  • 阶段一:标记

    在这里插入图片描述

  • 阶段二:整理

    在这里插入图片描述

优缺点与标记清除相反,既然不会产生内存碎片,那么我们在整理过程就会花费较长时间(因为需要移动对象)。

复制

把内存划分为两块:FROM、TO

在这里插入图片描述

将存活的迁移到另一块区域(TO),删除不要的对象(FROM),再交换一下内存区域(TO总是空闲空间)

在这里插入图片描述

优点:无内存碎片;缺点:占用双倍内存。

注意:JVM不会只采用单独某一种算法,而是混合实现!

2.3.分代回收

在这里插入图片描述

新生代和老年代

年轻代

特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁

其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做From区、To区)

几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。

老年代

特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁

配置新生代与老年代在堆结构的占比:

  • 默认-XX:NewRatio=2,表示新生代:老年代=1:2

  • 可以修改-XX:NewRatio=4,表示新生代:老年代=1:4

在HotSpot中,Eden空间和另外两个survivor空间默认所占的比例是8:1:1

回收过程:

  1. 伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区。

  2. 然后将伊甸园中的剩余对象移动到幸存者TO区,且幸存对象寿命+1(寿命信息保存在对象头当中),然后From和To区交换,使TO区空闲。

  3. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者TO区的,Eden区存活下来的也放到TO区,且寿命都+1,然后From和To区交换,使TO区空闲。

  4. 默认当对象寿命>=15时,就会晋升进入老年区。

    可以设置参数:进行设置-Xx:MaxTenuringThreshold= N

  5. 养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理 。

  6. 新生代和老年代都内存不足时,触发GC:Full GC,对整个堆进行清理,如果还内存不足,就直接OOM。

注意:每当触发GC时(无论哪种GC),都会sotp the world,即暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行,毕竟可能会涉及到对象的移动,所以垃圾回收线程不能与用户线程共同执行;Full GC相比于Minor GC的STW会时间会久一些,也很好理解。Full GC 是开发或调优中尽量要避免的,这样暂时时间会短一些

相关VM参数

在这里插入图片描述

补充一些GC细节问题:

  • 如果来了一个“大”对象,新生代放不下了,那么直接晋升到老年代。
  • 新生代的内存不断销毁,GC后还是占满了,没办法只有直接晋升到老年区。

2.4.垃圾回收器

按线程数分,可以分为串行垃圾回收器并行垃圾回收器

串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作(单线程),此时工作线程被暂停,直至垃圾收集工作结束,堆内存较小,适合个人电脑

并行收集可以运用多个CPU同时执行垃圾回收(多线程),因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“Stop-the-World”机制。

  • 吞吐量优先

    吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)。比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

    这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的

    吞吐量优先,意味着在单位时间内,STW的时间最短:0.2 + 0.2 = 0.4s

  • 响应时间优先

    响应时间优先,意味着尽可能让单次STW的时间最短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5s

现在标准:在最大吞吐量优先的情况下,降低停顿时间

串行-Serial GC

-XX:+UseSerialGC = Serial + SerialOld

Serial:工作在新生代,采用复制算法
SerialOld:工作在老年代,采用标记整理算法

在这里插入图片描述

吞吐量优先-Parallel GC

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n

-XX:+UseAdaptivesizePolicy 设置Parallel Scavenge收集器具有自适应调节策略

  • 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。

  • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆目标的吞吐量GCTimeRatio)和停顿时间MaxGCPauseMills),让虚拟机自己完成调优工作。

-XX:GCTimeRatio 垃圾收集时间占总时间的比例=1/(N+1)。用于衡量吞吐量的大小。

  • 取值范围(0, 100)。默认值99,也就是垃圾回收时间不超过1%。

  • -XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。

-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间),单位是毫秒。

  • 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数

  • 对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。

  • 该参数使用需谨慎

-XX:ParallelGCThreads 设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。

在这里插入图片描述

响应时间优先-Concurrent Mark Sweep GC

在JDK1.5时期,Hotspot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作

CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-World"

在G1出现之前,CMS使用还是非常广泛的,一直到今天,仍然有很多系统使用CMS GC。

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
  • -XX:+UseConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务。
    开启该参数后会自动将-xx:+UseParNewGC打开。即:ParNew(Young区用)+CMS(Old区用)+ Serial Old的组合。

  • -XX:CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收

    • JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
    • 如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低FullGC的执行次数。
  • -XX:+UseCMSCompactAtFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。

  • -XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理。

  • -XX:ParallelcMSThreads 设置CMS的线程数量。

    • CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。

在这里插入图片描述

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段并发标记阶段重新标记阶段并发清除阶段

  • 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。

  • 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

  • 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

    对象引用发生变动的会添加写屏障pre-write barrier,然后添加到一个satb_mark_queue队列中,STW时,从队列中取出对对象再次标记。

  • 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

在其初始化标记再次标记这两个阶段中**仍然需要执行“Stop-the-World”**机制暂停程序中的工作线程,所以并没有一个垃圾收集器不用STW。

另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

CMS的弊端

  • 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发FullGC。

  • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。

  • CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。

注:JDK9新特性:CMS被标记为Deprecate了(JEP291)

  • 如果对JDK9及以上版本的HotSpot虚拟机使用参数-XX: +UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。

JDK14新特性:删除CMS垃圾回收器(JEP363)

  • 移除了CMS垃圾收集器,如果在JDK14中使用 -XX:+UseConcMarkSweepGC的话,JVM不会报错,只是给出一个warning信息,但是不会exit。JVM会自动回退以默认GC方式启动JVM

对比三类GC

HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?

请记住以下口令:

  • 如果你想要最小化地使用内存和并行开销,请选Serial GC;

  • 如果你想要最大化应用程序的吞吐量,请选Parallel GC;

  • 如果你想要最小化GC的中断或停顿时间,请选CMS GC。

Garbage First(G1)

官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。

主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。G1有计划地避免在整个Java堆中进行全区域的垃圾收集。G1将内存划分为多个大小相同的Region,跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这个价值大其实就是释放后可以空出更多内存)。

JDK9以后的默认采用G1垃圾回收器。在jdk8中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC来启用。

参数设置

  • -XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务

  • -XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。

  • -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms(人的平均反应速度)

  • -XX:+ParallelGCThread 设置STW工作线程数的值。最多设置为8(上面说过Parallel回收器的线程计算公式,当CPU_Count > 8时,ParallelGCThreads 也会大于8)

  • -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。

  • -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。

G1 垃圾回收阶段

G1GC的垃圾回收过程主要包括如下三个环节:

  • 年轻代GC(Young GC)

  • 老年代并发标记过程(Concurrent Marking)

  • 混合回收(Mixed GC)(如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)

在这里插入图片描述

顺时针,Young GC -> Young GC + Concurrent mark -> Mixed GC顺序,进行垃圾回收。

  1. 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
  2. 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程,不会STW。
  3. 标记完成马上开始混合回收过程。对于一个混合回收期(最终标记+拷贝存活)都会STW,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。

Full GC

  • SerialGC、ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • G1、CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
      • 回收速度 > 垃圾产生速度 = 不会采用Full GC
      • 回收速度 < 垃圾产生速度 = 采用Full GC

Remembered Set

  • 一个对象被不同区域引用的问题,一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?

  • 在其他的分代收集器,也存在这样的问题(而G1更突出)回收新生代也不得不同时扫描老年代? 这样的话会降低MinorGC的效率;

无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描

  • 每个Region都有一个对应的Remembered Set
  • 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
  • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象),这些引用通常称为"跨代指针"。在进行老年代的垃圾回收时G1只需要扫描Remembered Set中的指针,而不需要对整个堆进行全局扫描,从而减少了回收的时间和成本。
  • 如果不同,通过CardTable(卡表)把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中,当一个跨代指针被记录在Remembered Set中时,G1会将该指针所在的卡标记为"脏卡"。在进行垃圾回收时,G1只需要扫描所有脏卡中的指针,而不需要扫描整个Remembered Set,从而进一步减少了回收的时间和成本。
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。

G1回收器优化建议

年轻代大小

  • 避免使用-Xmn-XX:NewRatio等相关选项显式设置年轻代大小

  • 固定年轻代的大小会覆盖暂停时间目标

暂停时间目标不要太过严苛

  • G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间

  • 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。

垃圾回收器总结

在这里插入图片描述

怎么选择垃圾收集器?官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

  1. 没有最好的收集器,更没有万能的收集
  2. 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

面试问题

  • 垃圾收集的算法有哪些?如何判断一个对象是否可以回收?

  • 垃圾收集器工作的基本流程。

参考博客:https://www.yuque.com/u21195183/jvm/qpoa81

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值