JVM内存结构和垃圾回收

JVM

整体结构

在这里插入图片描述

在这里插入图片描述

java代码执行流程

在这里插入图片描述

存在二次编译

第一次是将java源码编译成字节码(非常严格的编译标准)

第二次是将字节码文件通过解释和JIT编译成机器指令 -->执行引擎的重要性就在此

类加载子系统

在这里插入图片描述

类加载子系统负责加载class文件,class文件在文件开头有特定的文件标识

ClassLoader只负责class的加载,能否运行由ExecutionEngine(执行引擎)决定

加载的类信息存放在方法区(元空间)

加载阶段

(1)通过一个类的全限定类名获取定义此类的二进制字节流

(2)将这个字节流代表的静态存储结构转化为方法区(元空间)的运行时数据结构

(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

链接阶段

验证

目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全

主要于(1)文件格式验证(2)元数据验证(3)字节码验证(4)符号引用验证

准备

类变量(不包含final修饰的static字段)分配内存并设置该类变量的默认初始值,即零值

private static int a =1
在准备阶段,a=0
在初始化阶段,a=1
解析

将常量池内的符号引用转换为直接引用的过程

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等

初始化阶段

初始化就是执行类构造器方法()的过程

此方法不需定义,是javac编译器自动收集类中静态类变量的赋值动作和静态代码块中的语句合并而来

若该类存在父类,JVM会保证子类的()执行前先执行父类的()

虚拟机必须保证一个类的()方法在多线程下被同步加锁(即类只被初始化一次,将类信息会保存在方法区(直接内存)做缓存)

类加载器

在这里插入图片描述

在这里插入图片描述

(1)引导类加载器(Bootstrap Class Loader)

使用C/C++语言实现的,嵌套在JVM内部

用来加载Java的核心类库,用于提供JVM自身需要的类

并不继承java.lang.ClassLoader,没有父加载器

加载 扩展类加载器和系统类加载器,并指定为他们的父加载器

(2)扩展类加载器(Extension Class Loadr)

Java语言编写,派生于ClassLoader类

从java.ext.dirs系统属性指定目录中加载类库,或从JDK安装目录下jre/lib/ext子目录(扩展目录)下加载类库

如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载

(3)系统类加载器(System Class Loadr)

Java语言编写,派生于ClassLoader类

负责加载环境变量classpath或系统属性java.class.path指定下的类库

该类加载是程序中默认的类加载器

(4)自定义加载器(User Defined Class Loader)

双亲委派机制

在这里插入图片描述

(1)一个类加载器收到类加载请求时,会先将这个请求委托给父类加载器去执行

(2)如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加载器

(3)如果父类加载器可以完成加载,就成功返回。否则由子类加载器尝试加载

双亲委派机制避免类的重复加载,保护程序安全,防止核心API被随意篡改

其他

JVM中判断两个class对象是否为同一个对象的必要条件:

(1)类的完整类名必须一致,包括包名

(2)加载这个类的ClassLoader必须相同

JVM必须知道一个类型是由启动加载器还是由用户加载器加载的

Java程序对类的使用方式:主动使用、被动使用

主动使用情况:

(1)创建类的实例

(2)访问某个类或接口的静态变量或对该静态变量赋值

(3)调用某个类的静态方法

(4)反射使用对象(Class.forName(“com.travelsky.bean”))

(5)初始化一个类的子类

(6)JVM启动时被表明为启动类的类

(7)JDK7提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果

除开以上情况,都看做是对类的被动使用,都不会导致类的初始化

运行时数据区

在这里插入图片描述

红色区域是进程(多个线程)共享的

灰色区域是每个线程私有的

例如:一个进程有5个线程 则存在5个程序计数器和栈 5个线程共用一个方法区和堆

内存是硬盘与CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时进行

在这里插入图片描述

每个JVM只有一个Runtime实例,即为运行时环境

线程

线程是一个程序里运行单元,JVM允许一个应用有多个线程并行执行

JVM里每个线程都与操作系统的本地线程直接映射。当一个Java线程准备好执行后,此时操作系统一个本地线程也同时创建,初始化成功后会调用Java线程中的run()方法。当Java线程执行终止后,本地线程也会回收

程序计数器(PC寄存器)

JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟

PC寄存器用来存储指向下一条指令(即将执行的代码指令)的地址,由执行引擎读取下一条指令

在这里插入图片描述

是一块很小的内存空间,也是运行速度最快的存储区域

每个线程都有它自己的程序计数器,是线程私有的,声明周期与线程一致

任何时间一个线程只有一个方法在执行(当前方法),程序计数器会存储当前线程正在执行的Java方法的JVM指令地址

PC寄存器是JVM中唯一一个不会发生OOM情况的区域

在字节码文件中:

在这里插入图片描述

使用PC寄存器存储当前线程执行地址的原因:JVM的字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

PC寄存器设定为线程私有原因:线程执行时需要抢占时间片,线程间是并发执行的,为了保证每个线程正在执行的当前字节码指令地址不出现相互干扰

在这里插入图片描述

虚拟机栈

栈是运行时的单位,堆是存储的单位

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应一次次的Java方法调用。是线程私有的

JVM对栈的操作:

(1)每个方法执行,伴随着进栈(入栈、压栈)

(2)执行结束后的出栈工作

栈不存在垃圾回收问题

使用**-Xss**参数来设置线程的最大栈空间(默认1024KB),栈的大小直接决定了函数调用的最大深度

线程上正在执行的每个方法都各自对用一个栈帧

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

栈帧内部结构

在这里插入图片描述

局部变量表

定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量

局部变量表是建立在线程的栈上,是线程私有数据,不存在数据安全问题

局部变量表所需的容量大小是在编译期确定下来的,在方法运行期间不会改变大小

局部变量表基本存储单元是Slot(变量槽)

在局部变量表里,32位以内的类型只占用一个slot(包括returmAddress类型),64位类型(long、double)占用两个slot

当前帧是由构造方法或实例方法创建,那么该对象引用this将会存放在index为0的slot处

栈帧中的局部变量表中的槽位是可以重复使用的(一个局部变量过了作用域,在作用域后声明的新的局部变量会复用该槽位)

成员变量:在使用前,都经历默认初始化赋值

​ (1)类变量:Linking的prepare阶段,给类变量默认赋值 --> initial阶段给类变量显示赋值

​ (2)实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值

局部变量:在使用前,必须要进行显示赋值!否则编译不通过

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

操作数栈

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/ 出栈(pop)

主要用于保存计算结果的中间结果,同时作为计算过程中变量临时的存储空间

当一个方法刚开始执行时,一个新的栈帧随之被创建,该方法的操作数栈是空的

每一个操作数栈在编译期就明确了存储数值的深度

操作数栈并非采用访问索引的方式来进行数据访问

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

栈顶缓存:将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

动态链接

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中

动态链接:每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用

动态链接作用就是为了将这些符号引用转换为调用方法的直接引用

在这里插入图片描述

方法返回地址

存放调用该方法的PC寄存器的值

方法的调用

在JVM中,将符号引用转换为调用方法的直接引用,如果是在编译期确定下来的就是静态链接,如果是在运行期确定下来 的就是动态链接

方法绑定机制:一个字段、方法或者类在符号引用被替换成直接引用的过程,仅仅只发生一次。静态链接对应早期绑定,动态链接对用晚期绑定

非虚方法:指方法在编译期就确定了具体调用且在运行时不可变。静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法

本地方法栈

Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用

本地方法栈也是线程私有的

本地方法接口

一个native修饰的方法就是一个Java调用非java代码的接口

本地接口的作用是融合不同的编程语言为Java所用

在定义一个native method时,并不提供实现体(就像定义interface),因为实现体由非java语言在外面进行实现

核心概念

一个JVM实例只存在一个堆内存(大小是可调节的)

堆可以处于物理上不连续的内存空间,但在逻辑上应该被视为连续的

所有线程共享Java堆,为了提高并发性和保证数据安全,在堆上可以划分每个线程私有的缓冲区(TLAB)

几乎所有的对象实例以及数组都在堆上进行内存分配(可能逃逸分析后对象保存在栈上)

栈帧中保存的引用是用来指向对象实例或者数组在堆中的位置

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾回收的时候才会被移除

堆是GC执行垃圾回收的重点区域

内存细分

通过Java VisualVM,在插件找中安装Visual GC可以看见各个区域内存大小

在这里插入图片描述

JDK7

在这里插入图片描述

JDK8:年轻代 + 老年代 + 元空间

Java堆区用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了

-Xms用来设置堆空间(年轻代+老年代)的起始内存大小

-Xmx用来表示堆空间(年轻代+老年代)的最大内存

Java堆区:年轻代(YoungGen)和老年代(OldGen)

在这里插入图片描述

默认情况下爱,年轻代与老年代占比为1:2,年轻代中eden与survior占比为8:1:1

其实在运行时想达到8:1:1,需要显示设置vm参数**-XX:SurvivorRatio=8**

在这里插入图片描述

几乎所有的Java对象都是在Eden区被new出来的

对象分配过程

(1)new对象先存放在Eden区

(2)在Eden区内存满了后,触发垃圾回收(YGC/Minor GC),此时会将Eden和Survivor区同时进行垃圾回收

(3)将Eden剩余对象移动到Survivor中(此时Eden区没对象),并记录age(是用来判断进入Old区的标记值)

(4)多次垃圾回收,Survivor0和Survivor1两者中来回交换对象并将对象age加1,需保证Survivor区有一个为空(为了GC时进行进行复制清除算法),当Survivor区中对象的age达到阈值(默认15),则进入Old

(5)在Old区存在不足时,会触发Major GC

(6)当Old垃圾回收后仍然无法进行对象保存,则产生OOM异常

注意:

1、Survivor区不会发生Minor GC,是在Eden触发Minor GC时同时进行Survivor的垃圾回收

2、Survivor0和Survivor1复制之后有交换,谁空谁是to(to表示下一次Eden幸存对象保存位置)

3、垃圾回收频繁在新生代(Eden)收集,很少在老年代(Old)收集,几乎不在永久区/元空间收集

特殊情况:

1、超大对象(超过Eden区大小),则判断Old区是否能保存。不能保存的话会在Old先进行FGC,回收后还放不下就OOM

2、在进行YGC后,对象超过Survivor区大小则直接晋升Old

内存分配策略

在这里插入图片描述

TLAB

(Thread Local Allocation Buffer)

由于堆区是线程共享区域,任何线程都可以访问到堆区的共享数据,会存在线程安全问题(加锁的话会影响分配速度)

JVM为每个线程分配了一个私有缓存区域(TLAB),包含在Eden区

在这里插入图片描述

默认情况下,TLAB内存仅占Eden的1%,但JVM是将TLAB作为内存分配的首选

逃逸分析

如果经过逃逸分析(Escape Analysis)发现一个new的对象实体没有逃逸出方法,那么可能被优化成栈上分配

逃逸分析的基本行为就是分析对象动态作用域:

(1)对象在方法中定义后,只在方法内部使用,则没有发生逃逸

(2)对象在方法中定义后,被外部方法引用(如作为方法返回值),则发生逃逸

//sb对象作为返回值被外部方法引用,任务发生了逃逸
public static StringBuffer create(String S1,String S2){
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	return sb;
}
//改进后对象没有发生逃逸,则可以优化成栈上分配空间
public static String create(String S1,String S2){
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	return sb.toString();
}
代码优化

(1)栈上分配

(2)同步省略(消除)

public void fun(){
	Object obj = new Object();
	synchronized(obj){
		syso(obj);
	}
}
//对obj加锁,但obj生命周期只在方法内,不会发生逃逸,则在JIT编译期优化成以下
public void fun(){
	Object obj = new Object();
	syso(obj);
}

(3)标量替换

标量:是指一个无法再分解成更小数据的数据,如Java基本数据类型

聚合量:可以再分解的数据称为聚合量,如Java中对象

class Point(){
	private int x;
	private int y;
}
public void fun(){
	Point point = new Point(1,2);
	syso(point.x + ","+point.y)
}
//进过逃逸分析发现,point对象不会被外界访问,在JIT编译器把这个对象分解成其成员变量来代替(没有对象则不分配内存)
public void fun(){
	int x = 1;
	int y = 2
	syso(x + ","+y)
}

方法区

方法区(Method Area)看做是一块独立于Java堆的内存空间(别名:Non-Heap非堆)

方法区用来加载类信息

方法区和Java堆一样是各个线程共享的内存区域

方法区在JVM启动的时候被创建,并且其实际物理内存空间和Java堆一样都可以不连续

在这里插入图片描述

元空间与永久代区别:元空间不在虚拟机设置的内存中,而是使用本地内存

JDK7:

-XX:PermSize 设置初始化永久代空间大小

-XX:MaxPerSize 设置永久代最大空间大小

JDK8:

-XX:MetaspaceSize 初始化元空间大小(建议设置为相对较高的值)

-XX:MaxMetaspaceSize 最大元空间大小(一般为-1,表示没有限制)

在JDK8中,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
在这里插入图片描述

永久代被元空间替代的原因:

(1)为永久代设置空间大小是很难确定的

(2)对永久代进行调优是很困难的

内部结构

方法区用于存储已被虚拟机加载的类型信息、常量、静态信息、即时编译器(JIT)编译后的代码缓存

类型信息:类的完整名称、父类或接口的完整名称、修饰符、方法信息(返回类型、参数列表、修饰符、字节码、操作数栈和局部变量表的大小、异常表)

方法区保存类信息时会同时保存每个类的classLoader,且classLoader也会记录都加载了哪些类

static修饰的静态变量被类的所有实例共享,即时没有类实例时也能访问

运行时常量池

字节码文件中内部包含了常量池(Constant Pool)

常量池中存储的数据类型:

(1)数量值

(2)字符串值

(3)类引用

(4)字段引用

(5)方法引用

常量池可以看做一张表,虚拟机指令根据常量表找到要执行的类名、方法名、参数类型、字面量等类型

方法区内部包含了运行时常量池(Runtime Constant Pool)

常量池是Class文件的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

字节码文件中的常量池经过类加载器放入方法区后就称为运行时常量池

运行时常量池相较于class文件中常量池是具有动态性

对象实例化

创建对象的方式

在这里插入图片描述

创建对象的步骤

在这里插入图片描述

1、虚拟机遇见new指令,会先判断Metaspace的常量池汇总是否存在类的符号引用,若没有,则通过类加载器进行类加载并生成Class类对象

2、计算对象占用空间大小,在堆中划分一块内存给新对象

4、对象的默认初始化(零值初始化)

5、将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中

6、对象的显式初始化

对象内存布局

在这里插入图片描述

通过代码解释:

public class CustomerTest{
	public static void main(String[] args){
		Customer customer = new Customer();
	}
}
public class Customer{
	int id = 1001;
	String name;
	Account acc;
	{
		name = "匿名客户";
	}
	public Customer(){
		acc = new Account();
	}
}
public class Account{
	......
}

在这里插入图片描述

对象访问定位

Hotspot采用直接指针

在这里插入图片描述

执行引擎

概述

虚拟机的执行引擎是由软件自行实现的,能够执行那些不被硬件直接支持的指令集格式

在这里插入图片描述

执行引擎(Execution Engine)任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以

在这里插入图片描述

(1)执行引擎获取PC寄存器中保存的字节码指令来执行

(2)执行引擎通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中类型指针定位到目标对象的类型信息

Java代码编译和执行

在这里插入图片描述

橙色:javac编译器(前端编译器)

Java被称为半编译、半解释语言(绿色解释 蓝色编译)

解释器:JVM启动时对字节码采用逐行解释的方式执行

JIT编译器:JVM将源代码直接编译成和本地机器平台相关的机器语言

JVM运行方式:

当JVM启动时,解释器可以首先发挥作用,而不必等待即时编译期(JIT)全部编译完成后再执行,节省编译时间。随着时间推移,编译器发生作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更快程序执行效率

String

String声明为final ,不可被继承,实现了Serializable接口(支持序列化),实现了Comparable接口(支持排序比较)

jdk8:定义final char[ ] value来保存字符串数据

jdk9:改为byte[ ](节约空间)

String是代表不可变的字符序列(不可变性)

通过字面量的方式(区别于new)给字符串赋值,此时的字符串值声明在字符串常量池中

字符串常量池中是不会存储相同内容的字符串的

在jdk7之后,字符串常量池保存在堆中。调整原因①永久代permSize默认比较小②永久代垃圾回收频率低

字符串拼接操作

(1)常量与常量的拼接结果在常量池,原理是编译期优化

String s1 = "a" + "b" + "c";//等同于"abc"
String s2 = "abc"
sout(s1==s2)//true

(2)只要其中一个是变量,其结果就在堆中。变量拼接原理就是StringBuilder

//相当于在堆空间中new String(),其具体内容为拼接的结果
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + "b"
sout(s3==s4)//false

(3)一个字符串调用intern()方法,则判断字符串常量池中是否存在当前字符串值,若没有,则在字符串常量池中加载一份当前字符串值并返回地址

String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + "b"
String s5 = s4.intern();//s4此时值为"ab",调用intern()方法时发现字符串常量池中已存在该值(s3)
sout(s3==s5)//true

intern()使用

如果不是用双引号声明的String对象,可以使用String提供的intern()方法:从常量池中查询当前字符串是否存在,若不存在就将当前字符串放入常量池中

//问题1:new String("ab")会创建几个对象?
2个  对象1new关键字在堆空间中创建的
	 对象2:字符串常量池中的对象(通过字节码指令中 ldc <ab>//问题2:new String("a") + new String("b")会创建几个对象?
6个  对象1new StringBuilder()   用于拼接
	对象2new String()
    对象3:常量池中的"a"
    对象4new String()
    对象5:常量池中的"b"
深入剖析:  StringBuildertoString()
    对象6new String()
    注意:toString()方法调用,此时在字符串常量池中没有生成"ab"

关于intern()面试题:

main(String[] args){
	String s1 = new String("1");//new String()时在常量池中已经生成了"1"
	s1.intern();
	String s2 = "1";
	sout(s1 == s2)//false
    //此时s1为堆空间对象,s2为字符串常量池对象    故不相等
        
    String s3 = new String("1") + new String("1");//s3变量地址为new String("11"),但此时字符串常量池中没有"11"
    s3.intern();/**
    	调用intern()方法在常量池中生成"11" 
    	但是由于jdk版本中字符串常量池保存地址存在差异
    	jdk6:常量池在方法区,则在常量池中创建了一个新对象"11"
    	jdk7/8:常量池在堆中,前面s3已经在堆中创建了"11",此时为节省空间,不会创建新对象,而是创建一个指向堆空间中new String("11")的地址
    **/
    String s4 = "11";
    sout(s3 == s4)//jdk6:false     jdk7/8:true
    //jdk6:s3为堆空间对象,s为常量池对象   故不相等
    //jdk7/8:s3为堆空间对象,s4位常量池中保存的指向堆空间的地址    故相等
}

G1垃圾回收器可以实现堆上重复的String对象进行去重(-XX:UseStringDeduplication参数,需要手动开启)

垃圾回收

垃圾是指在运行程序中没有任何指针指向的对象

GC作用区域主要是方法区和堆

Java堆是垃圾收集器的工作重点

频繁收集Young区,较少收集Old区,基本不动Perm区(或元空间)

相关算法

标记阶段

在GC执行垃圾回收之前,首先需要区分出内存中的Java对象实例哪些是存活对象,哪些是已经死亡的对象

判断对象存活一般两种方式:引用计数法可达性分析算法

引用计数算法

引用计数算法是对每一个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况(对象A被引用则计数器加1,引用失效则计数器减1,当对象A计数器为0,可进行回收)

优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性

缺点:需要单独字段存储计数器增大内存开销;计数器的更新增大时间开销;无法处理循环引用(导致Java未使用该算法)

可达性分析算法

可达性分析算法是以根集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标是否可达,经过可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着(搜索走过的路径被称为引用链),如果目标对象没有任何引用链相连,则是不可达的,可以被标记为垃圾对象

可达性分析算法也称为根搜索算法

根集合(GC Roots):一组必须活跃的引用,包括以下几类元素:

1、虚拟机栈中引用的对象(例如各个线程被调用的方法中使用到的参数、局部变量等)

2、本地方法栈内引用到的对象

3、方法区中类静态属性引用的对象(例如Java类的引用类型静态变量)

4、方法区中常量引用的对象(例如字符串常量池的引用)

5、所有被同步锁synchronized持有的对象

6、Java虚拟机内部的引用(例如基本数据类型的Class对象、系统类加载器等)

另外,除了固定的GC Roots集合,根据当前垃圾收集器以及当前回收的内存区域不同(如分代收集和局部回收,只针对新生代进行垃圾回收时,非新生代对象也加入GC Roots集合中考虑),还可以有其他对象“临时性”加入集合

小技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针保存了堆内存里的对象,但自己又不存放在堆内存中,那它就是一个Root

注意:进行可达性分析算法判断内存是否可回收时,分析工作必须在保障一致性的快照中进行(Stop The World)

对象的finalization机制

Java提供对象终止(finalization)机制允许开发人员提供对象被销毁之前的自定义处理逻辑

垃圾回收某对象之前,总会调用该对象的finalize()方法,该方法允许被子类重写,通常用于对象被回收时进行资源释放

永远不要主动调用某个对象的finalize()方法,应该交由垃圾回收机制调用

由于finalize()方法存在,虚拟机中对象一般处于三种可能状态:

(1)可触及的:从跟节点(GC Roots)开始,可以到达这个对象

(2)可复活的:对象的所有引用都被释放,但是对象可能在finalize()方法中复活

(3)不可触及的:对象的finalize()方法被调用,并且该对象没有复活,则进入不可触及状态

finalize()方法只会被调用一次,只有在对象不可触及状态时才可以被回收

具体过程:

在这里插入图片描述

清除阶段
标记–清除(Mark-Sweep)算法

当堆中内存耗尽时,会停止整个程序(Stop The World),然后进行标记和清除

标记:Collectors从引用根节点(GC Roots)开始遍历,标记所有被引用对象,一般是在对象的Header中记录为可达对象

清除:Collectors对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收(此时清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次新对象加载时进行覆盖)

缺点:两次遍历;GC时停止整个应用程序;清理出来的内存会产生内存碎片,需要维护一个空闲列表用于新的对象分配

复制(Copying)算法

将或者的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中所有对象,交换两个内存角色,完成垃圾回收

优点:运行高效;不会出现内存碎片化问题

缺点:需要两倍内存空间;GC时进行的复制意味着GC需要维护对象之间(栈中本地变量表中对象引用与堆空间对象地址)的引用关系;

使用场景:特别适合垃圾对象很多,即存活对象很少的场景(如堆空间新生代中的Survivor0区Survivor1区

标记–压缩(Mark-Compact)算法

等同于标记–清除算法执行完成后,再进行一次内存碎片整理

缺点:效率低于复制算法;对象移动过程中,如果该对象被其他对象引用,则还需要调整其他对象引用的地址

分代收集

几乎所有的GC都是采用分代收集来执行垃圾回收的

1、年轻代:对象生命周期短、存活率低、回收频繁。采用复制算法

2、老年代:对象生命周期长、存活率高、回收不频繁。采用标记–清除或者标记–清除与标记–压缩混合使用

增量收集

让垃圾收集线程与应用程序线程交替执行(降低STW状态时间),垃圾收集线程收集一部分内存空间后,切换到应用程序线程执行,一次反复直到垃圾收集完成

增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

缺点:线程切换和上下文转换的消耗导致垃圾回收成本上升,造成系统吞吐量下降

分区收集

将整个堆空间划分成连续的不同小空间(region),每次合理回收若干个小空间,从而减少一次GC中STW状态时间

相关概念

显式gc

通过System.gc()方法会显式触发Full GC,同时对年轻代和老年代进行回收,尝试释放被丢弃对象占用的内存(但无法保证对垃圾回收器的调用)

System.runFinalization()方法会强制调用失去引用对象的finalize()方法

内存溢出(OOM)

没有空闲内存,并且垃圾回收器也无法提供更多内存

在抛出OOM异常前,通常垃圾回收器会被触发,尽可能去清理出空间

内存泄露

严格上说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露

宽泛上说,一些不太好的编码习惯导致对象的生命周期变得很长甚至OOM,也可以称为内存泄露(如局部变量提升成类变量)

举例:

(1)单例的生命周期和应用程序一样长,若单例程序中持有对外部对象的引用,那么该外部对象不能被回收,则内存泄漏

(2)数据库连接、网络连接的资源未关闭导致内存泄露

Stop The World

简称STW,指GC事件发生过程中,整个应用程序线程都会被暂停(保持GC时内存快照一致性),没有任何响应

STW是JVM在后台自动发起自动完成

并发与并行

并发:指多个事情在同一时间段内同时发生,多个任务之间互相抢占资源

并行:指多个事情在同一时间点上同时发生,多个任务之间不会抢占资源

只有在多CPU或一个CPU多核情况下才能发生并行

从垃圾收集器层面来看

(1)并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行,如CMS,G1

(2)并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态,如ParaNew、Parallel Scavenge、Parallel Old

安全点与安全区域

程序执行时只有在特定位置才能停顿下来开始GC,这些位置被称为安全点(SafePoint)

在安全点采用主动式中断,设置一个中断标志,各个线程运行到Safe Point时主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起

安全区域(Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何位置开始GC都是安全的(安全区域是为了避免某些线程处于Sleep或Block状态时无法处理中断请求)

Java引用

(1)强引用(Strong Reference)–不回收:代码中普遍存在的引用赋值(类似Object obj = new Object())。无论任何情况下,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象

强引用的对象是可触及状态,垃圾收集器永远不会回收掉被强引用的对象(即时抛出OOM也不回收)

强引用是造成Java内存泄露的主要原因之一

(2)软引用(Soft Reference)–内存不足即回收:在即将发生内存溢出前,把这些软引用关联对象列入回收范围内进行第二次回收,如果二次回收后仍然没有足够内存,才会抛出OOM

通常使用软引用来实现内存敏感的缓存,有空闲内存就缓存,内存不足时就清理掉

(3)弱引用(Weak Reference)–发现即回收:只被弱引用关联的对象只能生存到下一次垃圾回收之前,只要垃圾回收,无论内存是否足够,都会进行弱引用对象回收

软引用、弱引用都非常适合来保存那些可有可无的缓存数据

(4)虚引用(Phantom Reference)–对象回收跟踪:为对象设置虚引用唯一目的就是能在该对象被垃圾收集器回收时收到一个系统通知,用于跟踪垃圾回收过程

无法通过虚引用来获取被引用的对象(get()方法获取对象时总为null)

虚引用创建时必须提供一个引用队列(回收对象后,将对象虚引用加入队列,以通知应用程序对象的回收情况)作为参数

垃圾回收器

标准:在最大吞吐量优先的情况下,降低停顿时间

7款经典的垃圾回收器

串行回收器:Serial、Serial Old

并行回收器:ParaNew、Parallel Scavenge、Parallel Old

并发回收器:CMS、G1

分代收集:

在这里插入图片描述

垃圾回收器组合关系:

在这里插入图片描述

在JDK14中删除了CMS垃圾回收器

查看默认垃圾回收器(-XX:+PrintCommandLineFlags)

Serial回收器(串行回收)

采用复制算法串行回收和“Stop-the -World”机制的方式执行垃圾回收

是一个单线程的收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,且必须暂停其他所有工作线程(STW)

一般运行在Client模式下

-XX:+UseSerialGC

ParNew回收器(并行回收)

采用并行回收方式执行内存回收,同样采用复制算法、“Stop-the-World”机制

ParaNew是很多JVM在Client模式下新生代的默认垃圾收集器

在这里插入图片描述

新生代并行(回收次数频繁),老年代串行(回收次数少,串行减少CPU切换线程消耗)

-XX:+UseParNewGC

Parallel回收器(吞吐量优先)

基于并行回收,采用复制算法和“Stop-the-World”机制,Parallel收集器目标是达到一个可控制的吞吐量

高吞吐量可以高效运用CPU时间尽快完成程序运行任务(适合后台运算而不需要太多交互的任务

在这里插入图片描述

在Java8中是默认垃圾收集器

-XX:+UseParallelGC

-XX:ParallelGCThreads 设置年轻代并行收集器的线程数(一般与CPU核数相同(减少STW时间))

-XX:UseAdaptiveSizePolicy 设置Parallel收集器具有自适应调节策略(该模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会自动调整,用于达到堆空间、吞吐量、停顿时间的平衡)

CMS回收器(低延迟)

CMS(Concurrent-Mark-Sweep)收集器是第一款并发收集器,实现让垃圾收集线程与用户线程同时工作

采用标记–清除算法,有“Stop-the-World”机制

-XX:+UseConcMarkSweepGC

工作原理:

在这里插入图片描述

(1)初始标记:仅仅只是标记出GC Roots能直接关联到的对象,速度非常快(STW停顿存在,但时间很短)

(2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图(用时长但不需要暂停用户线程)

(3)重新标记:修正并发标记期间,因用户线程运作导致初始标记产生变动的部分对象的标记记录(存在STW但时间不长)

注意:重新标记只是修正并发标记阶段怀疑是垃圾的对象,对于并发标记时用户线程新产生的垃圾对象是无法进行标记的

(4)并发清理:清理删除掉标记阶段判断出已经死亡对象,释放内存空间(标记–清除算法,与用户线程并发执行,但会产生内存碎片)

注意:CMS收集过程中应确保程序用户线程有足够内存可以使用(垃圾收集阶段用户线程没有中断),CMS是当堆内存使用率达到某一阈值时就开始进行垃圾回收CMS失败时会临时启用Serial Old来重新老年代垃圾回收

优点:并发收集、低延迟

缺点:

(1)会产生内存碎片(导致并发清楚后用户线程可用空间不足,在无法分配大对象的情况下,不得不提前触发Full GC。为新对象分配内存空间时需要选择空闲列表执行内存分配(堆中维护空闲列表存在内存开销))

(2)对CPU资源非常敏感(占用一部分线程导致程序变慢降低吞吐量)

(3)无法处理浮动垃圾(在并发标记阶段若产生新的垃圾对象,CMS无法进行标记,最终这些新产生的垃圾对象没有及时回收(直到下一次GC时才能标记释放))

G1回收器(区域化分代式)

G1是并行回收器,将堆内存分割成很多不相关的区域(Region,物理上不连续的),G1跟踪各个Region里面垃圾堆积的价值大小(回收所获得空间和花费时间),在后台维护一个优先列表,每次根据允许收集时间,优先回收价值最大的Region(侧重于回收垃圾最大量的Region)

主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼顾高吞吐量的性能特征

G1是JDK9之后默认垃圾回收器,被称为“全功能的垃圾回收器”

-XX:+UseG1GC

特点:

(1)并行与并发

并行性:G1在回收期间,可以拥有多个GC线程同时工作,此时用户线程STW

并发性:G1拥有与应用程序交替执行的能力,部分工作可以与应用程序交替执行

(2)分代收集

G1仍然是分代型垃圾回收器,将堆空间分成若干个区域(Region),区域包含逻辑上的新生代和老年代(不要求内存连续)

在这里插入图片描述

H:主要用于存储大对象,如果超过1.5Region就放到H(避免堆中大对象保存在老年代影响垃圾回收)

一个H1区放不下一个大对象,G1会寻找连续的H区来存储

(3)空间整合

G1将内存划分为一个个Region,垃圾回收以Region为单位,Region之间是复制算法,但G1整体看做是标记–压缩算法

(4)可预测的停顿时间模型

每次根据允许收集的时间,优先回收价值最大的Region(保证有限时间内获取更高收集效率)

缺点:在内存占用和程序运行时额外执行负载方面存在更高消耗

参数设置:

(1)-XX:+UseG1GC 手动指定使用G1收集器执行内存回收

(2)-XX:G1HeapRegionSize 设置每个Region大小(值是2的整数幂,范围1-32MB之间)

(3)-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(默认200ms)

G1垃圾回收模式:Young GCMixed GCFull GC

G1垃圾回收器可以采用应用线程承担后台运行的GC工作(JVM内置GC线程慢时会调用应用程序线程加速垃圾回收)

G1收集器将整个Java堆内存划分为约2048个大小相同的独立Region,在JVM生命周期内不会被改变

垃圾回收过程:(1)Young GC (2)并发标记 (3)Mixed GC (4)可能存在Full GC

在这里插入图片描述

Young GC:当年轻代的Eden区用尽时开始垃圾回收,G1年轻代收集阶段是一个并行的独占式收集器(STW用户线程,多线程执行垃圾回收,将Eden存活对象移动到Survivor)

在这里插入图片描述

并发标记:当堆内存达到阈值(默认45%)开始老年代并发标记过程

在这里插入图片描述

Mixed GC:G1的老年代回收一次只需要小部分老年代Region(此时老年代Region是和年轻代一起回收)

在这里插入图片描述

Full GC(可选) 针对GC提供了失败保护机制(强力回收)
在这里插入图片描述

G1垃圾收集时避免全堆扫描Region间的引用:Remembered Set(记忆集RSet)

​ 只要是分代垃圾收集器,JVM都通过RSet避免全局扫描

​ 每个Region都对应一个自己的RSet

​ 每次引用类型数据写操作时,会在自身RSet中记录与其他Region间的引用关系,在进行垃圾收集时,在GC Roots范围内加入RSet
在这里插入图片描述

G1的Young GC需要使用到RSet

总结

在这里插入图片描述

高吞吐量选择并行收集,低延迟选择并发收集 官方推荐G1

GC日志说明

Young GC:
在这里插入图片描述

Full GC:
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值