目录
一、JVM概述
1、1为什么要学习JVM
1、2虚拟机
1、3JVM作用
1、4JVM整体组成部分
二、JVM结构--类加载器
2、1类加载子系统
2、2类加载过程
2、2、1加载
2、2、2链接
2、2、3初始化
2、3类加载器分类
2.3.1 引导类加载器(启动类加载器 BootStrap ClassLoader)
2.3.2 扩展类加载器(Extension ClassLoader)
2.3.3 应用程序类加载器(系统类加载器 Application ClassLoader)
2.4 双亲委派机制
2.5 如何打破双亲委派机制
三、JVM运行时数据区
3、1运行时数据区组成概述
3.1.1 程序计数器(Program Counter Register)
3.1.2Java 虚拟机栈(Java Virtual Machine Stacks)
3.1.3 本地方法栈(Native Method Stack)
3.1.4Java 堆(Java Heap)
3.1.5 方法区(Methed Area)
3.2.程序计数器(Program Counter Register)
3.3.Java 虚拟机栈(Java Virtual Machine Stacks)
3.4.本地方法栈(Native Method Stack)
3.5.Java 堆内存
3.5.1Java堆内存概述
3.5.2堆内存区域划分
3.5.3 为什么分区(代)?
3.5.4 对象创建内存分配过程
3.5.5 新生区与老年区配置比例
3.5.6 分代收集思想 Minor GC、Major GC、Full GC
3.5.7 堆空间的参数设置
3.5.8 字符串常量池
3.6方法区
3.6.1 方法区的基本理解
3.6.2 方法区大小设置
3.6.3 方法区的内部结构
3.6.4 方法区的垃圾回收
四、本地方法接口
4.1 什么是本地方法
4.2 为什么要使用 Native Method
五、执行引擎
5.1 概述
5.2 什么是解释器?什么是 JIT 编译器?
5.3 为什么 Java 是半编译半解释型语言?
六、垃圾回收
6.1 垃圾回收概述
6.1.1 概述
6.1.2 什么是垃圾?
6.1.3 为什么需要 GC?
6.1.4 早期垃圾回收
6.1.5Java 垃圾回收机制
6.2 垃圾回收相关算法
6.2.1 垃圾标记阶段算法
6.2.2 垃圾回收阶段算法
6.3 垃圾回收相关概念
6.3.1System.gc() 的理解
6.3.2 内存溢出与内存泄漏
6.3.3Stop the World
6.4 垃圾回收器
6.4.1 垃圾回收器概述
6.4.2 垃圾回收器分类
6.4.3GC 性能指标
6.4.4HotSpot 垃圾收集器
6. 4.5CMS 回收器
6.4.6G1(Garbage First)回收器
6.4.7 查看 JVM 垃圾回收器设置垃圾回收器
一、JVM概述
1、1为什么要学习JVM
中高程序员必备技能:
项目管理、性能调优
1、2虚拟机
- 虚拟机(Virtual Machine),虚拟计算机。他是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
- VMware属于系统虚拟机,完全对物理计算机的仿真,提供一个可运行完整操作系统的平台。程序虚拟机的典型代表是java虚拟机,专门为执行某个计算机程序而设计。
在java虚拟机中执行的指令称为java字节码指令。 - Java虚拟机是一种执行java字节码文件的虚拟机,它拥有独立的运行机制。
- Java技术的核心就是Java虚拟机,因为所有的java程序都要在java虚拟机内部运行。
1、3JVM作用
Java虚拟机负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,每一条java指令,java虚拟机中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪。
特点:
- 一次编译到处运行;
- 自动内存管理;
- 自动垃圾回收功能;
现在的JVM不仅可以执行java字节码文件,还可以执行其他语言编译后的字节码文件,是一个跨语言平台。
1、4JVM整体组成部分
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
程序在执行之前先要把 java 代码转换成字节码(class 文件),jvm 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中的运行时数据区(Runtime Data Area) ,而字节码文件是 jvm 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU 去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native
Interface) 来实现整个程序的功能,这就是这 4 个主要组成部分的职责与功能。而我们通常所说的 JVM 组成指的是 运行时数据区(Runtime Data Area) ,因为通常需要程序员调试分析的区域就是“运行时数据区”,或者更具体的来说就是“运行时数据区”里面的 Heap(堆)模块。
二、JVM结构--类加载器
2、1类加载子系统
类加载子系统负责从文件系统或者网络中加载class文件。classLoader只负责class文件的加载,至于它是否可以裕兴,则由Execution Engine决定。
加载的类信息存放于一块称为方法区的内存空间。
class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载 JVM 当中来,根据这个模板实例化出 n 个实例.
class file 加载到 JVM 中,被称为 DNA 元数据模板. 此过程就要有一个运输工具(类加载器 Class Loader),扮演一个快递员的角色
2、2类加载过程
2、2、1加载
- 通过类名(地址)获取此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构。
- 在内存中生成一个代表这个类的java.lang.class对象,作为这个类的各种数据的访问入口。
2、2、2链接
- 验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致;
验证文件格式是否一致: class 文件在文件开头有特定的文件标识(字节码文件都以 CA FE BA BE 标识开头);主,次版本号是否在当前 java 虚拟机接收范围内.
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java 语言规范的要求,例如这个类是否有父类;是否继承浏览不允许被继承的类(final 修饰的类)..... -
准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;
不包含用 final 修饰的 static 常量,在编译时进行初始化.
例如: public static int value = 123;value 在准备阶段后的初始值是 0,而不是 123. - 解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是 Class 文件的逻辑符号,直接引用指向的方法区中某一个地址)
2、2、3初始化
- 通过 new 关键字创建对象
- 访问类的静态变量,包括读取和更新
- 访问类的静态方法
- 对某个类进行反射操作
- 初始化子类会导致父类的的初始化
- 执行该类的 main 函数
除了以上几种主动使用,以下情况被动使用,不会加载类:
-
引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致类加载,比如:
public final static int NUMBER = 5 ; //不会导致类初始化,被动使用
public final static int RANDOM = new Random().nextInt() ; //会导致类加载 -
构造某个类的数组时不会导致该类的初始化,比如:
Student[] students = new Student[10] ;
类的初始化顺序
对static修饰的变量或语句块进行赋值
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
顺序是:父类 static –> 子类 static
public class ClassInit{
static{
num = 20;
}
static int num = 10;
public static void main (String[] args) {
//num从准备到初始化值变化过程 num=0 -> num=20 -> num=10
System.out.println(num);//10
}
}
public class ClassInit{
static int num = 10;
static{
num = 20;
}
public static void main (String[] args) {
//num从准备到初始化值变化过程 num=0 -> num=10 -> num=20
System.out.println(num);//20
}
}
2、3类加载器分类
站在JVM的角度看,类加载器可以分为两种:
- 引导类加载器(启动类加载器 Bootstrap ClassLoader).
- 其他所有类加载器,这些类加载器由 java 语言实现,独立存在于虚拟机外部,并 且全部继承自抽象类 java.lang.ClassLoader.
2.3.1 引导类加载器(启动类加载器 BootStrap ClassLoader)
2.3.2 扩展类加载器(Extension ClassLoader)
2.3.3 应用程序类加载器(系统类加载器 Application ClassLoader)
2.4 双亲委派机制
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请 求委托给父类的加载器去执行.
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终 将到达顶层的启动类加载器.
- 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完 成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制.
- 如果均加载失败,就会抛出 ClassNotFoundException 异常。
双亲委派优点:
- 安全,可避免用户自己编写的类替换 Java 的核心类,如 java.lang.String.
- 避免类重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次
2.5 如何打破双亲委派机制
三、JVM运行时数据区
3、1运行时数据区组成概述
3.1.1 程序计数器(Program Counter Register)
3.1.2Java 虚拟机栈(Java Virtual Machine Stacks)
3.1.3 本地方法栈(Native Method Stack)
3.1.4Java 堆(Java Heap)
3.1.5 方法区(Methed Area)
如图: 红色的为多个线程共享,灰色的为单个线程私有的,即 线程间共享:堆,方法区. 线程私有:程序计数器,栈,本地方法栈.
3.2.程序计数器(Program Counter Register)
- 它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域.
- 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致.
- 程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址.
- 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成.
- 它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
3.3.Java 虚拟机栈(Java Virtual Machine Stacks)
栈中会出现异常,当线程请求的栈深度大于虚拟机所允许的深度时 , 会出现StackOverflowError.
- JVM 直接对 java 栈的操作只有两个,就是对栈帧的入栈和出栈,遵循先进后出/后进先出的原则.
- 在一条活动的线程中,一个时间点上,只会有一个活动栈.即只有当前在执行的方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈帧对应的方法称为当前方法(CurrentMethod),定义这个方法的类称为当前类(Current Class).
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作.
- 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧.
不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法).
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧.
Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常.不管哪种方式,都会导致栈帧被弹出.
-
局部变量表(Local Variables)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。 -
操作数栈(Operand Stack)(或表达式栈)
栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。 -
动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。 -
方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
3.4.本地方法栈(Native Method Stack)
- Java 虚拟机栈管理 java 方法的调用,而本地方法栈用于管理本地方法的调用.
- 本地方法栈也是线程私有的.
- 允许被实现成固定或者是可动态扩展的内存大小.内存溢出方面也是相同的.
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出 StackOverflowError.本地方法是用 C 语言写的.
- 它的具体做法是在 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库.
3.5.Java 堆内存
3.5.1Java堆内存概述
- 一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域.
- Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理的最大一块内存空间.
-
堆内存的大小是可以调节.
例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)
一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率. - 《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的.
- 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.
- 《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分配在堆上.
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.
- 堆是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.
3.5.2堆内存区域划分
3.5.3 为什么分区(代)?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
3.5.4 对象创建内存分配过程
- new 的新对象先放到伊甸园区,此区大小有限制.
- 当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.
- 然后将伊甸园区中的剩余对象移动到幸存者 0 区.
- 如果再次出发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收,就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.
- 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区.
-
什么时候去养老区呢?默认是 15 次,也可以设置参数,最大值为 15
-XX:MaxTenuringThreshold=<N>
在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111,即为15。所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。 - 在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理.
- 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space
例如:
public static void main(String[] args) {
List<Integer> list = new ArrayList();
while(true){
list.add(new Random().nextInt());
}
}
3.5.5 新生区与老年区配置比例
- 默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
- 可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
- 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
- 在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 :1,当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如-XX:SurvivorRatio=8,新生区的对象默认生命周期超过 15 ,就会去养老区养老
3.5.6 分代收集思想 Minor GC、Major GC、Full GC
3.5.7 堆空间的参数设置
-XX:+PrintFlagsInitial
|
查看所有参数的默认初始值
|
-XX:+PrintFlagsFinal
|
查看所有参数的最终值(修改后的值)
|
-Xms
|
初始堆空间内存(默认为物理内存的 1/64)
|
-Xmx
|
最大堆空间内存(默认为物理内存的 1/4)
|
-Xmn
|
设置新生代的大小(初始值及最大值)
|
-XX:NewRatio
|
配置新生代与老年代在堆结构的占比
|
-XX:SurvivorRatio
|
设置新生代中 Eden 和 S0/S1 空间比例
|
-XX:MaxTenuringTreshold
|
设置新生代垃圾的最大年龄
|
XX:+PrintGCDetails
|
输出详细的 GC 处理日志
|
3.5.8 字符串常量池
public static void main(String[] args) {
String temp = "world";
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = temp + temp;
temp = str;
str.intern();//将字符串存储到字符串常量池中
}
}
3.6方法区
3.6.1 方法区的基本理解
方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field 等元数据、static final 常量、static 变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。
方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的.
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展.
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误
关闭 JVM 就会释放这个区域的内存.
3.6.2 方法区大小设置
Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.
- 元数据区大小可以使用参数-XX:MetaspaceSize 和 -XX:MaxMataspaceSize 指定,替代上述原有的两个参数.
- 默认值依赖于平台,windows 下,-XXMetaspaceSize 是 21MB,
- -XX:MaxMetaspaceSize 的值是-1,级没有限制.
- 这个-XX:MetaspaceSize 初始值是 21M 也称为高水位线 一旦触及就会触发 Full GC.
- 因此为了减少 FullGC 那么这个-XX:MetaspaceSize 可以设置一个较高的值
3.6.3 方法区的内部结构
javap -v -p Demo.class > test.txt
3.6.4 方法区的垃圾回收
- 有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。
- 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。
方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
四、本地方法接口
4.1 什么是本地方法
4.2 为什么要使用 Native Method
-
与 java 环境外交互。
有时 java 应用需要与 java 外面的环境交互,这是本地方法存在的主要原因。 你可以想想 java 需要与一些底层系统,如某些硬件交换信息时的情况。本地方法正式这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 java 应用之外的繁琐细节。 -
Sun 的解释器是用 C 实现的,这使得它能像一些普通的 C 一样与外部交互。
jre大部分是用 java 实现的,它也通过一些本地方法与外界交互。例如:类 java.lang.Thread 的 setPriority()方法是用 Java 实现的,但是它实现调用的是该类里的本地方法 setPriority0()。
五、执行引擎
5.1 概述
- 执行引擎是 Java 虚拟机核心的组成部分之一。
- JVM 的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被 JVM 所识别的字节码指令、符号表,以及其他辅助信息。
- 那么,如果想要让一个 Java 程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。
- 前端编译:从 Java 程序员-字节码文件的这个过程叫前端编译.
- 执行引擎这里有两种行为:一种是解释执行,一种是编译执行(这里的是后端编译)。
5.2 什么是解释器?什么是 JIT 编译器?
5.3 为什么 Java 是半编译半解释型语言?
- 当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行。
- 编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取一个平衡点。
六、垃圾回收
6.1 垃圾回收概述
6.1.1 概述
- Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要程序员手动的收集。
- 垃圾收集,不是 Java 语言的伴生产物。早在 1960 年,第一门开始使用内存动态分配和垃圾收集技术的 Lisp 语言诞生。
-
关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?
如何回收? - 垃圾收集机制是 Java 的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java 的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战。
6.1.2 什么是垃圾?
- 垃圾是指在运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾。
- 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
6.1.3 为什么需要 GC?
- 对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
- 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。
6.1.4 早期垃圾回收
MibBridge *pBridge= new cmBaseGroupBridge();
//如果注册失败,使用 Delete 释放该对象所占内存区域
if(pBridge->Register(kDestroy)!=NO ERROR)
delete pBridge;
MibBridge *pBridge=new cmBaseGroupBridge();
pBridge->Register(kDestroy);
6.1.5Java 垃圾回收机制
6.2 垃圾回收相关算法
6.2.1 垃圾标记阶段算法
- 在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是有用对象,哪些是垃圾对象。只有被标记为己经是垃圾对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
- 那么在 JVM 中究竟是如何标记一个垃圾对象呢?简单来说,当一个对象已经不再被任何引用指向时,就可以宣判为垃圾对象。
- 判断对象是否为垃圾对象一般有两种方式:引用计数算法和可达性分析算法。
- 引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
- 对于一个对象 A,只要有任何一个引用指向了对象 A,则对象 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,即表示对象 A 不可能再被使用,可进行回收。
- 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
-
缺点:
1.它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3.引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在.Java 的垃圾回收器中没有使用这类算法。
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
- 相较于引用计数算法,这里的可达性分析就是 Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)
-
虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。 -
方法区中类静态属性引用的对象
比如:Java 类的引用类型静态变量 - 所有被同步锁 synchronized 持有的对象
-
Java 虚拟机内部的引用。
基 本 数 据 类 型 对 应 的 Class 对 象 , 一 些 常 驻 的 异 常 对 象 ( 如 : NullPointerException、OutofMemoryError),系统类加载器。
protected void finalize() throws Throwable { }
- 在 finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize()方法将没有执行机会。
- 一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。
6.2.1.5 生存还是死亡?
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在 finalize()中复活。
- 不可触及的:对象的 finalize()被调用,并且没有复活,那么就会进入不可触及状态。
判定一个对象 objA 是否可回收,至少要经历两次标记过程:
- 如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。
- 进行筛选,判断此对象是否有必要执行 finalize()方法:
- 如果对象 objA 没有重写 finalize()方法,或者 finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA 被判定为不可触及的。
- 如果对象 objA 重写了 finalize()方法,且还未执行过,那么 objA 会被插入到队列中,由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其finalize()方法执行。
- finalize()方法是对象逃脱死亡的最后机会,稍后 GC 会对队列中的对象进行第二次标记。如果 objA 在 finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA 会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态。
6.2.2 垃圾回收阶段算法
当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是:
- 标记-复制算法(Copying)
- 标记-清除算法(Mark-Sweep)
- 标记-压缩算法(Mark-Compact)
6.2.2.1 标记-复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。在垃 圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除 正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
复制算法的优缺点
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销也不小.
复制算法的应用场景
- 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,效率较高
- 老年代大量的对象存活,那么复制的对象将会有很多,效率会很低
- 在新生代,对常规应用的垃圾回收,一次通常可以回收 70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
6.2.2.2 标记-清除算法
执行过程
当堆中的有效内存空间被耗尽的时候,然后进行这项工作.
清除:这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放(也就是覆盖原有的地址)
标记-清除算法的优点:
非常基础和常见的垃圾收集算法容易理解
标记-清除算法的缺点:
标记清除算法的效率不算高
这种方式清理出来的空闲内存是不连续的,产生内碎片。
6.2.2.3 标记-压缩算法
背景
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。 如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以 JVM 的设计者需要在此基础之上进行改进
标记压缩算法执行过程
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记-压缩算法与标记-清除算法的比较
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
二者的本质差异在于标记-清除算法是一种非移动式的回收算法(空闲列表记录位置),标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标记-压缩算法的优缺点
优点
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
- 从效率上来说,标记-压缩算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。即:STW
6.2.2.4 垃圾回收算法小结
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。而为了尽量兼顾上面提到的三个指标,标记-压缩算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
标记清除 | 标记整理 | 复制 | |
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(会堆积碎片) | 少(无堆积碎片) | 通常需要活动对象的两倍空间(无堆积碎片) |
移动对象 | 否 | 是 | 是 |
6.2.2.5 分代收集
为什么要使用分代收集
前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集应运而生。
分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在 Java 程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关: 比如 Http 请求中的 Session 对象、线程、Socket 连接,这类对象跟业务直接挂钩,因此生命周期比较长。 但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String 对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
目前几乎所有的 GC 都采用分代手机算法执行垃圾回收的在 HotSpot 中,基于分代的概念,GC 所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过 hotspot 中的两个 survivor 的设计得到缓解。
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记- 清除或者是标记-清除与标记-压缩的混合实现。
- Mark 阶段的开销与存活对象的数量成正比。
- Sweep 阶段的开销与所管理区域的大小成正相关。
- Compact 阶段的开销与存活对象的数据成正比。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
6.3 垃圾回收相关概念
6.3.1System.gc() 的理解
在默认情况下,通过 System.gc()者 Runtime.getRuntime().gc() 的调用,会显式触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
然而 System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)。
JVM 实现者可以通过 System.gc() 调用来决定 JVM 的 GC 行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,我们可以在运行之间调用 System.gc()。
6.3.2 内存溢出与内存泄漏
内存溢出
内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
由于 GC 一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现 OOM 的情况。
大多数情况下,GC 会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存,供应用程序继续使用。
Javadoc 中对 OutofMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
内存泄漏
内存泄漏也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,也可以叫做宽泛意义上的“内存泄漏”。
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutofMemory 异常,导致程序崩溃。注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
常见例子
单例模式
单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
一些提供 close()的资源未关闭导致内存泄漏
数据库连接 dataSourse.getConnection(),网络连接 socket 和 io 连接必须手动 close,否则是不能被回收的。
6.3.3Stop the World
Stop-the-World,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。
可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿,为什么需要停顿所有 Java 执行线程呢?
- 分析工作必须在一个能确保一致性的快照中进行
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
- 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证,会出现漏标,错标问题
- 被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少 STW 的发生。
- 越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
6.4 垃圾回收器
6.4.1 垃圾回收器概述
如果说垃圾收集算法是内存回收的方法论,那么收集器就是内存回收的实践者.
垃圾收集器没有在 java 虚拟机规范中进行过多的规定,可以由不同的厂商、不同版本的 JVM 来实现。
由于 JDK 的版本处于高速迭代过程中,因此 Java 发展至今已经衍生了众多的垃圾回收器。从不同角度分析垃圾收集器,可以将 GC 分为不同的类型。
实际使用时,可以根据实际的使用场景选择不同的垃圾回收器,这也是 JVM 调优的重要部
6.4.2 垃圾回收器分类
按线程数可以分为单线程(串行)垃圾回收器和多线程(并行)垃圾回收器
单线程垃圾回收器(Serial)
只有一个线程进行垃圾回收,使用于小型简单的使用场景,垃圾回收时,其他用户线程会暂停.
按照工作模式分,可以分为独占式和并发式垃圾回收器。
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
6.4.3GC 性能指标
- 垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
- 内存占用:Java 堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
6.4.4HotSpot 垃圾收集器
图中展示了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
6. 4.5CMS 回收器
CMS 概述
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
垃圾回收过程
初始标记:
Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
并发标记:
垃圾回收线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。
重新标记:
Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
并发清除:
只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。 这个过程非常耗时。
并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上 说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS 的优点:
可以作到并发收集
CMS 的弊端:
- CMS 是基于标记-清除算法来实现的,会产生内存碎片。
- CMS 在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
- CMS 收集器无法处理浮动垃圾(floating garbage)。
三色标记算法
为了提高 JVM 垃圾回收的性能,从 CMS 垃圾收集器开始,引入了并发标记的概念。引入并发标记的过程就会带来一个问题,在业务执行的过程中,会对现有的引用关系链出现改变。
- 黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了,例如GCRoots 对象。
- 灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC需要从此对象中去寻找垃圾);
- 白色:表示对象没有被垃圾收集器访问过,即表示不可达.
三色标记的过程:
为了解决并发的问题,引入中间状态(灰色),当一个对象被标记的时候,会有下面几个过程:
- 刚开始,确定为 GC Roots 的对象为黑色。
- 将 GC Roots 直接关联的对象置为灰色。
- 遍历灰色对象的所有引用,灰色对象本身置为黑色,其引用置为灰色。
- 重复步骤 3,直到没有灰色对象为止。
- 结束时,黑色对象存活,白色对象回收。
这个过程正确执行的前提是没有其他线程改变对象间的引用关系,然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况。
漏标
假设 GC 已经在遍历对象 B 了,而此时用户线程执行了 A.B=null 的操作,切断了 A 到 B 的引用
只要打破任一条件,就可以解决错标的问题。
原始快照和增量更新
原始快照打破的是第一个条件:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。
增量更新打破的是第二个条件:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象。
总结
CMS 为了让 GC 线程和用户线程一起工作,回收的算法和过程比以前旧的收集器要复杂很多。究其原因,就是因为 GC 标记对象的同时,用户线程还在修改对象的引用关系。因此 CMS 引入了三色算法,将对象标记为黑、灰、白三种颜色的对象,将用户线程修改的引用关系记录下来,以便在「重新标记」阶段可以修正对象的引用。
虽然 CMS 从来没有被 JDK 当做默认的垃圾收集器,存在很多的缺点,但是它开启了「GC 并发收集」的先河,为后面的收集器提供了思路。
6.4.6G1(Garbage First)回收器
既然我们已经有了前面几个强大的 GC,为什么还要发布 Garbage First(G1)GC?
原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC 就不能保证应用程序正常进行,而经常造成 STW 的 GC 又跟不上实际的需求,所以才会不断地尝试对 GC 进行优化。G1(Garbage-First)垃圾回收器是在 Java7 update 4 之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一.
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
官方给 G1 设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
G1 是一款面向服务端应用的垃圾收集器。
为什么名字叫做 Garbage First(G1)呢?
因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的逻辑上连续的)。使用不同的 Region 来表示 Eden、幸存者0 区,幸存者 1 区,老年代等。
G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region.
由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给 G1 一个名字:垃圾优先(Garbage First)。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核 CPU 及大容量内存的机器,以极高概率满足 GC 停顿时间的同时,还兼具高吞吐量的性能特征。
如下图所示,G1 收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和 CMS 收集器前几步的收集过程很相似:
- 初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。
- 并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
- 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。
- 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域.这就是 Garbage First 的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。
6.4.7 查看 JVM 垃圾回收器设置垃圾回收器
打印默认垃圾回收器
-XX:+PrintCommandLineFlags -version
JDK 8 默认的垃圾回收器
年轻代使用 Parallel Scavenge GC
老年代使用 Parallel Old GC
打印垃圾回收详细信息
-XX:+PrintGCDetails -version
设置默认垃圾回收器
Serial 回收器
-XX:+UseSerialGC 年轻代使用 Serial GC, 老年代使用 Serial Old GC
ParNew 回收器
-XX:+UseParNewGC 年轻代使用 ParNew GC,不影响老年代。
CMS 回收器文章来源:https://www.toymoban.com/news/detail-770039.html
-XX:+UseConcMarkSweepGC 老年代使用 CMS GC。
# G1 回收器 文章来源地址https://www.toymoban.com/news/detail-770039.html
-XX:+UseG1GC 手动指定使用 G1 收集器执行内存回收任务。
-XX:G1HeapRegionSize 设置每个 Region 的大小。
到了这里,关于JVM(Java虚拟机)-史上最全、最详细JVM笔记的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!