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规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
程序计数器: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 notnative
, thepc
register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread isnative
, the value of the Java Virtual Machine'spc
register is undefined. The Java Virtual Machine'spc
register is wide enough to hold areturnAddress
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 程序计数器的作用。
虚拟机栈: 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 虚拟机堆栈相关:
StackOverflowError
.
如果线程中的计算需要比允许的更大的 Java 虚拟机堆栈,则 Java 虚拟机抛出 StackOverflowError。OutOfMemoryError
.
如果 Java 虚拟机堆栈可以动态扩展,并且尝试扩展,但是没有足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始的 Java 虚拟机堆栈,Java 虚拟机抛出 OutOfMemoryError。stack
线程私有,每个线程对应一个Java虚拟机栈,其生命周期与线程同进同退。每个Java方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。
图解栈和栈帧
void a(){b();}
void b(){c(); }
void c(){ }
image-20201011011202515
帧 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. 请注意,由线程创建的帧是该线程的本地帧,不能由任何其他线程引用。
javap -c Person.class > Person.txt
在《JVM 面试基础准备篇(一)》
我们输出过一个简单类的字节码文件:
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;
简单分析下执行流程:
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开始)
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开始在连续的局部变量中传递。
堆: 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: 下列异常情况与堆相关联:
OutOfMemoryError
.
如果计算需要比自动存储管理系统所能提供的更多的堆,那么 Java 虚拟机将抛出 OutOfMemoryError 错误。heap
堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:几乎所有的对象实例及数组都在对上进行分配。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。堆有自己进一步的内存分块划分,按照GC分代收集角度的划分请参见上图。
方法区: 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: 下列异常情况与方法区域相关联:
OutOfMemoryError
.
如果方法区域中的内存不能用于满足分配请求,则 Java 虚拟机抛出 OutOfMemoryError。值得说明的:JVM方法区是一种规范,真正的实现:
在 JDK 8 中就是 Metaspace,在 JDK6 或 7 中就是Perm Space。
运行时常量池: A run-time constant pool is a per-class or per-interface run-time representation of the
constant_pool
table in aclass
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 anOutOfMemoryError
. 在创建类或接口时,如果运行时常量池的构造需要比 Java 虚拟机的方法区域可用的内存更多,则 Java 虚拟机抛出 OutOfMemoryError。 See §5 (Loading, Linking, and Initializing) for information about the construction of the run-time constant pool. 有关构造运行时常量池的信息,请参见5(加载、链接和初始化)。
本地方法栈 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 loadnative
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: 下面的异常情况与本机方法堆栈相关联:
StackOverflowError
.
如果线程中的计算需要比允许的更大的本机方法堆栈,那么 Java 虚拟机抛出一个 StackOverflowError。OutOfMemoryError
.
如果本机方法堆栈可以动态扩展,本机方法堆栈扩展可以尝试,但是没有足够的内存可用,或者如果没有足够的内存可用来为新线程创建初始的本机方法堆栈,Java 虚拟机将抛出 OutOfMemoryError。如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。那如果在Java方法执行的时候调用native的方法呢?
方法区指向堆
方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。
private static Object obj = new Object();
堆指向方法区
注意,方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?
一个对象怎么知道它是由哪个类创建出来的?怎么记录?这就需要了解一个Java对象的具体信息。一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充。
上面对运行时数据区描述了很多,其实重点存储数据的是堆和方法区(非堆),所以内存的设计也着重从这两方面展开(注意这两块区域都是线程共享的)。对于虚拟机栈,本地方法栈,程序计数器都是线程私有的。可以这样理解,JVM运行时数据区是一种规范,而JVM内存模式是对该规范的实现。
image-20201009231453878
一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。
我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor 区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的。
MinorGC:新生代
MajorGC:老年代
FullGC:新生代+老年代
如果没有Survivor,Eden区每进行一次MinorGC,存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发MajorGC(因为MajorGC一般伴随着MinorGC,也可以看做触发了FullGC)。
老年代的内存空间远大于新生代,进行一次FullGC消耗的时间比MinorGC长得多。
执行时间长有什么坏处?频发的FullGC消耗的时间很长,会影响大型程序的执行和响应速度。
可能你会说,那就对老年代的空间进行增加或者较少咯。
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低FullGC频率,但是随着老年代空间加大,一旦发生FullGC,执行所需要的时间更长。
假如减少老年代空间,虽然FullGC所需时间减少,但是老年代很快被存活对象填满,FullGC频率增加。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少FullGC的发生,Survivor的预筛选保证,只有经历16次MinorGC还能在新生代中存活的对象,才会被送到老年代。
最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置
Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次MinorGC,Eden中的存活对象就会被移动到Survivor 区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行MinorGC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
永远有一个Survivorspace是空的,另一个非空的Survivorspace无碎片。
新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区为8:1
即新生代中Eden:S1:S2=8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”的
JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread LocalAllocationBuffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
3000~5000
左右。之前说堆内存中有垃圾回收,比如Young区的MinorGC,Old区的MajorGC,Young区和Old区的FullGC。但是对于一个对象而言,怎么确定它是垃圾?是否需要被回收?怎样对它进行回收?等等这些问题我们还需要详细探索。因为Java是自动做内存管理和垃圾回收的,如果不了解垃圾回收的各方面知识,一旦出现问题我们很难进行排查和解决,自动垃圾回收机制就是寻找Java堆中的对象,并对对象进行分类判别,寻找出正在使用的对象和已经不会使用的对象,然后把那些不会使用的对象从堆上清除。
要想进行垃圾回收,得先知道什么样的对象是垃圾。
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其 引用,它就是垃圾。弊端:
如果AB相互持有引用,导致永远不能被回收。
通过GCRoot的对象,开始向下寻找,看某个对象是否可达。
image-20201009233200905
能作为GCRoot的有: 类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。但是不建议手动调用该方法,因为GC消耗的资源比较大。
(1)当Eden区或者S区不够用了
(2)老年代空间不够用了
(3)方法区空间不够用了
(4)System.gc()
已经能够确定一个对象为垃圾之后,接下来要考虑的就是回收,怎么回收呢?得要有对应的算法,下面介绍常见的垃圾回收算法。
找出内存中需要回收的对象,并且把它们标记出来
此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
image-20201009233748188
清除掉被标记需要回收的对象,释放出对应的内存空间
image-20201009233842912
弊端:
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程
序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
将内存划分为两块相等的区域,每次只使用其中一块,如下图所示:
image-20201009233954400
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
image-20201009234022894
弊端:
缺点:空间利用率降低。
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
其实上述过程相对"复制算法"来讲,少了一个"保留区"
image-20201010084600618
让所有存活的对象都向一端移动,清理掉边界意外的内存。
image-20201010084651076
既然上面介绍了3中垃圾收集算法,那么在堆内存中到底用哪一个呢?
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。 它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。
image-20201010085146815
SerialOld收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算法",运行过程和Serial收集器一样。
image-20201010085247022
可以把这个收集器理解为Serial收集器的多线程版本。
image-20201010085417195
发音:['perə.lel] ['skævəndʒ]
Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是 Parallel Scanvenge 更关注系统的吞吐量。
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
。比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,-XX:GCRatio直接设置吞吐量的大小。
ParallelOld收集器是ParallelScavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收,也是更加关注系统的吞吐量。
CMS(ConcurrentMarkSweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的是"标记-清除算法",整个过程分为4步
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。
image-20201010090421565
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别。
工作过程可以分为如下几步:
1602318174773
JDK11 引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了。会分为一个个 page,当进行 GC 操作时会对 page 进行压缩,因此没有碎片问题只能在64位的linux上使用,目前用得还比较少。
1602322566132
Serial 和 SerialOld
只能有一个垃圾回收线程执行,用户线程暂停。适用于内存比较小的嵌入式设备。
Parallel Scanvenge、ParallelOld
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适用于科学计算、后台处理等若交互场景。
CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。适用于相对时间有要求的场景,比如 Web。
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
小结:这两个指标也是评价垃圾回收器好处的标准。
本文主要介绍了 JVM 的运行时数据区域的组成、内存模型、常见垃圾回收算法以及垃圾收集器的选择。