JVM基础2

JVM基础

官网概括
官网 :https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

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.

在这里插入图片描述

Method Area(方法区)

(1)方法区是所有线程共享的内存区域,在虚拟机启动时创建。

The Java Virtual Machine has a method area that is shared among all Java Virtual
Machine threads.
The method area is created on virtual machine start-up.

(2)虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非 堆),目的是与Java堆区分开来

Although the method area is logically part of the heap,......

(3)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

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.

(4)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

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

在这里插入图片描述

Heap(堆)

(1)Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享
(2)Java 对象实例以及数组都在堆上分配。

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.
The heap is created on virtual machine start-up.

Java Virtual Machine Stacks(虚拟机栈)

(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程 的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创 建。

Each Java Virtual Machine thread has a private Java Virtual Machine stack,
created at the same time as the thread.

(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。 每调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

A Java Virtual Machine stack stores frames (§2.6).A new frame is created each time a method is invoked. A frame is destroyed when
its method invocation completes.

图解栈和栈帧

void a(){
    b();
}
void b(){
	c(); 
}
void c(){ 
}

在这里插入图片描述

栈帧

栈帧: 每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
在这里插入图片描述

动态链接:
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)

pc Register(程序计数器)

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

如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。

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.

Native Method Stacks(本地方法栈)

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
那如果在Java方法执行的时候调用native的方法呢?
在这里插入图片描述
除了上面五块内存之外,其实我们的JVM还会使用到其他两块内存 :
直接内存和其它内存。

直接内存(Direct Memory)

并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频 繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆 里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能, 因为避免了在Java 堆和Native 堆中来回复制数据。

本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总 内存的大小及处理器寻址空间的限制。因此在分配JVM空间的时候应该考虑直接内存所带来的影 响,特别是应用到NIO的场景。

其他内存:

Code Cache:**JVM本身是个本地程序,还需要其他的内存去完成各种基本任务,比如,JIT 编译器在运行时对热点方法进行编译,就会将编译后的方法储存在Code Cache里面;GC等 功能。需要运行在本地线程之中,类似部分都需要占用内存空间。这些是实现JVM JIT等功能 的需要,但规范中并不涉及。

java对象模型

栈指向堆

如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元 素指向堆中的对象。
在这里插入图片描述

方法区指向堆

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

private static Object obj=new Object();

在这里插入图片描述

堆指向方法区

What?堆还能指向方法区?
是的,你没听错。
注意,方法区中会包含类的信息堆中会有对象,那怎么知道对象是哪个类创建的呢?
在这里插入图片描述
一个对象怎么知道它是由哪个类创建出来的?怎么记录?
这就需要了解一个Java对象的具体信息咯。

Java对象内存模型

一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充
在这里插入图片描述

验证hashCode的储存方式

使用到我们的jol工具
依赖:

<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>RELEASE</version>
</dependency>

实体类:

package com.example.jvmcase.domain;
import lombok.Data;
import org.openjdk.jol.info.ClassLayout;
public class Worker {
    private Integer id;
    private String username;
    private String password;
    public Integer getId() {
        return id;
}
    public String getPassword() {
        return password;
}
    public String getUsername() {
        return username;
}
    public void setUsername(String username) {
        this.username = username;
}
    public void setPassword(String password) {
        this.password = password;
}
    public void setId(Integer id) {
        this.id = id;
}
    @Override
    public String toString() {
        return super.toString();
    }
public static void printf(Worker p) {
// 查看对象的整体结构信息
// JOL工具类 
System.out.println(ClassLayout.parseInstance(p).toPrintable());
} 
}

测试代码:

package com.example.jvmcase.basic;
import com.example.jvmcase.domain.Worker;
public class Test {
    public static void main(String[] args) {
        Worker work = new Worker();
        System.out.println(work);
        Worker.printf(work);
        System.out.println(work.hashCode());
        } }

测试结果:

com.example.jvmcase.domain.Worker@6acbcfc0
com.example.jvmcase.domain.Worker object internals:
 OFFSET  SIZE                TYPE DESCRIPTION
VALUE
04 (object header) 01 c0 cf cb (00000001 11000000 11001111 11001011) (-875577343)
      4     4                     (object header)                           6a
00 00 00 (01101010 00000000 00000000 00000000) (106)
      8     4                     (object header)                           43
c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 16 4 20 4
java.lang.Integer Worker.id                                 null
 java.lang.String Worker.username                           null
 java.lang.String Worker.password                           null
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

1791741888

1791741888这个数字是我们的HashCode值,转换成16进制可得6a cb cf c0,经过对比。
由此可得,我们的哈希码使用的大端储存。

例如:
十进制数9877,如果用小端存储表示则为:
高地址 <-------- 低地址 10010101`[高序字节]` 00100110`[低序字节]` 用大端存储表示则为:
高地址 <-------- 低地址 00100110`[低序字节]` 10010101`[高序字节]`

小端存储:便于数据之间的类型转换,例如:long类型转换为int类型时,高地址部分的数据可以 直接截掉。
大端存储:便于数据类型的符号判断,因为最低地址位数据即为符号位,可以直接判断数据的正 负号。

Class Pointer

引用定位到对象的方式有两种,一种叫句柄池访问,一种叫直接访问
在这里插入图片描述
直接访问:
优点:节省了一次指针定位的开销。
句柄池访问:
缺点:增加了一次指针定位的时间开销。

JVM内存模型

上面对运行时数据区描述了很多,其实**重点存储数据的是堆和方法区(非堆),**所以内存的设计也着重从 这两方面展开(注意这两块区域都是线程共享的)。

对于虚拟机栈,本地方法栈,程序计数器都是线程私有的

可以这样理解,JVM运行时数据区是一种规范,而JVM内存模式是对该规范的实现在这里插入图片描述

一块是非堆区,一块是堆区
堆区分为两大块,一个是Old区,一个是Young区
Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区
S0和S1一样大,也可以叫From和To

对象创建过程

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

在这里插入图片描述

如何理解各种GC

Partial GC :

Partial其实也就是部分的意思.那么翻译过来也就是回收部分GC堆的模式,他并不会回收我们整个堆.
而我们 的youngGC以及我们的Old GC都属于这种模式

young GC:

只回收young区

old GC:

只回收Old区

full GC:

实际上就是对于整体回收

为什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了 Full GC)。
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。

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

