JVM详解

一、JVM概述

在windows中,虚拟一个运行环境。

Java技术的核心就是java虚拟机,因为所有的java程序都运行在java虚拟机内部。

jdk、jre、jvm区别:

  • jdk是给开发人员提供的集成环境。打包,编译等工具
  • jre 运行环境,提供系统的类库
  • jvm管运行,字节码在jvm中运行,是一个虚拟的环境

jvm的组成部分

  1. 类加载器:负责将字节码加载到虚拟机中
  2. 运行时数据区:存储运行时的数据:程序计数器、本地方法栈、方法区、堆、java虚拟机栈(运行java自己的方法)
  3. 执行引擎:字节码翻译为机器码
  4. 本地方法接口:调用系统底层函数

程序在执行之前先要把java代码转换成字节码(class 文件),jvm首先通过类加载器把字节码加载到内存中的运行时数据区 ,然后通过执行引擎将字节码翻译成机器码,再交由CPU去执行,而这个过程中需要调用其他语言的接口本地库接口来实现整个程序的功能,这就是这 4 个主要组成部分的职责与功能。

JVM详解_第1张图片

JVM详解_第2张图片

二、类加载器

1、作用

将硬盘上的class文件(字节码),加载到内存中运行时数据区。至于它是否可以运行,由加载引擎决定。

JVM详解_第3张图片

User.class通过类加载成User的Class对象(Class类的对象来表示这个类的信息),然后去创建对象

2、类加载过程

JVM详解_第4张图片

2.1加载

使用IO读取字节码文件,转换为方法区中运行时结构,为每个类创建一个Class类的对象,存储在方法区中

2.2链接(验证、准备、解析)

验证:

  • 对字节码文件格式进行验证,文件是否被污染。.class 文件在文件开头有特定的文件标识(字节码文件都以CA FE BA BE标识开头)
  • 对基本的语法格式进行验证

准备:为静态的变量进行内存分配,并设置默认初始值;不包含用final修饰的static常量(final修饰的静态常量在编译时进行初始化)

public static int value = 123;//value 在准备阶段后的默认值是0,而不是123

解析:将编译后的字节码中的符号引用转成直接引用

(符号引用:Class文件的逻辑符号。直接引用:方法区中某一个地址)

2.3初始化

类的初始化,为类中的静态变量进行赋值

public static int value = 123;//value在初始化阶段后值是123.
2.4类什么时候会被加载(初始化)

类被初始化后,才认为类的加载完成了

JVM 规定,每个类或者接口被首次主动使用时才对其进行初始化。以下5种情况,类加载的过程是完整进行的

  1. 运行类中的main方法
  2. new关键字创建对象
  3. 使用类中的静态变量、静态方法
  4. 子类被加载,父类也会被加载
  5. 反射 Class.forName("类的地址");

以下两种情况类不会被初始化(不会被完整加载):

static final int b = 20; //编译期间赋值的静态常量
System.out.println(User.b);
User[] users = new User[10];//作为数组类型

3、类加载器分类

1.引导类加载器(启动类加载器)

用c/c++语言开发的,负责加载java核心类库。与java语言无关的。

2.扩展类加载器。

java 语言编写的,由sun.misc.Launcher$ExtClassLoader实现,继承ClassLoader类。从JDK安装目录的 jre/lib/ext子目录(扩展目录)下加载类库

3.应用程序类加载器。

Java语言编写的,由sun.misc.Launcher$AppClassLoader实现,派生于ClassLoader类。加载程序中自己开发的类。

JVM详解_第5张图片

应用程序类加载器是由扩展类加载器加载,扩展类加载器由引导类加载器加载的

4、双亲委派机制

加载一个类时,先委托给父类加载器加载。如果父加载器没有找到,继续向上级委托,直到最顶级的引导类加载器。父级找到就返回,如果没有找到就委派给子级加载器。最终所有的类加载器没有找到,报ClassNotFoundException。

