前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM 面试深入理解内存模型和垃圾回收(二)

JVM 面试深入理解内存模型和垃圾回收(二)

作者头像
架构探险之道
发布2023-03-04 10:53:25
4470
发布2023-03-04 10:53:25
举报
文章被收录于专栏:架构探险之道

JVM 面试深入理解内存模型和垃圾回收(二)

1. 运行时数据区域

这里写图片描述

Run-Time Data Areas:The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

Java 虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在 Java 虚拟机启动时创建的,只有在 Java 虚拟机退出时才会销毁。其他数据区域为每个线程独自使用的,每个线程的数据区域是在创建线程时创建的,并在线程退出时销毁。

1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存

1.1 The PC Register

程序计数器:The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6) for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. The Java Virtual Machine's pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.

Java 虚拟机可以同时支持多个执行线程(jls17)。每个 Java 虚拟机线程都有自己的 pc (程序计数器)寄存器。在任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法(2.6)。如果这个方法不是本机的,那么 pc 寄存器就会包含当前正在执行的 Java 虚拟指令的地址。如果当前由线程执行的方法是本地方法,那么 Java 虚拟机的 pc 寄存器的值是未定义的(null)。Java 虚拟机的 pc 寄存器足够宽,可以在特定平台上保存 returnAddress 或本机指针。

简而言之,每个线程独自占用一个寄存器,指向当前线程正在执行的字节码代码的行号。如果当前线程执行的是native方法,则其值为null。

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据 CPU 调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置,也就是 PC 程序计数器的作用。

1.2 Java Virtual Machine Stacks

虚拟机栈: Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous. 每个 Java 虚拟机线程都有一个专用的 Java 虚拟机堆栈,与线程同时创建。Java 虚拟机堆栈存储帧(2.6)。Java 虚拟机堆栈类似于传统语言(如 c)的堆栈: 它保存局部变量和部分结果,并在方法调用和返回中发挥作用。因为 Java 虚拟机栈从来不会被直接操作,除了推送和弹出帧,帧可能会被分配到堆中。Java 虚拟机堆栈的内存不需要是连续的。 In the First Edition of The Java® Virtual Machine Specification, the Java Virtual Machine stack was known as the Java stack. 在 Java 虚拟机规范的第一版中,Java 虚拟机堆栈被称为 Java 堆栈。 This specification permits Java Virtual Machine stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the Java Virtual Machine stacks are of a fixed size, the size of each Java Virtual Machine stack may be chosen independently when that stack is created. 该规范允许 Java 虚拟机堆栈具有固定的大小,或者根据计算的需要动态扩展和收缩。如果 Java 虚拟机堆栈的大小是固定的,那么在创建该堆栈时可以独立地选择每个 Java 虚拟机堆栈的大小。 A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of Java Virtual Machine stacks, as well as, in the case of dynamically expanding or contracting Java Virtual Machine stacks, control over the maximum and minimum sizes. 一个 Java 虚拟机实现可以为程序员或用户控制 Java 虚拟机堆栈初始大小,以及,在动态扩展或收缩 Java 虚拟机堆栈的情况下,控制最大和最小值。 The following exceptional conditions are associated with Java Virtual Machine stacks: 下列异常情况与 Java 虚拟机堆栈相关:

  • If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError. 如果线程中的计算需要比允许的更大的 Java 虚拟机堆栈,则 Java 虚拟机抛出 StackOverflowError。
  • If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError. 如果 Java 虚拟机堆栈可以动态扩展,并且尝试扩展,但是没有足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始的 Java 虚拟机堆栈,Java 虚拟机抛出 OutOfMemoryError。

stack

线程私有,每个线程对应一个Java虚拟机栈,其生命周期与线程同进同退。每个Java方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。

图解栈和栈帧

代码语言:javascript
复制
void a(){b();}
void b(){c(); }
void c(){ }

image-20201011011202515

1.2.1 Frames