所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生。
Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么需要两个Survivor区?

最大的好处就是解决了碎片化。
也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置 Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程: 刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor 区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些 存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的, 也就导致了内存碎片化。
永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

新生代中Eden:S1:S2为什么是8:1:1?

新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区为8:1
即新生代中Eden:S1:S2 = 8:1:1 现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是 “朝生夕死”的。

堆内存中都是线程共享的区域吗?

JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。

JVM垃圾回收

jvm会回收没有引用的对象,那么如何判断是否该回收呢?
也就是如何寻找垃圾的算法。
1)判断对象的引用==0
在这里插入图片描述
这种算法,是看一个对象(A),是否被另一个对象引用。
缺陷如上图,如果A和B都是垃圾,且相互引用,这样GC就识别不到了。所以,这种算法被遗弃了。Python目前还在用这种算法。

2)可达性分析算法(也叫 根搜索算法)
在这里插入图片描述
这种算法,只要从根节点无法到达的节点,都认为是垃圾。GC会把f节点和g节点认为是垃圾。

引用

说了这么多,什么是引用?
Java 中的引用的定义很传统:如果 reference (引用)类型的数据中存储的数值代表的是另外一块内存 的起始地址,就称这块内存代表着一个引用。

我们应该先聊一聊对象的引用, 一般来说,我们的引用有四种:
强引用、软引用、弱引用、虚引用。

强引用

Java 中最常见的就是强引用,在开发过程中经常会使用到的引用.把一个对象赋给一个引用 变量,这个引用变量就是一个强引用。

软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它 不会被回 收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

软引用的常见使用:

package com.example.jvmcase.reference;
import com.example.jvmcase.domain.Worker;
import org.hibernate.jdbc.Work;
import java.lang.ref.SoftReference;
/**
* @ClassName SoftReferenceDemo * @Title:
* @Author: Carl
* @Date: 2020/12/12 0012 15:26 * @Description:TODO
 */
public class SoftReferenceDemo {
public static void main(String[] args) { 
	//。。。一堆业务代码
	Worker a = new Worker(); 
	//。。业务代码使用到了我们的Worker实例
//
// 使用完了a,将它设置为soft 引用类型,并且释放强引用; 
	SoftReference sr = new SoftReference(a);
	a = null;
	// 下次使用时
	if (sr!=null) {
	     a = (Worker) sr.get();
	}else{
// GC由于内存资源不足,可能系统已回收了a的软引用,
 // 因此需要重新装载。
     a = new Worker();
    sr=new SoftReference(a);
} }
}

弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象 来说,只 要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