为什么要这么做?

保证安全,为保证先加载系统中的类,避免我们写的类把系统里面的类覆盖掉,防止类被重复加载。

JVM详解_第6张图片

思考:自己创建一个名为java.lang的包,再创建一个名为String的类,当我们new String()时,会将加载创建核心类库中的String对象还是创建我们自己创建的String类对象?

先确保加载系统类,避免我们写的类把系统里面的类覆盖掉

public static void main(String[] args) {
	new java.lang.String();//逐级向上委托,最终引导类加载器找到了系统中真正的String
}
public class String{
	static {
		System.out.printIn("自定义的String");
    }
}

双亲委派优点?

  1. 安全,可避免用户自己编写的类替换Java的核心类,如java.lang.String。
  2. 避免类重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

双亲委派机制,是java提供的类加载的规范,但不是强制不能改变的。我们可以通过自定义的类加载器,改变加载方式。

打破双亲委派机制

可以通过继承ClassLoader类,重写findClass方法,去自己指定要加载的类。

典型的tomcat中,加载部署在tomcat中的项目时,就使用的是自己的类加载器

三、运行时数据区

1、程序计数器

每个线程都有一个程序计数器,用来记录每个线程所运行到的位置(用来存储下一条指令的地址,由执行引擎读取下一条指令),是线程私有的,生命周期与线程一致。

内存空间小,运行速度快,不会出现内存溢出的情况和垃圾回收。

2、本地方法栈

java虚拟机栈运行java方法的区域,而本地方法栈用来运行本地方法;(被Native修饰的方法//private native void start0();)

是线程私有的;空间大小可以调整,可能会出现内存溢出错误(栈溢出),不会有垃圾回收。

3、java虚拟机栈

栈是运行单位(堆是存储单位),是用来运行java中的方法,可能会出现栈溢出,不会有垃圾回收。每个方法在执行的同时都会创建一个线帧(用于存储局部变量表、操作数栈、动态链接、方法出口等信息),每个方法从调用到执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。

是线程私有的,每一个线程对应一个栈。不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。

栈的操作有两个,调用方法(入栈),运行结束(出栈)

JVM详解_第7张图片

栈的特点

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器(管运行的,所以速度快)
  • JVM直接对java栈的操作只有两个:对栈帧的入栈和出栈,遵循先进后出的原则
  • 不存在垃圾回收问题

栈中会出现异常,当线程请求的栈深度大于虚拟机所允许的深度时 , 会出现StackOverflowError。(内存不够)

在一条活动的线程中,一个时间点上,只有一个活动栈,只有当前在执行的方法的栈帧(栈顶)是有效的,被称为当前栈帧,与当前栈帧对应的方法为当前方法,定义这个方法的类为当前类。

JVM详解_第8张图片

栈帧结构:

(1)局部变量表(存储方法中的变量)

(2)操作数栈(数据计算的过程 所在的空间)

(3)动态链接 。A调用B,会在A方法中存储B方法的地址

void A(){
    B();//B方法的地址
}

(4)方法返回地址。方法执行完毕之后,要返回之前调用它的地方。

4、堆

1.堆内存概述

是存储空间,用来存储java对象,是Java虚拟机中最大的一块内存,是所有线程共享的。

在jvm启动时就被创建,大小可以调整(jvm调优)。例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小),一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除,堆是垃圾回收的重点区域。

2.堆内存区域划分

Java8及之后堆内存分为:新生区(新生代)+老年区(老年代)

新生区分为伊甸园区和两个幸存者区

伊甸园区:存放刚刚创建的对象

两个幸存者区:始终有一个区域是空的,减少内存中的碎片

老年区:存放生命周期较长的对象,垃圾回收的频率会降低;年轻代垃圾回收的频率高

JVM详解_第9张图片

3.为什么要分区?(分代)

可以根据对象存活的时间长短,把对象放在不同的区域,不同区域的垃圾回收策略不同,可以提高垃圾回收效率。