帧 A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions. 帧用于存储数据和部分结果,以及执行动态链接、方法返回值和分派异常。 A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes, whether that completion is normal or abrupt (it throws an uncaught exception). Frames are allocated from the Java Virtual Machine stack (§2.5.2) of the thread creating the frame. Each frame has its own array of local variables (§2.6.1), its own operand stack (§2.6.2), and a reference to the run-time constant pool (§2.5.5) of the class of the current method. 每次调用方法时都会创建一个新的帧。当方法调用完成时,帧将被销毁,不管该完成是正常的还是突然的(它将引发未捕获的异常)。帧是从创建帧的线程的 Java 虚拟机栈(2.5.2)中分配的。每个帧都有自己的局部变量数组(2.6.1)、自己的操作数堆栈(2.6.2) ,以及对当前方法类的运行时常量池(2.5.5)的引用。 A frame may be extended with additional implementation-specific information, such as debugging information. 可以使用额外的特定于实现的信息(如调试信息)扩展帧。 The sizes of the local variable array and the operand stack are determined at compile-time and are supplied along with the code for the method associated with the frame (§4.7.3). Thus the size of the frame data structure depends only on the implementation of the Java Virtual Machine, and the memory for these structures can be allocated simultaneously on method invocation. 局部变量数组和操作数堆栈的大小在编译时确定,并与与帧(4.7.3)关联的方法的代码一起提供。因此,帧数据结构的大小仅取决于 Java 虚拟机的实现,并且这些结构的内存可以在方法调用时同时分配。 Only one frame, the frame for the executing method, is active at any point in a given thread of control. This frame is referred to as the current frame, and its method is known as the current method. The class in which the current method is defined is the current class. Operations on local variables and the operand stack are typically with reference to the current frame. 在给定的控制线程中,只有一个帧(执行方法的帧)处于活动状态。这个帧称为当前帧,它的方法称为当前方法。定义当前方法的类是当前类。对局部变量和操作数堆栈的操作通常与当前帧有关。 A frame ceases to be current if its method invokes another method or if its method completes. When a method is invoked, a new frame is created and becomes current when control transfers to the new method. On method return, the current frame passes back the result of its method invocation, if any, to the previous frame. The current frame is then discarded as the previous frame becomes the current one. 如果一个帧的方法调用了另一个方法或者它的方法完成了,那么这个帧就不再是当前的。当方法被调用时,将创建一个新的帧,并在控制转移到新方法时成为当前帧。在方法返回时,当前帧将其方法调用的结果(如果有的话)返回给前一个帧。然后,当前帧变成当前帧时,当前帧将被丢弃。 Note that a frame created by a thread is local to that thread and cannot be referenced by any other thread. 请注意,由线程创建的帧是该线程的本地帧,不能由任何其他线程引用。

  • 栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
    • 局部变量表: 方法中定义的局部变量以及方法的参数存放在这张表中 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
    • 操作数栈: 以压栈和出栈的方式存储操作数的
    • 动态链接: 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
    • 方法返回地址: 当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇 见异常,并且这个异常没有在方法体内得到处理。
1.2.2 结合字节码指令理解栈帧

javap -c Person.class > Person.txt

《JVM 面试基础准备篇(一)》我们输出过一个简单类的字节码文件:

代码语言:javascript
复制
public static int calc(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=2
         0: iconst_3
         1: istore_0
         2: iload_0
         3: iload_1
         4: iadd
         5: istore_2
         6: new           #10                 // class java/lang/Object
         9: dup
        10: invokespecial #1                  // Method java/lang/Object."<init>":()V
        13: astore_3
        14: iload_2
        15: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0   op1   I
            0      16     1   op2   I
            6      10     2 result   I
           14       2     3   obj   Ljava/lang/Object;

简单分析下执行流程:

代码语言:javascript
复制
0: iconst_3   //将int类型常量3压入[操作数栈]
1: istore_0   //将int类型值存入[局部变量0]
2: iload_0    //从[局部变量0]中装载int类型值入栈
3: iload_1    //从[局部变量1]中装载int类型值入栈
4: iadd     //将栈顶元素弹出栈,执行int类型的加法,结果入栈
5: istore_2   //将栈顶int类型值保存到[局部变量2]中
6: iload_2   //从[局部变量2]中装载int类型值入栈
7: ireturn  //从方法中返回int类型的数据

[字节码行号]:[字节码指令]_下标(从0开始)

代码语言:javascript
复制
On class method invocation, any parameters are passed in consecutive local
variables starting from local variable 0. On instance method invocation, local
variable 0 is always used to pass a reference to the object on which the
instance method is being invoked (this in the Java programming language). Any
parameters are subsequently passed in consecutive local variables starting from
local variable 1.
在类方法调用时,任何参数都从局部变量0开始以连续的局部变量传递。对于实例方法调用,总是使用局部变量0来传递对实例方法被调用的对象的引用(在 Java 编程语言中是这样的)。任何参数随后从局部变量1开始在连续的局部变量中传递。

1.3 Heap

堆: The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated. Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的堆。堆是运行时数据区域,从中分配所有类实例和数组的内存。 The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous. 堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收; 对象永远不会显式释放。Java 虚拟机没有特定类型的自动存储管理系统,可以根据实现者的系统需求选择存储管理技术。堆的大小可以是固定的,也可以根据计算的需要进行扩展,如果不需要更大的堆,还可以进行收缩。堆的内存不需要是连续的。 A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size. 一个 Java 虚拟机实现可以提供程序员或用户对堆的初始大小的控制,以及,如果堆可以动态扩展或收缩,对堆的最大和最小大小的控制。 The following exceptional condition is associated with the heap: 下列异常情况与堆相关联:

  • If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an OutOfMemoryError. 如果计算需要比自动存储管理系统所能提供的更多的堆,那么 Java 虚拟机将抛出 OutOfMemoryError 错误。

heap

堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:几乎所有的对象实例及数组都在对上进行分配。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。堆有自己进一步的内存分块划分,按照GC分代收集角度的划分请参见上图。

1.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. Java 虚拟机有一个方法区域,该区域在所有 Java 虚拟机线程之间共享。方法区域类似于常规语言或操作系统进程中的“文本”段的编译代码的存储区域。它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化和接口初始化的特殊方法(2.9)。 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. Java 虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在变大小方法区域的情况下对最大和最小方法区域大小的控制。 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 虚拟机抛出 OutOfMemoryError。

值得说明的:JVM方法区是一种规范,真正的实现:在 JDK 8 中就是 Metaspace,在 JDK6 或 7 中就是Perm Space。

1.5 Run-Time Constant Pool

运行时常量池: A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file (§4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time. The run-time constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table. 运行时常量池是类文件(4.4)中常量 _ 池表的每类或每接口运行时表示形式。它包含几种常量,从编译时已知的数值文本到必须在运行时解析的方法和字段引用。运行时常量池提供的功能类似于传统编程语言的符号表,尽管它包含的数据范围比典型的符号表更广。 Each run-time constant pool is allocated from the Java Virtual Machine's method area (§2.5.4). The run-time constant pool for a class or interface is constructed when the class or interface is created (§5.3) by the Java Virtual Machine. 每个运行时常量池都是从 Java 虚拟机的方法区域(2.5.4)中分配的。类或接口的运行时常量池是在 Java 虚拟机创建类或接口时构造的(5.3)。 The following exceptional condition is associated with the construction of the run-time constant pool for a class or interface: 下面的异常条件与类或接口的运行时常量池的构造相关: When creating a class or interface, if the construction of the run-time constant pool requires more memory than can be made available in the method area of the Java Virtual Machine, the Java Virtual Machine throws an OutOfMemoryError. 在创建类或接口时,如果运行时常量池的构造需要比 Java 虚拟机的方法区域可用的内存更多,则 Java 虚拟机抛出 OutOfMemoryError。 See §5 (Loading, Linking, and Initializing) for information about the construction of the run-time constant pool. 有关构造运行时常量池的信息,请参见5(加载、链接和初始化)。

1.6 Native Method Stacks

本地方法栈 An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine's instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created. Java 虚拟机的实现可以使用传统的栈(通常称为“ c 栈”)来支持本机方法(用 Java 编程语言以外的语言编写的方法)。本机方法堆栈也可以用于 Java 虚拟机指令集的解释器的实现,如 c 语言的 Java 虚拟机实现,不能加载本机方法,也不依赖于传统的堆栈,不需要提供本机方法堆栈。如果提供,通常在创建每个线程时为每个线程分配本机方法堆栈。 This specification permits native method stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the native method stacks are of a fixed size, the size of each native method stack may be chosen independently when that stack is created. 该规范允许本机方法堆栈具有固定的大小,或者根据计算的需要动态扩展和收缩。如果本机方法堆栈的大小固定,则在创建该堆栈时可以独立选择每个本机方法堆栈的大小。 A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the native method stacks, as well as, in the case of varying-size native method stacks, control over the maximum and minimum method stack sizes. Java 虚拟机实现可以为程序员或用户提供对本机方法堆栈初始大小的控制,以及对于不同大小的本机方法堆栈,对最大和最小方法堆栈大小的控制。 The following exceptional conditions are associated with native method stacks: 下面的异常情况与本机方法堆栈相关联:

  • If the computation in a thread requires a larger native method stack than is permitted, the Java Virtual Machine throws a StackOverflowError. 如果线程中的计算需要比允许的更大的本机方法堆栈,那么 Java 虚拟机抛出一个 StackOverflowError。
  • If native method stacks can be dynamically expanded and native method stack expansion is attempted but insufficient memory can be made available, or if insufficient memory can be made available to create the initial native method stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError. 如果本机方法堆栈可以动态扩展,本机方法堆栈扩展可以尝试,但是没有足够的内存可用,或者如果没有足够的内存可用来为新线程创建初始的本机方法堆栈,Java 虚拟机将抛出 OutOfMemoryError。

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。那如果在Java方法执行的时候调用native的方法呢?

2. 内存模型

2.1 引用

方法区指向堆

方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。

代码语言:javascript
复制
private static Object obj = new Object();

堆指向方法区

注意,方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?

2.2 Java对象内存模型

一个对象怎么知道它是由哪个类创建出来的?怎么记录?这就需要了解一个Java对象的具体信息。一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充。

上面对运行时数据区描述了很多,其实重点存储数据的是堆和方法区(非堆),所以内存的设计也着重从这两方面展开(注意这两块区域都是线程共享的)。对于虚拟机栈,本地方法栈,程序计数器都是线程私有的。可以这样理解,JVM运行时数据区是一种规范,而JVM内存模式是对该规范的实现。

image-20201009231453878

2.2.1 对象创建的过程

一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor 区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的。

2.2.2 常见问题
  • 如何理解Minor/Major/FullGC
代码语言:javascript
复制
MinorGC:新生代
MajorGC:老年代
FullGC:新生代+老年代
  • 为什么需要 Survivor 区?只有 Eden 不行吗?
代码语言:javascript
复制
如果没有Survivor,Eden区每进行一次MinorGC,存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发MajorGC(因为MajorGC一般伴随着MinorGC,也可以看做触发了FullGC)。
老年代的内存空间远大于新生代,进行一次FullGC消耗的时间比MinorGC长得多。
执行时间长有什么坏处?频发的FullGC消耗的时间很长,会影响大型程序的执行和响应速度。

可能你会说,那就对老年代的空间进行增加或者较少咯。
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低FullGC频率,但是随着老年代空间加大,一旦发生FullGC,执行所需要的时间更长。
假如减少老年代空间,虽然FullGC所需时间减少,但是老年代很快被存活对象填满,FullGC频率增加。

所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少FullGC的发生,Survivor的预筛选保证,只有经历16次MinorGC还能在新生代中存活的对象,才会被送到老年代。
  • 为什么需要两个 Survivor 区?
代码语言:javascript
复制
最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置
Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次MinorGC,Eden中的存活对象就会被移动到Survivor 区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行MinorGC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
永远有一个Survivorspace是空的,另一个非空的Survivorspace无碎片。
  • 新生代中Eden:S1:S2为什么是8:1:1?
代码语言:javascript
复制
新生代中的可用内存:复制算法用来担保的内存为9:1 
可用内存中Eden:S1区为8:1 
即新生代中Eden:S1:S2=8:1:1 
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”的
  • 堆内存中都是线程共享的区域吗?
代码语言:javascript
复制
JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread LocalAllocationBuffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
2.2.3 参数设置
  • Heap 堆内存大小设置:-Xmx20M -Xms20M
  • Metaspace 元空间大小设置:-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M
  • Stack Space 每个线程的堆栈大小: -Xss128k:设置每个线程的堆栈大小。
    • JDK5 以后每个线程堆栈大小为1M,以前每个线程堆栈大小为 256K。
    • 根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000左右。
    • 线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。
    • Stack Space用来做方法的递归调用时压入 Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出 StackOverflow 的错误。

3. 垃圾回收

之前说堆内存中有垃圾回收,比如Young区的MinorGC,Old区的MajorGC,Young区和Old区的FullGC。但是对于一个对象而言,怎么确定它是垃圾?是否需要被回收?怎样对它进行回收?等等这些问题我们还需要详细探索。因为Java是自动做内存管理和垃圾回收的,如果不了解垃圾回收的各方面知识,一旦出现问题我们很难进行排查和解决,自动垃圾回收机制就是寻找Java堆中的对象,并对对象进行分类判别,寻找出正在使用的对象和已经不会使用的对象,然后把那些不会使用的对象从堆上清除。

3.1 如何确定一个对象是垃圾?

要想进行垃圾回收,得先知道什么样的对象是垃圾。

3.1.1 引用计数法

对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其 引用,它就是垃圾。弊端: 如果AB相互持有引用,导致永远不能被回收。

3.1.2 可达性分析

通过GCRoot的对象,开始向下寻找,看某个对象是否可达。

image-20201009233200905

能作为GCRoot的有: 类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。

代码语言:javascript
复制
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。

3.2 什么时候会垃圾回收?

GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。但是不建议手动调用该方法,因为GC消耗的资源比较大。

代码语言:javascript
复制
(1)当Eden区或者S区不够用了
(2)老年代空间不够用了
(3)方法区空间不够用了
(4)System.gc()

3.3 垃圾收集算法

已经能够确定一个对象为垃圾之后,接下来要考虑的就是回收,怎么回收呢?得要有对应的算法,下面介绍常见的垃圾回收算法。

3.3.1 标记-清除(Mark-Sweep)
  • 标记

找出内存中需要回收的对象,并且把它们标记出来

此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时

image-20201009233748188

  • 清除

清除掉被标记需要回收的对象,释放出对应的内存空间

image-20201009233842912

弊端:

代码语言:javascript
复制
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程
序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.3.2 标记-复制(Mark-Copying)

将内存划分为两块相等的区域,每次只使用其中一块,如下图所示:

image-20201009233954400

当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。

image-20201009234022894

弊端:

代码语言:javascript
复制
缺点:空间利用率降低。
3.3.3 标记-整理(Mark-Compact)

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。

标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

其实上述过程相对"复制算法"来讲,少了一个"保留区"

image-20201010084600618

让所有存活的对象都向一端移动,清理掉边界意外的内存。

image-20201010084651076

3.4 分代收集算法

既然上面介绍了3中垃圾收集算法,那么在堆内存中到底用哪一个呢?

  • Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
  • Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

3.5 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

3.5.1 Serial

Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。 它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。

  • 优点:简单高效,拥有很高的单线程收集效率缺点:收集过程需要暂停所有线程
  • 算法:复制算法
  • 适用范围:新生代
  • 应用:Client模式下的默认新生代收集器

image-20201010085146815

3.5.2 SerialOld

SerialOld收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算法",运行过程和Serial收集器一样。

image-20201010085247022

3.5.3 ParNew

可以把这个收集器理解为Serial收集器的多线程版本。

  • 优点:在多CPU时,比Serial效率高。
  • 缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
  • 算法:复制算法
  • 适用范围:新生代
  • 应用:运行在Server模式下的虚拟机中首选的新生代收集器

image-20201010085417195

3.5.4 Parallel Scavenge

发音:['perə.lel] ['skævəndʒ]

Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是 Parallel Scanvenge 更关注系统的吞吐量。

吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。

-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,-XX:GCRatio直接设置吞吐量的大小。

3.5.5 ParallelOld

ParallelOld收集器是ParallelScavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收,也是更加关注系统的吞吐量。

3.5.6 CMS

CMS(ConcurrentMarkSweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的是"标记-清除算法",整个过程分为4步

  • 初始标记CMS initial mark 标记GCRoots直接关联对象,不用Tracing,速度很快
  • 并发标记CMS concurrent mark 进行GCRootsTracing
  • 重新标记CMS remark 修改并发标记因用户程序变动的内容
  • 并发清除CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾

由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

image-20201010090421565

  • 优点:并发收集、低停顿
  • 缺点:产生大量空间碎片、并发阶段会降低吞吐量
3.5.7 G1(Garbage-First)

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别。

  • 它将整个Java堆划分为多个大小相等的独立区域(Region)。
  • 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
  • 每个 Region 大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂如果对象太大,一个Region 放不下[超过Region大小的50%],那么就会直接放到 H 中
  • 设置 Region 大小:-XX:G1HeapRegionSize=M
  • 所谓 Garbage-Frist,其实就是优先回收垃圾最多的 Region 区域
  • 分代收集(仍然保留了分代的概念)
  • 空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
  • 可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)

工作过程可以分为如下几步:

  • 初始标记(Initial Marking):标记 GC Roots 能够关联的对象,并且修改 TAMS 的值,需要暂停用户线程
  • 并发标记(Concurrent Marking):从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
  • 最终标记(Final Marking):修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程**
  • 筛选回收(Live Data Counting and Evacuation):对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间制定回收计划

1602318174773

3.5.8 ZGC

JDK11 引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了。会分为一个个 page,当进行 GC 操作时会对 page 进行压缩,因此没有碎片问题只能在64位的linux上使用,目前用得还比较少。

  • 可以达到10ms以内的停顿时间要求
  • 支持TB级别的内存
  • 堆内存变大后停顿时间还是在10ms以内

1602322566132

3.6 垃圾收集器分类

3.6.1 串行收集器

Serial 和 SerialOld

只能有一个垃圾回收线程执行,用户线程暂停。适用于内存比较小的嵌入式设备。

3.6.2 并行收集器[吞吐量优先]

Parallel Scanvenge、ParallelOld

多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适用于科学计算、后台处理等若交互场景。

3.6.3 并发收集器[停顿时间优先]

CMS、G1

用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。适用于相对时间有要求的场景,比如 Web。

3.7 常见问题

3.7.1 吞吐量和停顿时间
  • 停顿时间:垃圾收集器进行垃圾回收终端应用执行响应的时间
  • 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

小结:这两个指标也是评价垃圾回收器好处的标准。

3.7.2 如何选择合适的垃圾收集器
  • 优先调整堆的大小让服务器自己来选择
  • 如果内存小于100M,使用串行收集器
  • 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
  • 如果允许停顿时间超过1秒,选择并行或JVM自己选
  • 如果响应时间最重要,并且不能超过1秒,使用并发收集器
3.7.3 G1收集
  • JDK7开始使用,JDK8非常成熟,JDK9默认的垃圾收集器,适用于新老生代。
  • 是否使用G1收集器?
    • 50%以上的堆被存活对象占用
    • 对象分配和晋升的速度变化非常大
    • 垃圾回收时间比较长
  • G1中的RSet:全称RememberedSet,记录维护Region中对象的引用关系
    • 试想,在G1垃圾收集器进行新生代的垃圾收集时,也就是 MinorGC,假如该对象被老年代的 Region 中所引用,这时候新生代的该对象就不能被回收,怎么记录呢?
    • 不妨这样,用一个类似于 hash 的结构,key 记录 region 的地址,value 表示引用该对象的集合,这样就能知道该对象被哪些老年代的对象所引用,从而不能回收。
3.7.4 如何开启需要的垃圾收集器
  • 串行 -XX:+UseSerialGC -XX:+UseSerialOldGC
  • 并行(吞吐量优先) -XX:+UseParallelGC -XX:+UseParallelOldGC
  • 并发收集器(响应时间优先) -XX:+UseConcMarkSweepGC -XX:+UseG1GC

小结

本文主要介绍了 JVM 的运行时数据区域的组成、内存模型、常见垃圾回收算法以及垃圾收集器的选择。

REFERENCES

  • JDK1.8 JVM运行时数据区域划分
  • JDK 官网
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构探险之道 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JVM 面试深入理解内存模型和垃圾回收(二)
    • 1.1 The PC Register
      • 1.2 Java Virtual Machine Stacks
        • 1.3 Heap
          • 1.4 Method Area
            • 1.5 Run-Time Constant Pool
              • 1.6 Native Method Stacks
                • 2. 内存模型
                  • 2.1 引用
                  • 2.2 Java对象内存模型
                • 3. 垃圾回收
                  • 3.1 如何确定一个对象是垃圾?
                  • 3.2 什么时候会垃圾回收?
                  • 3.3 垃圾收集算法
                  • 3.4 分代收集算法
                  • 3.5 垃圾收集器
                  • 3.6 垃圾收集器分类
                  • 3.7 常见问题
                • 小结
                  • REFERENCES
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档