/**
* 弱引用关联对象何时被回收
* Created by carl at 2020/12/3. */
public class WeakReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
	//100M的缓存数据
	byte[] cacheData = new byte[100 * 1024 * 1024]; 
	//将缓存数据用软引用持有
	WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData); System.out.println("第一次GC前" + cacheData); System.out.println("第一次GC前" + cacheRef.get()); 
	//进行一次GC后查看对象的回收情况
	System.gc();
	//等待GC
	Thread.sleep(500);
	System.out.println("第一次GC后" + cacheData); System.out.println("第一次GC后" + cacheRef.get());
	//将缓存数据的强引用去除 
	cacheData = null; System.gc(); 
	//等待GC 
	Thread.sleep(500);
	System.out.println("第二次GC后" + cacheData);
	System.out.println("第二次GC后" + cacheRef.get()); }
} 
第一次GC前[B@7d4991ad 
第一次GC前[B@7d4991ad 
第一次GC后[B@7d4991ad 
第一次GC后[B@7d4991ad 
第二次GC后null 
第二次GC后null

虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主 要作用是跟踪对象被垃圾回收的状态。

对象的生命周期

在Java中,对象的生命周期包括以下几个阶段:
1.创建阶段( Created )
2.应用阶段( In Use )
3.不可见阶段( Invisible )
4.不可达阶段( Unreachable )
5.收集阶段( Collected )
6.终结阶段( Finalized )
7.对象空间重分配阶段( De-allocated )

创建阶段

为对象分配存储空间
开始构造对象
从超类到子类对static成员进行初始化
超类成员变量按顺序初始化,递归调用超类的构造方法
子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段

应用阶段( In Use )

对象至少被一个强引用持有着。

不可见阶段( Invisible )

当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
简单说就是程序的执行已经超出了该对象的作用域了。

不可达阶段( Unreachable )

对象处于不可达阶段是指该对象不再被任何强引用所持有。 与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被 JVM 等系统下的某些已装载的静态变量或线程或 JNI 等强引用持有着,这些特殊的强引用被称为” GC root ”。存在着这些 GC root 会导致对象的内存泄露情况,无法被回收。

收集阶段( Collected )

当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了 finalize() 方法,则会去执行该方法的终 端操作。

finalize() 方法:
特别说明一下:不要重载finazlie()方法!原因有两点:
1)会影响JVM的对象分配与回收速度
在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对 象执行两次GC。
2)可能造成该对象的再次“复活”
在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。
这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。
在这里插入图片描述

终结阶段

在该阶段是等待垃圾回收器对该对象空间进行回收。

什么时候会垃圾回收

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

(1)当Eden区或者S区不够用了
(2)老年代空间不够用了
(3)方法区空间不够用了
(4)System.gc()

垃圾收集算法

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

标记-清除(Mark-Sweep)

标记
找出内存中需要回收的对象,并且把它们标记出来。
此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
在这里插入图片描述
清除
清除掉被标记需要回收的对象,释放出对应的内存空间。
在这里插入图片描述
缺点:

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

标记-复制(Mark-Copying)

将内存划分为两块相等的区域,每次只使用其中一块,如下图所示:
在这里插入图片描述
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
在这里插入图片描述
缺点:

空间利用率降低。

标记-整理(Mark-Compact)

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果 不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有 100%存活的极端情况,所以老年代一般不能直接选用这种算法。
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活 的对象都向一端移动,然后直接清理掉端边界以外的内存。

其实上述过程相对"复制算法"来讲,少了一个"保留区"
在这里插入图片描述
让所有存活的对象都向一端移动,清理掉边界意外的内存。
在这里插入图片描述

分代收集算法

既然上面介绍了3中垃圾收集算法,那么在堆内存中到底用哪一个呢?
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
在这里插入图片描述

Serial

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

适用范围:新生代

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

在这里插入图片描述

Serial Old

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算法",运行过程和Serial收集器一样
在这里插入图片描述

ParNew

可以把这个收集器理解为Serial收集器的多线程版本。
适用范围:新生代

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

CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。
采用的是"标记-清除算法",整个过程分为4步:
(1)初始标记 CMS initial mark
标记GC Roots 直接关联对象,不用Tracing,速度很快
(2)并发标记 CMS concurrent mark
进行GC Roots tracing
(3)重新标记 CMS remark
修改并发标记因用户程序变动的内容
(4)并发清除 CMS concurrent sweep
清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为 浮动垃圾。

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

优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量,还会并发失败

G1收集器

JDK 9默认的垃圾收集器。
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个 大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再 是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂
如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中
设置Region大小:-XX:G1HeapRegionSize=M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域

(1)分代收集(仍然保留了分代的概念)
(2)空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消 耗在垃圾收集上的时间不得超过N毫秒)
在这里插入图片描述

初始标记(Initial Marking):
标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程

并发标记(Concurrent Marking):
从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行。

最终标记(Final Marking)
修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程。

筛选回收(Live Data Counting and Evacuation)
对各个Region的回收价值和成本进行排序,根据 用户所期望的GC停顿时间制定回收计划
在这里插入图片描述
相关参数
-XX: +UseG1GC 开启G1垃圾收集器
-XX: G1HeapReginSize 设置每个Region的大小,是2的幂次,1MB-32MB之间 -XX:MaxGCPauseMillis 最大停顿时间
-XX:ParallelGCThread 并行GC工作的线程数
-XX:ConcGCThreads 并发标记的线程数
-XX:InitiatingHeapOcccupancyPercent 默认45%,代表GC堆占用达到多少的时候开始垃圾收集

ZGC收集器

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

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

垃圾收集器分类

串行收集器->Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。

适用于内存比较小的嵌入式设备 。

并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

适用于科学计算、后台处理等若交互场景 。

并发收集器[停顿时间优先]->CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。

适用于相对时间有要求的场景,比如Web 。

如何开启需要的垃圾收集器

这里JVM参数信息的设置大家先不用关心,后面会学习到。

(1)串行
	 -XX:+UseSerialGC 
	 -XX:+UseSerialOldGC
(2)并行(吞吐量优先): 
	-XX:+UseParallelGC 
	-XX:+UseParallelOldGC
(3)并发收集器(响应时间优先)
	 -XX:+UseConcMarkSweepGC 
	 -XX:+UseG1GC

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值