频繁回收年轻代,较少回收老年代。

4.对象创建,在堆内存中分配的过程
  1. 新创建的对象,都存储在伊甸园区。
  2. 当垃圾回收时,将伊甸园中垃圾对象直接销毁,将存活的对象移到幸存者0区。
  3. 之后创建的新对象还是存储在伊甸园区,再次垃圾回收时,将伊甸园中存活的对象和幸存者0区的存活对象移动到幸存者1区。每次保证一个幸存者区为空的,之后相互转换。
  4. 每次垃圾回收时,都会记录此对象经历的垃圾回收次数。当一个对象经历过15次回收,仍然存活,就会被移动到老年区。(垃圾回收次数,在对象头中有一个4bit的空间记录,最大值只能是15)
  5. 老年区回收次数较少,当内存空间不够用时,才会回收老年代。若养老区执行了Major GC之后发现依然无法进行对象保存,就会产生OOM异常,Java.lang.OutOfMemoryError:Java heap space
public static void main(String[] args) {
	List list = new ArrayList();
	while(true){
		list.add(new Random().nextInt());
	}
}
5.堆空间的配置比例

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

  1. 默认-XX:NewRatio=2,表示新生代与老年代的比例为1:2,新生代占整个堆的1/3。可以修改-XX:NewRatio=4,表示新生代与老年代的比例为1:4,新生代占整个堆的1/5。
  2. 在HotSpot中,伊甸园和两个幸存者区比例为8:1:1,可以通过选项-XX:SurvivorRatio调整这个空间比例。比如-XX:SurvivorRatio=8

在整个项目中,生命周期长的对象偏多,就可以把老年代设置更大,来进行调优

对象垃圾回收的年龄 -XX:MaxTenuringThreshold=

6.分代收集思想Minor GC、Major GC、Full GC

JVM在进行GC时,大部分时候回收的都是指新生区,针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:部分收集、整堆收集。

  1. 部分收集:

    对年轻代进行垃圾回收称为Minor GC/yong GC,是频繁进行的回收

    对老年代进行垃圾回收称为Major GC/old gc,回收的次数较少

  2. 整堆收集:在内存实在不够用时,会触发Full GC(整堆收集),对整个java堆和方法区进行垃圾收集。

以下情况会触发Full GC:

  • System.gc();时(主动触发Full GC,程序员几乎不用)
  • 老年区空间不足
  • 方法区空间不足

开发期间尽量避免整堆收集,因为垃圾回收时,会造成其他现成的暂停。

7.堆空间的参数设置

JVM调优:根据实际的场景,来对内存空间的大小、比例、垃圾回收器进行设置。

官方文档 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

JVM详解_第10张图片

  • -XX:+PrintFlagsInitial 查看所有参数的默认初始值
  • -XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)
  • -Xms:初始堆空间内存(默认为物理内存的1/64)-Xmx:最大堆空间内存(默认为物理内存的1/4)
  • -Xmn:设置新生代的大小(初始值及最大值)
  • -XX:NewRatio:配置新生代与老年代在堆结构的占比
  • -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间比例
  • -XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
  • -XX:+PrintGCDetails 输出详细的GC处理日志
8.字符串常量池为什么要调整位置?

在jdk7之前,字符串常量池在方法区中,由于方法区的垃圾回收在整堆收集时发生,回收频率低。在jdk7之后,将字符串常量池的位置移到了堆空间中,回收频率高

5、方法区

1.基本理解

俗称为方法区,在jdk1.7之前称为永久代,在1.7之后称为元空间。

主要是存储类信息。在jvm启动时创建,大小可以调整,是线程共享,也会出现内存溢出,会发生垃圾回收(Full GC/整堆收集)。

Java虚拟机规范中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。

方法区、堆、栈的交互关系

JVM详解_第11张图片

  • 方法区存储:类的信息(元信息)
  • 堆中存储:对象
  • 栈中存储:引用变量
2.方法区大小设置

Java方法区的大小不是固定的,JVM可以根据应用的需要动态调整。

元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。

默认值依赖于平台,windows下,-XXMetaspaceSize是21MB,也称为高水位线,一旦触及就会触发Full GC,尽量将方法区的初始值设置较大一点,因为方法区一旦空间不足,就会触发FULL GC。

-XX:MaxMetaspaceSize 的值是-1,即没有限制,就可以使用计算机内存。

3.方法区的内部结构
  1. 类信息
  2. 运行时常量池(指的就是类中各个元素的编号)

JVM详解_第12张图片

运行时常量池:与字符串常量池不同,运行时常量池中,主要用于存放编译期生成的各种字面量与符号引用

4.方法区的垃圾回收

在FULL GC时,方法区发生垃圾回收。

主要是回收类信息,类信息回收条件比较苛刻,满足以下3点即可:

  1. 在堆中,该类及其子类的对象都不存在了
  2. 该类的类加载器不存在了
  3. 该类的Class对象不再被使用

一般可以认为类一旦被加载就不会被卸载(回收)。

5.特点总结

程序计数器、java栈、本地栈是线程私有的;堆、方法区是线程共享的,是会出现垃圾回收的

程序计数器不会出现内存溢出;java栈,本地栈、堆、方法区可能会出现内存溢出

java栈、本地栈、堆、方法区大小是可以调整的

四、本地方法接口

1、什么是本地方法

本地方法:用native关键字修饰的方法,没有方法体。不是java语言实现的(操作系统底层方法)例如hashCode();

关键字native可以与其他所有的java标识符连用,但是abstract除外。

2、为什么用本地方法

java语言需要与外部的环境进行交互(例如需要访问内存,硬盘,其他的硬件设备),直接访问操作系统的接口即可。

java的jvm本身开发也是在底层使用到了C语言

五、执行引擎

1、概述

作用:将加载到内存中的字节码(不是直接运行的机器码),解释/编译为不同平台的机器码

.java文件 ---编译器(javac)-->.class(字节码文件),在开发期间,由jdk提供的编译器(javac)进行源码编译 (称为前端编译,编译期

.class(字节码)----解释/编译成--->机器码(后端编译,在运行时由执行引擎完成的, 运行时

2、什么是解释器?什么是JIT编译器?

解释器:将字节码逐行解释执行,效率速度慢,一般刚开始的时候使用解释执行

编译器(JIT just in time 即时编译器):将一些代码进行整体编译,后期执行效率快,但是编译是需要时间的。在程序运行过程中,将一些频繁执行的热点代码进行编译,并缓存到方法区中,以后执行效率提高了。

是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升java程序的执行性能。

热点代码:一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体。

目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。

4、JIT编译器执行效率高为什么还需要解释器?

  1. 程序启动后,先使用解释器立即执行,快速响应起来,省去了编译时间
  2. 程序运行一段时间后,对热点编译缓存,提高后续执行效率。

采用的解释器和编译器结合的方案。

六、垃圾回收

1、概述

java是支持自动垃圾回收,有些语言不支持需要手动。自动垃圾回收不是java语言首创的。

垃圾回收关心的问题:

  1. 哪些区域需要回收?方法区、堆
  2. 什么时候回收
  3. 如何回收
1.什么是垃圾

在运行过程中,当一个对象不再被任何引用指向时,被称为垃圾对象。

String s1 = "abc";
s1 = null;
2.为什么需要GC

如果不及时清理内存中的垃圾对象,内存会被耗尽,严重时会出现内存溢出,新的对象没有空间存储。

垃圾回收也可以清除内存里的碎片。碎片整理 将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象(数组必须是连续空间的)

3.早期的垃圾回收

在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new进行内存申请,并使用delete进行内存释放。比如以下代码:

MibBridge *pBridge= new cmBaseGroupBridge();
//如果注册失败,使用 Delete 释放该对象所占内存区域
if(pBridge->Register(kDestroy)!=NO ERROR)
delete pBridge;

这种方式控制更加精确,但是给程序员带来了更多的工作量,万一忘记删除,那么会占用内存不释放(内存泄漏)

现在除了Java以外,C#、Python、Ruby等语言都使用了自动垃圾回收的思想。

4.自动内存管理

好处:对内存管理更合理、自动化,降低内存泄漏和内存溢出的风险;将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。

坏处:对程序员管理内存的能力降低了,解决问题能力变弱了,不能调整垃圾回收的机制。

哪些区域的回收?

垃圾回收主要针对方法区进行,方法区只有在full gc时触发。

  • 频繁收集Young区
  • 较少收集Old区
  • 基本不收集元空间(方法区)
5.内存溢出和内存泄漏

内存溢出:对象不断被创建,经过垃圾回收后,仍然不能满足新对象的存储,就会导致程序崩溃。OutofMemoryError

内存泄漏:一些对象不再被使用,但是垃圾回收器不能判定为垃圾,这些对象就默默的占用着内存资源,称为内存泄漏。大量的此类对象存在,也是导致内存溢出的原因。

内存泄漏的场景:

一些提供close()的资源未关闭导致内存泄漏:IO流、jdbc连接,Socket用完后没有关闭

单例对象,一个程序中,只存在一个对象,生命周期长

2、垃圾标记阶段算法

作用:判断对象是否为垃圾对象。

当一个对象不再被任何引用指向时,就为垃圾对象。相关的标记算法:引用计数算法和可达性分析算法。

1.引用计数算法

引用计数算法(在现代的jvm中并没有被使用)。

实现原理:对每个对象保存一个整型的引用计数器属性,计数器来记录对象被引用的情况

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

String s1 = new String("aaa");//1
String s2 = s1;  //2,有两个引用变量指向aaa对象
s2 = null; -1
s1 = null; -1

缺点:

  1. 需要存储计数器,占用存储空间
  2. 每次赋值都需要更新计数器,伴随着加法和减法操作,比较耗时间。
  3. 无法解决循环引用问题。多个对象之间相互引用(A对象引用B,B对象引用A),没有其他外部引用指向他们中的任何一个,计数器都不为0,就无法被外界使用,但是也不能被回收,产生内存泄漏。JVM详解_第13张图片
2.可达性分析算法(根搜索算法)

实现思路:从一些根对象(GCRoots)出发去查找,与根对象直接或间接连接的对象就是存活对象,否则判定为垃圾对象

JVM详解_第14张图片

GC Roots可以是哪些元素?

总的来说是活跃的,正在使用的或者系统中的

  • 在虚拟机栈中引用的对象(正在运行的方法中用到的对象).
  • 类中静态属性引用的对象,由于静态变量生命周期较长。
  • 被当做同步锁的对象
  • 虚拟机内部使用的一些对象,Class对象、异常对象、类加载器对象等等
3.对象的finalization机制

final,finally,finalize()区别?

  • final关键字,修饰类不能被继承,修饰常量值不能被修改, 修饰方法不能被重写。
  • finally{ }代码块,在异常处理中使用,最终是否出现异常都会执行的代码
  • finalize()方法:当一个对象被标记为垃圾后,在真正被回收之前,会先调用该对象的finalize()方法,是否还需要进行处理。在finalize()中,对象有可能复活,一旦复活,对象就不能被回收。但是finalize()只能被调用一次,下一次被判定为垃圾后,就不能调用了,直接被回收。(finalize()被定义在Object类中)

finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

自己不要在程序中调用finalize(),留给垃圾回收器调用。理由包括下面三点:

  1. 在finalize()时可能会导致对象复活。
  2. finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
  3. 一个糟糕的finalize()会严重影响GC的性能。比如finalize是个死循环。

生存还是死亡?

由于finalization机制的存在,在虚拟机中的对象状态分为3种:

  1. 可触及的。不是垃圾对象,从根对象可以找到的。
  2. 可复活的。判定为垃圾了,但是还没有调用对象的finalize(),在finalize()中对象可能会复活)。
  3. 不可触及的。被判定为垃圾,已经调用过finalize()的。
4.垃圾回收相关概念

1.System.gc()会触发Full GC,程序员不要主动去调用。

.gc()方法被调用后,不会保证立即执行,因为垃圾回收也是一个线程,需要操作系统调度之后才可以执行。

2.内存溢出和内存泄漏

内存溢出:对象不断被创建,经过垃圾回收后,仍然不能满足新对象的存储,就会导致程序崩溃。OutofMemoryError

内存泄漏:一些对象不再被使用,但是垃圾回收器不能判定为垃圾,这些对象就默默的占用着内存资源,称为内存泄漏。大量的此类对象存在,也是导致内存溢出的原因。

内存泄漏的场景:

一些提供close()的资源未关闭导致内存泄漏:IO流、jdbc连接,Socket用完后没有关闭

单例对象,一个程序中,只存在一个对象,生命周期长。

3.Stop the World(STW)

在当垃圾回收的线程工作时,其他的线程会暂停一下。

为什么要暂停其他线程?确保是在某一个时间点上,否则运行中的分析是不准确的。

垃圾线程执行完后,其他线程恢复执行。越优秀的回收器,暂停时间比较短.

3、垃圾回收阶段算法

区分出内存中存活对象和死亡对象后,GC接下来就是垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是:

  • 标记-复制算法(Copying)
  • 标记-清除算法(Mark-Sweep)
  • 标记-压缩算法(Mark-Comp)
1.标记-复制算法

有两个大小相等的内存,一个是正在使用的,一个是空闲的;把正在使用的内存中存活的对象复制到空闲内存中,将正在使用的空间中的所有对象清除。

JVM详解_第15张图片

优点:减少内存的碎片,运行效率高。

缺点:需要两块内存空间,其中一块是空闲的。

使用场景:垃圾对象多、存活对象少,适合于新生代的垃圾回收。

2.标记-清除算法

清除不是真正的把垃圾对象清除掉,也没有移动存活的对象。

将垃圾对象地址保存到一个空闲列表中。后面有新对象到来时,从空闲列表中找到一个能够放的下新对象的地址,覆盖掉垃圾对象。

优点:运行简单

缺点:回收效率低;清理后的内存是不连续的,会产生碎片

3.标记-压缩算法(标记-整理)

将所有的存活对象移动到内存的一端,将其他区域的垃圾对象进行清理。

压缩算法是需要移动对象的

优点:不像复制算法有两块内存空间,不像清除算法有内存碎片。

缺点:回收速度慢

4.垃圾回收算法小结

复制算法适合新生区,其余适合老年区

复制 清除 压缩
速率 最快 中等 最慢
空间开销 通常需要2倍空间(不堆积碎片) 少(会堆积碎片) 少(不堆积碎片)
移动对象
5.分代收集

前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点,分代收集应运而生。

不同的对象的生命周期是不一样的,可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

年轻代特点:区域较小、对象生命周期短、存活率低、回收频繁。这种情况复制算法的回收整理,速度是最快的

老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。一般是由标记-清除标记-清除与标记-压缩的混合实现

4、垃圾回收器

垃圾回收器是内存回收的实践者,垃圾回收算法是方法论。jvm中提供了许多不同类型的垃圾回收器,可以根据实际情况进行选择,这也是jvm调优的一部分。

1.垃圾回收器分类

(1)按线程数量:单线程垃圾回收器、多线程垃圾回收器

  • 单线程垃圾回收器(Serial):只有一个线程进行垃圾回收,适合小型设备,垃圾回收时其他用户线程会暂停
  • 多线程垃圾回收器(Parallel):有多个线程进行垃圾回收,服务器端,但同样也会暂停其他用户线程。

(2)按工作模式:独占式、并发式

  • 独占式:垃圾回收线程执行时,其他用户线程暂停
  • 并发式:垃圾回收线程可以和用户线程同时执行(从CMS垃圾回收器开始)

JVM详解_第16张图片

(3)按回收区间:年轻代垃圾回收器、老年代垃圾回收器

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。

JVM详解_第17张图片

2.垃圾回收器性能指标
  • 暂停时间:垃圾回收时,用户线程暂停的时间。(越短越好)
  • 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
  • 内存占用:Java堆区所占的内存大小。
  • 回收的速度
  • 垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
  • 快速:一个对象从诞生到被回收所经历的时间
3.CMS垃圾回收器
3.1 概述

Concurrent Mark Sweep并发标记清除,支持垃圾回收线程与用户线程并发(同时)执行(只是在某个阶段做到了并发,还存在独占的)

初始标记:独占式的,暂停用户线程

并发标记:垃圾回收线程与用户线程并发(同时)执行

重新标记:独占式的,多垃圾线程执行

并发清除:垃圾回收线程与用户线程并发(同时)执行 进行垃圾对象的清除

JVM详解_第18张图片

从CMS垃圾回收器开始,首创了并发并发垃圾收集

优点:可以作到并发收集

弊端:使用标记清除算法,会产生内存碎片;并发执行影响到用户线程;无法处理浮动垃圾

3.2 三色标记算法

引入并发标记后,用户线程和垃圾回收线程可以同时执行,会带来一个问题:对象是否为垃圾不好确定。

所以在标记时,将对象分为3种颜色(3种状态)

  • 黑色:例如GCRoots,确定是存活的对象。
  • 灰色:已经扫描过一次,下属还没有被标记,之后还需要再次进行扫描。
  • 白色:没有被扫描到的对象。

三色标记的过程:

  1. 先确立GCRoots,把GCRoots标记为黑色。
  2. 与GCRoots直接关联的对象标记为灰色。
  3. 再次扫描灰色,灰色下如果有关联的对象,灰色变为黑色,并把关联的对象变为灰色。重复这一步骤,逐级遍历。
  4. 最终保留黑色、灰色,清除白色对象

这个过程正确执行的前提是:没有其他线程改变对象间的引用关系。然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标错标的情况。

(1)漏标

JVM详解_第19张图片

灰色对象下面还有未扫描的白色对象,但是灰色与黑色断开,本来灰色和白色就与跟对象没有关系了,可以被回收,但是灰色的还会再进行一次扫描,这样本次垃圾回收就不能回收灰色和白色。

(2)错标

B.D=null;//B到D的引用被切断

A.xx=D;//A到D的引用被建立

进行一次扫描,这样本次垃圾回收就不能回收灰色和白色  

JVM详解_第20张图片

灰色对象和白色对象断开联系,白色被判定为垃圾,但是某个黑色与白色关联,黑色对象不再被扫描了,这样错将与黑色关联的白色 认定为垃圾。

错标的结果比漏表严重的多,会把不该清除的对象清除掉。

出现错标的条件:

  1. 灰色对象与白色对象断开联系
  2. 黑色对象与白色对象建立联系

只要打破任一条件,就可以解决错标的问题。

解决错标:

JVM详解_第21张图片

  1. 原始快照:当灰色对象与白色对象断开时,进行拍照,将这条引用关系记录下来。当扫描结束后。再以这些灰色对象为根,重新扫描一次。
  2. 增量更新:一旦黑色对象与白色对象建立联系,就对黑色对象进行重新扫描。
4. G1回收器(Garbage-First)(垃圾优先)

JVM详解_第22张图片

将堆内存划分成较小的多个区域,对这些个区域进行跟踪,优先回收 垃圾数量大的区域。

G1也是支持并发收集的,而且吞吐量优于CMS,可以对整个堆进行管理。

你可能感兴趣的:(jvm)