JVM笔记

JVM位置
在操作系统上
在这里插入图片描述
JVM整体结构
在这里插入图片描述
类加载器:将.class 字节码文件加载进内存中生成class对象
加载、链接、初始化

运行时数据区:方法区和堆是线程共享的;程序计数器、本地方法栈、虚拟机栈是独有的
执行引擎:包含 解释器、JIT编译器、垃圾回收器(GC)
本地方法接口

Java代码执行流程
Java程序------------(通过java编译器(又叫前端编译器))---------------->生成字节码文件(对应一个java类)
第一次编译,把文件编译成字节码文件
---->类加载器------>解释器和JIT编译器(执行器)
第二次编译,把字节码文件中的字节码编译成机器指令,同时将机器指令缓存起来

生命周期
启动,执行,退出
启动是通过引导类加载器创建一个初始化类来完成的
执行一个所谓的java程序的时候,真真正正在执行的是一个叫做java虚拟机的进程
程序结束或者遇到异常或错误而异常终止,也可以使用Runtime或System的exit()方法退出虚拟机

1.4类加载过程

类加载器和类加载过程
加载阶段,链接阶段,初始化阶段
类加载器子系统负责从文件系统或者网络中加载Class文件
ClassLoader只负责class文件的加载,是否执行由execution Engine决定
加载的类信息存放在方法区中,方法区除了类的信息外,还会存放运行时常量池的信息,包括字符串字面量和数字常量

类的加载过程
① Loading(加载阶段)
通过一个类的全限定类目获取定义此类的二进制字节流
将字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
(重点:生成java.lang.Class 这个类的实例 是在加载阶段完成的)
② Linking(链接阶段):验证,准备,解析
验证阶段: 保证被加载类的正确性,不会危害虚拟机自身安全;字节码文件的开头均为 CAFE BABE ,如果出现不合法的字节码文件,那么将会验证不通过
准备阶段 为类变量(static变量)分配内存并且为类变量的默认赋值,即零值
(开辟空间,为static的成员变量进行默认赋值)
解析阶段:将常量池内的符号引用转换为直接引用的过程

初始化阶段:(执行类构造方法<client>,显示赋值即给静态代码块赋值)
当我们代码中包含static变量的时候,就会有clinit方法
若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁

Jclasslib的使用
在这里插入图片描述

1.5 类加载器的分类

凡是继承自ClassLoader的类加载器都称为自定义类加载器

类加载器的分类
启动类加载器 bootstrap classloader
这个类加载器使用C/C++实现的,嵌套在jvm内部
没有父加载器
加载扩展类和应用程序类加载器,并指定为他们的父类加载器
Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
(resources、rt、sunrsasign、jsse、jce、charsets、jfr、classes)

扩展类加载器
Java语言编写,由ExtClassLoader实现
父加载器为启动类加载器
加载java.ext.dirs系统属性所指定的目录中加载类库,或jdk的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动被扩展类加载器加载(ext目录)

应用程序类加载器(系统类加载器,AppClassLoader)
Java语言编写,由AppClassLoader实现
派生与ClassLoader类

父加载器为扩展类加载器
负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
这类加载是程序中默认的类加载器
通过CLassLoader#getSystemClassLoader()方法可以获取到该类加载器

用户自定义类加载器实现步骤:
1.通过基础抽象类ClassLoader类的方式,实现自己的类加载器,以满足一些特性的需求
2.jdk1.2后建议把自定义的类加载逻辑编写在findCLass()方法中
3.在编写自定义类加载器时,如果没有太过复杂的需求,可以直接继承URLClassloader类,这样可以避免自己编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁
(要么继承抽象类ClassLoader,重写findClass()方法,要么直接继承URLClassloader

双亲委派机制

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
  • 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常

类加载器收到加载请求,向上委托,委托到引导类目录加载器进行加载,若父加载器加载不了就向下委托给子加载器进行加载

双亲委派机制优势
避免类重复加载
避免核心类被篡改

沙箱安全机制

  • 自定义String类时:在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java.lang.String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。
  • 这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

类的主动使用和被动使用

如何判断两个class对象是否相同?

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

① 类的完整类名必须一致,包括包名
② 加载这个类的CLassLoader(指ClassLoader实例对象)必须相同
(即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但是加载的ClassLoader实例对象不同,这两个类对象也不是相等的)
具体举例 java.lang.String 异常 没有mian

主动使用七种情况
初始化一个子类,(要先调用其父类)
反射
调用类的静态方法,(包括静态代码块)
创建类的实例

主动使用被动使用区别:初始化阶段是否调用**clinit**方法,调用了就是主动使用

初始化inishualation
执行类构造方法 (执行static方法)
若有父类先执行父类构造方法
并发情况下同步加锁

运行时数据区

线程
当一个java线程准备好执行后,操作系统会创建本地线程,当java线程被执行完后,本地线程也会被回收

本地线程决定JVM是否终止,如果只有守护线程(非普通线程),而没有普通线程 那JVM就立刻终止

操作系统负责线程调度,将线程调度到CPU上,一旦本地线程初始化成功就执行线程的run()方法(操作系统相当于CPU和线程之间的桥梁)

Hotspot后台运行的线程主要有以下几类:
虚拟机线程
周期任务线程
GC线程
编译线程
信号调度线程

PC寄存器(程序计数器)
用来存储下一条指令的地址由执行引擎读取下一条指令
(用于存放下一条执行指令的地址(操作数栈),执行引擎去pc寄存器中读取)

既没有GC,也没有OOM
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
PC寄存器面试题
使用PC寄存器存储字节码指令地址有什么用?
为什么使用PC寄存器记录当前线程的执行地址呢?
CPU不停的切换进程,需要通过PC寄存器来获取下一条字节码指令的地址

为什么PC寄存器被设定为线程私有的?
为了能 准确记录各个线程正在执行的字节码指令地址

并行:多个线程,多个CPU
串行:多个线程依次排队被一个CPU执行
并发:一个cpu不停地切换线程执行

虚拟机栈
主要特点:跨平台,指令集小,编译器容易实现

栈是运行时的单位,而堆是存储的单位
(基本数据类型在栈中, 引用数据类型(类、接口、数组)在堆中;
局部变量(基本数据类型、对象的引用地址)存在栈中,成员变量(属性)存储在堆中)

虚拟机栈是什么?
每个线程创建时都会创建一个虚拟机栈,其内部结构为一个个栈帧,一个栈帧对应这一次java方法的调用;
线程是私有的

虚拟机栈的生命周期
生命周期和线程一致。(虚拟机栈随着线程的创建而创建,随着线程的消亡而消亡)

栈的特点(优点):访问速度仅次于 pc寄存器;
jvm堆java栈的操作只有两个 入栈,出栈;
不存在垃圾回收的问题

设置栈内存大小
-Xss
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

栈的存储结构和运行原理

栈中存储什么(栈的单位):栈帧
在执行的每个方法都对应的一个栈帧
执行引擎运行的所有字节码指令只针对当前栈帧进行操作,如果该方法调用了其他方法,对应的新栈帧就会被创建出来,放在栈帧的顶部,成为新的当前栈

不同的线程中所包含的栈帧是不允许互相引用的

Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种就是抛出异常。不管是哪种方式,都会导致栈帧被弹出

一个栈帧的入栈对应这一个新的方法调用,一个栈帧的出栈对应一个方法的结束,如果有异常的话,会返回给栈帧的调用者,依次往下执行

栈帧的内部结构

局部变量表操作数栈、动态链接(指向运行时常量池中相对应方法的引用)、方法返回地址、一些附加信息

局部变量表

又叫局部变量数组/本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法内的局部变量
不存在数据安全问题(不涉及线程共享数据
局部变量表所需要的容量大小是在编译期确定下来的
在这里插入图片描述
在这里插入图片描述
方法嵌套调用的次数由栈的大小决定。栈越大,方法嵌套调用次数就越多(栈帧大小决定局部变量表的大小)
局部变量表中的变量只在当前方法调用中有效。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
局部变量表:存储局部变量,和方法参数

变量槽(slot):局部变量表的最基本存储单位
局部变量表存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量
在局部变量表中,32位以内的数据类型都只占1个变量槽,64位的数据类型(long,double)占用2个变量槽如果需要访问局部变量表中64位的局部变量值时,只需要使用它的前一个索引位置即可(long或者double)

如果当前栈帧是构造方法或实例方法(非static方法)创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排序
如果栈帧是非static方法或者构造方法会默认自带一个slot插槽,静态方法也有一个slot插槽

变量的分类:按照数据类型分:① 基本数据类型 ② 引用数据类型

8大基本数据类型:linking的prepare阶段会进行默认赋值
实例变量:随着实例的创建而在堆中分配内存空间,并赋值,随着实例被gc回收而销毁

局部变量:使用前必须(显示)赋值,否则编译无法通过

栈的性能调优,关系最为密切的部分就是局部变量表

Slot的重复利用

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public void test4() {
int a = 0;
{
int b = 0;
b = a + 1;
}
//变量c使用之前已经销毁的变量b占据的slot的位置
int c = a + 1;
}
局部变量 c 重用了局部变量 b 的 slot 位置

操作数栈
满足特殊条件的数组或者说是链表;但是操作数栈不能采用访问索引的方式进行数据访问的

栈帧中除了局部变量表之外,还包含一个先进后出的操作数栈有,也可称为表达式栈
操作数栈,在方法执行过程中,根据字节码指令,在栈中写入数据或提取数据,即入栈(push)/出栈(pop)

方法的执行过程
在这里插入图片描述
操作数栈,主要保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
栈中的任何一个元素都可以是任意的java数据类型
32字节的类型占用1个栈单位深度
64字节的类型占用2个栈单位深度(long,double)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令

栈顶缓存技术
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为此,
将栈顶元素全部缓存在物理CPU的寄存器,以此降低对内存的读/写次数,提升执行引擎的执行效率

动态链接(指向运行时常量池的方法引用)
栈帧内部指向运行时常量池所属方法的引用
在这里插入图片描述
作用:将符号引用转化为调用方法的直接引用

常量池的作用:就是提供一些符号和常量,便于指令的识别
在这里插入图片描述
方法的调用编译期间不确定-->涉及到多态 用接口或者父类作为方法参数

将符号引用转换为调用方法的直接引用与方法的绑定机制相关

静态链接(早期绑定):被调用方法在编译期间已确定
在这里插入图片描述
动态链接(晚期绑定):被调用方法在编译期间无法确定,在运行期间绑定相关的方法
在这里插入图片描述
虚方法和非虚方法

非虚方法:编译期就确定了具体的调用版本,且运行时不可变,这样的方法称为非虚方法
虚方法:编译期间无法确定(可重写的方法)

静态方法、私有方法、final方法、实例化构造器、父类方法都是非虚方法 (不涉及多态)
其他方法称为虚方法

invokestaticinvokespecial 指令调用的方法称为非虚方法,其余的(final修饰的除外)
都成为虚方法
Invokevirtual(调用所有的虚方法)、 虚方法指令(final或super修饰的除外) invokeinterface(调用接口方法)

动态调用指令:
Invokedynamic:动态解析需要调用的方法,然后执行

动态类型语言和静态类型语言
二者区别是对类型的检查是在编译期还是运行期间,动态语言是在运行,静态语言在编译期

静态语言判断变量自身的类型信息;动态语言判断的是变量值的类型信息
Java是一门静态语言,但因为lambda表达式的出现,java也算一门动态语言
(编译期间未确定类型信息)

在这里插入图片描述
方法重写的本质:
IllegalAccessError 调用方法的权限不够(编译期间);类不兼容的改变,说白了jar包冲突(运行期间)
AbstractMethodError 调用方法时未重写方法,导致调用的是个抽象方法

虚方法表:面向对象过程中,频繁使用动态分派,每次动态分派都影响性能,为此,在类的方法区建立了一个虚方法表(virtual method table)(非虚方法不会出现在表中)。使用索引表来代替查找

虚方法表在类加载的链接阶段被创建并初始化的
在这里插入图片描述
方法返回地址(方法出口)
调用pc寄存器中的值作为返回地址,调用该方法的下一条指令,让执行引擎执行
pc寄存器将下一条指令的地址值 交给方法返回地址 让执行引擎继续执行操作

方法的结束方式:
正常执行完成;出现异常,非正常退出
如果是非正常退出,返回地址是要通过异常信息表来确定,栈帧中一般不会保存这部分信息
异常完成出口退出的不会给他的上层调用者产生返回值
正常退出有给他的上层调用者返回地址信息(字节码指令中有返回指令(return)),
非正常退出(异常)要去异常信息表中查找异常信息,且不会给他的上层调用者返回消息

方法区和堆空间存在GC和Error
虚拟机栈和本地方法栈存在Error不存在GC
PC寄存器不存在Error和GC

分配的栈空间越大越好吗? 越大只能让异常出现的晚一些,不可能不出现;其次栈空间分配多了,内存空间就少了

方法中定义的局部变量是否线程安全? ---->逃逸分析

本地方法接口
被native 修饰的方法被称为本地方法,调用其他语言的接口(例如Thread)

JVM还不是个操作系统,必须依赖操作系统来实现,而操作系统都是用C/C++来写的,本地方法接口的作用就是如此

本地方法栈
也是线程私有的;可以设置本地方法栈的内存大小(在内存溢出方面是相同的)具体做用是在本地方法栈中登记native方法,由执行引擎执行执行

在这里插入图片描述
在hotspot jvm中本地方法栈和虚拟机栈合二为一了
当要执行本地方法栈中方法时,由执行引擎执行

除了引导类加载器是用C++写的,扩展类、应用程序类加载器及自定义类加载器都是直接或间接继承ClassLoader(或URLClassLoader)

每个JVM实例对应一个堆空间
堆是逻辑上连续,物理上不连续的一个内存空间
所有的线程共享java堆,在堆中还可以划分为线程私有的缓冲区
(Thread Local Allocation Buffer ,TLAB)
TLAB线程缓存区是不共享的

几乎所有的数组和对象实例都是分配在堆中的
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置。
方法结束后,堆中的对象不会马上被移除,在GC垃圾回收的时候才会被移除
堆是GC回收的重点区域

堆内存结构细分
现代垃圾收集器大部分都是基于分代收集理论设计的,堆空间细分为
新生区+老年区+ 永久代(1.8为元空间)——————>方法区
新生区:伊甸园区+幸存from和幸存to

新生代与老年代

配置新生代与老年代在堆结构的占比(一般情况下不会去修改这个比例的)
默认:-XX:NewRatio=2 ,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改: -XX:NewRatio=4 ,表示新生代占1,老年代占4,新生代占整个堆的1/5

某些对象的生命周期比较长,此时建议增大老年区比例
几乎所有的java对象都是在Eden区被new出来的
绝大部分的java对象的销毁都在新生代进行(IBM研究80%的对象都是“朝生夕死”的)

对象分配的过程
Minor GC触发 Eden区满了就会触发,Survivor区满了不会触发
当Minor GC触发后,会回收Eden区和Survivor区的垃圾对象(若Eden区的对象未被回收,自动进入survivor区)
Survivor区满了,Survivor区的对象直接进入老年代

针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
关于垃圾回收:频繁在新生区收集,很少在老年区收集,几乎不在永久区/元空间收集

Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都对新生区、老年区;方法区 一起回收的,大部分时候回收的都是新生代
针对Hotspot VM的实现,它里面的GC按照回收区域分为两大类型:一种是部分收集(partial GC),一种是整堆收集(full gc)

部分收集:
新生代收集(minor GC /young GC)
老年代收集(major GC/old GC)
目前只CMS GC会单独收集老年代
混合收集(mixed GC):收集整个新生代以及部分老年代的垃圾收集 ,
目前只有G1 GC会有这种行为

整堆收集 Full GC:收集整个java堆和方法区的垃圾收集。

年轻代GC(Minor GC)触发机制:
当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden区满,SUrvivor满不会引发GC(每次Minor GC 会清理年轻代(Eden,Survivor)的内存)
因为java对象大多数都是朝生夕灭的特性,所以Minor GC分层频繁,一般回收速度也比较块。
Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行

老年代GC(Major GC/Full GC)触发机制:
出现Major GC,经常会伴随至少一次的Minor GC(但非绝对的,例如parallel 收集器的收集策略就是直接进行Major GC)
(在老年代空间不足时,会先尝试触发Minor GC 。如果之后空间还不足,则触发Major GC) 如果Major GC后,内存还不够,就OOM了)
Major GC 的速度一般会比Minor GC慢10倍以上

Full GC触发机制
1.调用System.gc() 时,系统建议执行Full GC,但是不必然执行
2.老年代空间不足
3.方法区空间不足
4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5.由Eden、Survivor0向Survivor1区复制时,对象大小大于 To Space可用内存,则把该对象转存到老年代,且老年代的内存小于该对象大小 (Survivor区满了后,old区放不下)
(full GC尽量避免)

内存分配策略
优先分配到Eden
大对象直接分配到老年代(需要一个连续的内存空间对象,说白了Eden区放不下了)
(比较长的数组或者字符串,Eden区塞不下
尽量少创建大对象(空间不足以放下这些大对象就会GC,进而STW)
长期存活的对象分配到老年代
动态年龄判断
同一年龄的所有对象大于Survivor区的一半,将大于或等于该年龄的对象直接进入老年代)
空间分配担保
-XX:HandlePromotionFailure

TLAB(为对象分配内存Thread Local Allocation Buffer)
(给线程分配个缓冲区)
堆区是线程共享的,在并发环境下从堆区中划分内存空间是线程不安全

什么是TLAB?
内存分配时,JVM为每个线程分配了一个私有缓存区域包含在Eden内

TLAB仅占整个Eden区的1%
开启TLAB -XX:UseTLAB (默认情况下是开启的)
-XX:TLABWasteTargetPerent 修改TLAB的空间大小
尽管不是所有的对象实例都能在TLAB上成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
一旦对象在TLAB空间分配失败,JVM会尝试通过加锁机制来确保操作数据的原子性,从而直接在Eden区中分配内存。
在这里插入图片描述
堆空间都是共享的吗? TLAB

空间分配担保机制(jdk7 开始默认都是开启的)
-XX:HandlePromotionFailure:是否设置空间分配担保
Minor GC前,jvm会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
如果大于,minor GC
如果小于,则查看虚拟机是否开启担保机制(默认是开启的)

如果开启,那么会继续检查老年代最大连续可用空间是否大于历次晋升到老年代的对象平均大小
如果大于Minor GC
如果小于 full GC
如果没开启,直接full GC

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行minor GC,否则就进行Full Gc

栈上分配

逃逸机制
当一个对象在方法当中被定义后,对象只在方法内部使用 (未发生逃逸机制,满足栈上分配)
当一个对象在方法中被定义后,他被外部方法所引用,则认为发送了逃逸。
例如作为调用参数传递到其他地方中。
在这里插入图片描述
在这里插入图片描述
如何快速的判断是否发生了逃逸分析,就看new的对象实体是否在方法外被调用。

只在方法内new 对象实例,在方法内使用,未返回
在这里插入图片描述
Jdk7开始默认开启逃逸机制
开发中能使用局部变量的,就不要使用在方法外定义
在这里插入图片描述
优化代码
栈上分配
同步省略
分离对象或标量替换

栈上分配
JIT编译器在编译期间根据逃逸分析的结果,如果对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收

同步省略(锁消除)
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步

线程同步的代价是相当高的,同步的后果是降低并发性和性能

在动态编译同步块的时候,JIT编译器可以结束逃逸分析来判断同步块锁使用的锁对象是否只能够被一个线程服务而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。
(在方法中的对象只能被一个线程访问,不涉及并发,没必要使用同步代码块)
对象作为局部变量的话不考虑同步机制
在这里插入图片描述
在这里插入图片描述
分离对象或标量替换
将聚合量拆分成标量 (将对象打成基本数据类型分配在栈上)
Java对象就是聚合量,因为他可以分解成为其他聚合量和标量

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会吧这个对象拆解成若干个成员变量替换(这就是标量替换)

堆是对象分配的唯一选择? 是的,对象实例都是创建在堆上
Oracle的hot spot虚拟机目前也没默认应用栈上分配,但是有应用标量替换

方法区
在这里插入图片描述
虚拟机栈栈帧中局部变量表的对象引用指向 堆中的对象实例 ,对象实例指向方法区中的类型数据(类信息 Class)

方法区在逻辑上是属于堆的一部分,但是一些简单的实现可能不会选择去进行垃圾收集或者压缩,对于HotspotJVM而言,方法区还有一个别名叫做非堆,目的就是要和堆分开

所以,方法区是一块独立于堆的内存空间 (设置堆空间的大小不会影响方法区)

方法区和堆一样都是线程共享的
方法区在jvm启动的时候被创建,并且它的实际物理内存空间和java堆区一样可以不连续
方法区的大小与堆空间一样,可以选择固定大小或者是可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,会导致方法区的溢出,虚拟机同样会抛出内存溢出错误:OOM
例如加载大量的第三方jar;tomcat部署的工程过多;大量生成动态的反射类
关闭JVM就会释放这个区域的内存

方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。
Jdk7及之前

 -XX:PermSize 设置初始分配空间,默认是20.75M
 -XX:MaxPermSize 设置永久代最大可分配空间  32位电脑默认是64M,64位电脑默认是82M

Jdk8及之后

-XX:MetaspaceSize   默认约等于21M(实际算出来为20.79M)
-XX:MaxMetaspaceSize  默认为-1,即没有限制

正常开发情况下设置 -XX:MetaspaceSize

-XX:MetaspaceSize :设置初始的元空间大小。对于一个64位的服务器端JVM来说
默认的大小为21M,这是初始的最高位线,一旦触及这个水平线,full GC就会触发,然后水平线会重置。新的水平线取决于GC后释放了多少元空间。如果释放空间不够,那么在不超过MaxMetaspaceSize 时,适当的提高该值。如果释放空间过多,则适当降低该值。

如果测试的水平线过低,上述水平线调整情况会发生好多次。通过垃圾回收器的日志可以观察到full gc多次调用。为了避免频繁地GC,建议
-XX:MetaspaceSize 设置为一个相对较高的值

方法区内存结构
在这里插入图片描述
.java源文件 编译成 .class字节码文件 通过类加载器 加载进运行时数据区
类信息本身存放到方法区中

方法区存储什么?类型信息(类型基本信息,方法信息,域信息),常量,静态变量,
JIT编译后的代码缓存

类型信息:
对每个加载的类型(类Class、接口interface、枚举Enum、注解annotation)在方法区中存
储以下类型信息
1这个类的位置类名(全名=包名.类名)
2这个类直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
3这个类的修饰符(public,abstract,final的某个子集)
4这个类直接接口的一个有序列表

域(field)信息
域信息包括:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

方法(method)信息
方法名称,方法的返回类型(或void),方法参数的数量和类型(按顺序),方法的修饰符,
方法的字节码、操作数栈、局部变量表及大小,异常表(abstract和native方法除外)

全局常量:static final (面试别人的坑)
被声明为final的类变量的处理方法则不同,每个全局变量在编译期(prepare阶段)就被分配了(面试别人的坑)
在这里插入图片描述
在这里插入图片描述
运行时常量池
字节码指令中的常量池(Constant pool)运行时加载进常量池中就是运行时常量池了

为什么要有常量池?
在这里插入图片描述
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池。这个字节码包含了指向常量池的引用。在动态链接的时候将运行时常量池的符号引用转成直接引用。
在这里插入图片描述
#xx 都是在常量池中加载的

常量池的引用类似于炒菜时的基本原料;代码就类似于菜(方法类似菜,方法里面的内容,都是原料,都在常量池中)
几种常量池内存储的数据类型包括:数据值、字符串值、类引用、字段引用、方法引用

常量池:存放编译期生成的各种字面量和符号引用,运行时转化为直接引用

小结:常量池、可以看做是一张表,表中存放编译期生成的各种字面量和符号引用,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

运行时常量池
运行时常量池是方法区的一部分
常量池是class文件的一部分,用于存放编译期生成的各种字面量和符号引用(加载后转化为直接地址或者叫直接引用),这部分内容在类加载后存放到方法区的运行时常量池中 (常量池在类加载后,存入运行时常量池中)

加载类和接口到虚拟机后,jvm就会创建对应的运行时常量池
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组都是通过索引访问的
(每个.class 文件加载进jvm时,jvm会创建相对应的常量池。说白了,将每个.class文件的常量池加载进运行时常量池中)
运行时常量池,相对于class文件中的常量池的另一重要特征就是:具备多态性(字符串拼接)
例如: String.intern()
在这里插入图片描述
面试考点:jvm内存空间在版本演进过程中存在那些变化吗
方法区演进细节
只有Hotspot才有永久代的概念, IBM、JRocket等不存在永久代的概念

1.6及之前有永久代,静态变量存放在永久代上
1.7有永久代,但已经逐步“去永久代”,字符串常量池、静态变量被移除出永久代,保存在堆中
1.8及之后无永久代,类信息、字段、方法常量保存在本地内存的元空间,但字符串常量池、静态变量人在堆

永久代为什么要被元空间替换?
1永久代设置空间大小不好确定,很容易OOM
太小容易导致full GC 进而STW
而元空间不存在虚拟机中,使用的是本地内存,最大空间为-1(无限大)。默认情况下,元空间大小仅受本地内存的限制
2对永久代调优比较困难
为什么静态变量和字符串常量池StringTable要移到堆中

StringTable为什么要移到堆中?
永久代只有full gc时才会回收,而full gc只有老年代永久代空间不足时才会触发
导致StringTable回收效率不高。而开发中会有大量的字符串被创建,回收效率低,导致永久代空间不足,放到堆里,能及时被回收。

** 静态变量存在哪里?**
在这里插入图片描述
在这里插入图片描述
1、静态引用对应的对象实体(也就是这个new byte[1024 * 1024 * 100])始终都存在堆空间中。
2、只是那个变量(相当于下面的arr变量名)在JDK6,JDK7,JDK8存放位置中有所变化

变量(名)存放在哪里?

Jdk7及其之后的版本,Hotspot把静态变量与类型在java语言一端的映射Class对象存放在一起,存储于Java堆之中
在这里插入图片描述
方法区的垃圾回收
有些人认为方法区(如Hotspot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载)。

方法区的垃圾回收主要回收两部分内容:
常量池中废弃的常量和不在使用的类型

方法区常量池中主要的两大类常量:字面量符号引用
字面量比较接近java语言层次的常量概念,如文本字符串、被声明为final的常量值等
而符号引用则属于编译原理方面的概念,包括下面三类常量:
类和接口的权限的类名、字段的名称和描述符、方法的名称和描述符

Hotspot虚拟机对常量池的回收策略是很明确的,只要常量池的常量没有被然任何地方引用,就可以被回收
(回收废弃常量和java堆中的对象非常类似)

判断一个类型是否被回收(不再使用) 条件比较苛刻,需同时满足下面三个条件
该类的实例都已经被回收了(类实例的对象 不再被使用)
加载类的加载器已经被回收了
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访该类的方法(反射不在被使用)

(无实例对象,无类加载器,无反射的使用)

方法区要不要垃圾回收? 可以要可以不要,《java虚拟机规范》中没有明说

对象的实例化内存布局与访问定位

创建对象的方式

1   new
	直接new
	单例模式中的静态方法
	XXXBuilder/XXXFactory的静态方法
2   Class的newInstance()      反射的方式,只能调用空参的构造器,权限必须是public
3   Constructor的newInstance(XXX)   反射的方式,可以调用空参、带参构造器,权限没要求
4   使用clone()   不调用任何构造器,当期类需要实现cloneable接口,实现clone()
5   使用反序列化  从文件中、网络中获取一个对象的二进制流
6   第三方库Objenesis  

对象创建的步骤
加载类源信息、为对象分配内存空间、处理安全并发问题、属性初始化赋值、设置对象头、属性的 显示初始化、代码块、构造器赋值(init())

1 判断对象对应的类是否加载、链接、初始化方法区中是否存在相对应的类
(虚拟机遇到一条new指令,首先去检查这个指令的参数能否在metaspace的常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经内加载、解析、初始化。[即判断这个类是否存在]。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为key进行查找对应的.class文件。如果没找到文件,则抛出CLassNotFoundException异常,如果找到,则进行类加载,并生成相对应的Class类对象)
2 为对象分配内存 (取决于垃圾收集器是否有压缩整理功能)
(首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。)
指针碰撞(内存规整) Serial、ParNew垃圾回收器
空参列表(内存不规则) CMS垃圾回收器—标记清楚算法
3 处理并发安全问题
TLAB 为每个线程预先分配一块本地线程缓冲区
采用CAS配上失败重试保证原子性

4 初始化分配到的空间 (属性的默认初始化值)
所有属性设置默认值
给对象的属性赋值操作: ①属性的默认初始化 ②显示初始化 ③代码块初始化④构造器初始化
5 设置对象头

6 执行init方法进行初始化(显示初始化,包括③②)
(类构造器对应的方法,静态属性初始化方法)

== 对象内存布局 (对象在堆空间的布局)==
对象头、实例数据、对齐填充

对象头:运行时元数据、类型指针

运行时元数据:
		哈希值(hashcode):  局部变量表中引用变量指向堆中的地址值
		GC分代年龄
		锁状态
类型指针 -----指向元数据InstanceKclass,确定该对象所属的类型
(说明:如果是数组,还要记录数组的长度)

实例数据(Instance Data) (类的成员变量,以及父类的成员变量)
对象存储的有效信息,包括程序代码中定义的各种类型的字段
(包括父类继承和自己本身拥有的字段)

对齐填充 不是必须的,没有特别含义,仅仅起到占位符的作用

对象访问定位
JVM是如何通过栈帧中的对象引用访问到其内部的对象实例?
定位,通过栈上reference访问

对象访问方式主要有两种:句柄访问、直接指针(虚拟机默认的)
在这里插入图片描述
优点:栈帧的本地变量表中变量操(slot)的reference地址稳定,对象被移动(垃圾收集时移动移动对象很普遍)时只改变句柄中的实例对象地址即可,reference地址不需要被修改
在这里插入图片描述
优点:节省内存空间
(Hotspot默认的方式)

** 直接内存 ** (Jdk1.8元空间)
不是虚拟机运行时数据区的一部分,也不是jvm规范定义的一部分
直接内存是在java堆外的、直接向系统申请的内存区间
来源于NIO,通过存在堆中的DirectByteBuffer操作native内存
通常,访问直接内存的速度会优于java堆。即读写性能高

因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存
Java的nio库允许java程序使用直接内存,用于数据缓冲区

有可能导致OutOfMemoryError异常 (nio的OOM异常是监控器中堆内存未满状况下)
在这里插入图片描述
由于直接内存(nio)在java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统给出的最大内存。

缺点

分配回收成本较高	
不熟jvm内存回收管理

直接内存带下可以通过MaxDirectMemorySize设置
如果不指定,默认与堆的最大值-Xmx参数值一致

执行引擎

概述:
执行引擎是java虚拟机核心的组成部分之一。
“虚拟机”是一个相当于“物理机”的概念,他们都有执行代码的能力,区别是物理机的执行引擎是建立在处理器、缓存、指令集合操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行不被硬件直接支持的指令集格式

Jvm的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被jvm所识别的字节码指令、符号表,以及其他辅助信息。

要想让一个java程序运行起来,执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。(执行引擎就相当于一个翻译官,将(高级语言)字节码指令翻译为(机器语言)机器指令,由操作系统执行
(jvm的任务就是加载字节码文件,由执行引擎执行变成机器指令,让操作系统执行)

工作过程

执行引擎在执行的过程中依赖于PC寄存器
每执行完一项指令操作后,pc寄存器就会更新下一条执行指令的地址
执行引擎执行过程中,会通过局部变量表的对象引用准确定位到存储在堆中的对象实例信息,以及通过对象头中的类型指针定位到对象的类源信息

执行引擎将字节码指令转化为机器指令,让cpu执行 (作用)

从外观上来看,所有的java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码的二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果

Java代码编译和执行的过程
大部分的程序代码转化为物理机的目标代码或虚拟机能执行的指令集之前,都需要经历这些步骤

Javac—> 编译成 抽象语法树
在这里插入图片描述

什么是解释器,什么是JIT编译器
解释器:当java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件的内容“翻译”为对应平台的本地机器指令执行。(将每条字节码翻译成机器指令)

JIT编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言(机器指令)(将源代码直接变成机器指令)

为什么java是半编译型半解释型语言
Jdk1.0时代只有解释器,只能解释执行,后来java也发展出直接生成本地代码的编译器。
执行引擎执行字节码文件时既可以使用解释器,也可以使用编译器
Jit编译器将字节码指令翻译成机器指令后,能进行缓存,执行效率更高

机器码
各种用二进制编码方式表示的指令,叫做机器指令码。开始就用机器指令码来编写程序,这就是机器语言。
机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用机器指令码编程极容易出错。
和其他语言相比,机器语言(机器字节码)的执行速度最快
机器指令和CPU密切相关,不同种类的CPU所对应的机器指令也不相同
(二进制编码)

指令
由于机器码由0和1组成的二进制序列,可读性太差,于是出现了指令
指令就是把机器码中特定的0和1序列,简化成对应的指令,可读性好
由于不同的硬件平台,执行同一操作,对应的机器码可能不同,所以不同的硬件台的同一种指令,对应的机器码也可能不同

指令集
不同的硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称为对应平台的指令集。
如常见的
X86指令集,对应的是X86架构的平台
ARM指令集,对应的是ARM架构的平台

汇编语言
由于指令的可读性还是太差,于是又发明出了汇编语言。
在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址
在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换为机器指令

由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行

高级语言
为了是计算机用户编程序更容易些,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言
当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。

二者关系
高级语言转换成汇编语言,再转换成机器指令,最后由cpu执行
在这里插入图片描述
字节码
字节码是一种中间状态(中间码)的二进制代码(文件),比机器码更抽象,需要直译器转译后才能成为机器码
字节码主要为了实现特定软件运行和软件环境、与硬件环境无关
字节码的实现方式是通过编译器和虚拟机。编译器将源码编译成字节码,特定平台上的虚拟机将字节码转译成可直接执行的指令
字节码的典型应用为java bytecode。
(字节码是可以在不同平台上运行)

解释器
为什么要有解释器?
JVM设计者们的初衷仅仅只是单纯地为了满足java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法(也就是产生了一个中间产品字节码)。

解释器工作机制(工作任务)
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件的内容“翻译”为对应平台的本地机器指令执行
当一条字节码指令被解释执行完成后,接着再根据PC寄存器记录的下一条需要被执行的字节码指令执行解释操作。

分类:字节码解释器,模板解释器
字节码解释器在执行时通过纯软件代码模拟字节码的执行,效率十分低下。
模板解释器将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而提升解释器的性能。

JIT编译器(运行效率比解释器块(直接将源代码翻译成机器指令,无需通过javac变成字节码指令))
HotSpot JVM的执行方式
Hotspot VM采用的是解释器与编译器并存的架构,在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。(目的是各种协作取长补短)
(具体是解释执行还是即时编译,要看Hotspot的判断标准)
Jit的优势:比解释执行速度快,已经将代码翻译成机器指令且缓存了,比解释器逐行翻译的效率要高很多

解释器的优点:当程序启动后,解释器可以马上发挥作用,响应速度快,而jit想要发挥作用,将字节码翻译成机器指令然后执行,这需要一定的时间,但翻译成机器指令后,jit编译器执行效率高。

Hotspot JVM的执行方式
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待jit编译器全部编译完成后再执行,这样可以省去很多不必要的编译时间。随着程序运行时间的推移,jit编译器逐渐发挥作用,根据热点推测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率

编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高

JIT编译器
相关概念解释
① Java 语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程。
(将java文件转化为字节码文件的过程)
典型的前端编译器:SUN的javac、eclipse JDT中的增量式编译器(ECJ)
② 也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程。
典型的JIT编译器:HotSpot的C1、C2编译器
③ 还可能是指使用静态提前编译器(AOT编译器,Ahead of Time Compiler)直接把.java文件编译成本地机器代码的过程。(可能是后续发展的趋势)

热点探测及探测方式
是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。
需要被编译为本地代码的字节码,也被称为“热点代码”,JIT编译器在运行时会针对“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升java程序的执行性能。
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR (On StackReplacement)编译。
一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能
目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。

方法调用计数器
这个计数器用于统计方法被调用的次数,他的默认阙值在client模式下1500次,在server模式下10000次。超过这个阙值,就会触发JIT编译。(64位电脑默认server模式)
这个阙值可以通过虚拟机参数 -XX:CompileThreshold 来人为设定。

当一个方法被调用时,查看方法区(元空间)中是否存在相对应的jit缓存

如果存在,则优先使用编译后的本地代码来执行
如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阀值。
如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。(编译好后留下jit缓存,存放在元空间当中)
如果未超过阈值,则使用解释器对字节码文件解释执行

在这里插入图片描述
热度衰减
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)(半衰周期是化学中的概念,比如出土的文物通过查看C60来获得文物的年龄)

进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样的话,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。

回边计数器
回边计数器的作用是统计一个方法体中循环代码的执行次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。

HotSpotVM可以设置程序执行方法

缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。
(cmd模式下运行或者 IDEA中的参数)
-Xint:完全采用解释器模式执行程序;
-Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行
-Xmixed:采用解释器+即时编译器的混合模式共同执行程序。

HotSpot VM中JIT分类
在Hotspot VM中内嵌两个jit编译器,分别为Client Compiler(c1)和Server Compiler(c2)

-client:指定java虚拟机运行在Client模式下,并使用c1编译器;

C1编译器会对字节码进行简单和可靠优化,耗时端。以达到更快的编译速度

-server:指定java虚拟机运行在Server模式下,并使用C2编译器。

C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
C1和C2编译器不同的优化策略

在不同的编译器上有不同的优化策略,
C1(client)编译器上主要有方法内联,去虚拟化、元余消除。
方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
去虚拟化:对唯一的实现樊进行内联
冗余消除:在运行期间把一些不会执行的代码折叠掉
C2(server模式,64位电脑默认是server模式,无法更改,idea运行参数倒是可以更改执行方式)的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:
标量替换:用标量值代替聚合对象的属性值
栈上分配:对于未逃逸的对象分配对象在栈而不是堆
同步消除:清除同步操作,通常指synchronized

分层编译策略:程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。

在Java7版本之后,一旦开发人员在程序中显式指定命令“-server”时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同来执行编译任务。

一般来说,jit编译出来的机器码性能比解释器高;
c2编译器启动比c1编译器慢,系统稳定执行后,c2编译器执行速度比c1编译器快

Graal 编译器
自JDK10起,HotSpot又加入了一个全新的即时编译器:Graal编译器
编译效果短短几年时间就追平了C2编译器,未来可期(对应还出现了Graal虚拟机,是有可能替代Hotspot的虚拟机的)
目前,带着实验状态标签,需要使用开关参数去激活才能使用
-XX:+UnlockExperimentalvMOptions -XX:+UseJVMCICompiler

StringTable(字符串常量池)

String的基本特性
String:字符串,使用一对 “” 引起来表示
String s1 = “atguigu” ; // 字面量的定义方式
String s2 = new String(“hello”); // new 对象的方式

String被声明为final的,不可被继承
String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String可以比较大小

String在jdk8及以前内部定义了final char value[]用于存储字符串数据。JDK9时改为byte[]

为什么 JDK9 改变了 String 的结构

String类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。

从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且大多数字符串对象只包含拉丁字符(Latin-1)。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用,产生了大量浪费。

之前 String 类使用 UTF-16 的 char[] 数组存储,现在改为 byte[] 数组 外加一个编码标识存储。该编码表示如果你的字符是ISO-8859-1或者Latin-1,那么只需要一个字节存。如果你是其它字符集,比如UTF-8,你仍然用两个字节存

结论:String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间
同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改

String 的基本特性

String:代表不可变的字符序列。简称:不可变性。
当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

String 的底层结构

字符串常量池中是不会存储相同内容的字符串的
String 的String Pool 是一个固定大小的HashTable ,默认长度为1009(1.6)。如果放进String Pool的String 非常多,就会造成hash冲突严重,从而导致链表很长,而链表长了后会造成的影响就是当调用String.intern时性能会下降
使用 -XX:StringTableSize可设置StringTable的长度
在jdk1.6中StringTable是固定的,就是1009的长度,如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求
在jdk1.7中,StringTable的长度默认是60013,
Jdk1.8开始1009是设置的最小值

String 的内存分配

在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种

直接使用双引号声明出来的String对象会直接存储在常量池中。
比如:String info=“hello”;

如果不是用双引号声明的String对象,可以使用String提供的intern()方法。

Jdk6及以前,字符串常量池存放在永久代。
Jdk7 中oracle的工程师对字符串池的逻辑做了很大的改变,将字符串常量池的位置调整至java堆内
所有的字符串都保存在堆(heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以。
字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在jdk7中使用String.intern()
Jdk8元空间,字符串常量在堆中

StringTable 为什么要调整?

永久代默认空间较小
永久代垃圾回收频率较低,极容易发送full GC,产生stw,或OOM
堆空间足够大,字符串能及时被回收

String 的基本操作

Java语言规范要求完全相同的字符字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例

分析运行时内存(foo() 方法是实例方法,其实图中少了一个 this 局部变量)
在这里插入图片描述

字符串拼接操作

常量与常量的拼接结果在常量池,原理是编译器优化
常量池中不会存在相同内容的常量
只要其中有一个是变量,结果就在堆中变量拼接的原理是StringBuilder
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
toString底层约等于new了一个新的String对象,并将字符串赋值给String对象
字符串破解操作不一定使用的是StringBuilder,如果拼接符号左右两边都是字符串常量或者常量引用(被final修饰的),仍然使用编译器优化,即非StringBuilder方式

针对final修饰的类、方法、基本数据类型、引用数据类型的量的结构时,能使用final的时候建议使用

如果拼接的结果调用intern(),则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
在这里插入图片描述
如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),新new了String对象

Intern():判断字符串常量池中是否存在相对应的值,存在,返回相对应的地址
不存在,则在常量池中加载一份,并返回次对象地址

intern() 的使用

Intern是一个native方法,调用的是底层C的方法

字符串常量池最初是空的,用String类私有的维护。调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将对象添加到池中,并返回对该字符串对象的地址

如何保证变量s指向的是字符串常量池中的数据呢?

方式1:String s=‘a’;
方式2:String s= new String(‘a’).intern();
  	    String s= new StringBuilder(‘a’).toString().intern();

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

new String()的说明

new String(“ab”)会创建几个对象? 2个

new #2 <java/lang/String>:在堆中创建了一个 String 对象;
ldc #3 <ab> :在字符串常量池中放入 “ab”(如果之前字符串常量池中没有 “ab” 的话)

new String(“a”) + new String(“b”) 会创建几个对象?

对象1:  new  StringBuilder()
对象2:  new  String(‘a’)
对象3:  常量池中的‘a’
对象4:  new  String(‘b’)
对象5:  常量池中的‘b’
对象6:toString()中  new String(“ab”)
强调,toString方法中的字符串在字符串常量池中没有生成
(intern()倒是在字符串常量池中生成了)

在这里插入图片描述
在这里插入图片描述

Intern的面试题

String s = new String(“1”);
s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
String s2 = “1”;
System.out.println(s == s2);//jdk6:false jdk7/8:false

1、s3变量记录的地址为:new String(“11”)
2、经过上面的分析,我们已经知道执行完pos_1的代码,在堆中有了一个new String(“11”)
这样的String对象。但是在字符串常量池中没有"11"
3、接着执行s3.intern(),在字符串常量池中生成"11"
3-1、在JDK6的版本中,字符串常量池还在永久代,所以直接在永久代生成"11",也就有了新的地址
3-2、而在JDK7的后续版本中,字符串常量池被移动到了堆中,此时堆里已经有new String(“11”)了
出于节省空间的目的,直接将堆中的那个字符串的引用地址储存在字符串常量池中。没错,字符串常量池中存的是new String(“11”)在堆中的地址
4、所以在JDK7后续版本中,s3和s4指向的完全是同一个地址。

String s3 = new String("1") + new String("1");//pos_1
s3.intern();  //s3等于用StringBuilder的append(),然后用toString() 生成一个String对象,此时新new的String对象字符串,在字符串常量池中没有生成
        
 String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
 System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
/*如何保证变量s指向的是字符串常量池中的数据呢?
 * 有两种方式:
 * 方式一: String s = "shkstart";//字面量定义的方式
 * 方式二: 调用intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern()  */

String s5 = "shkstart";
String s6 = new String("shkstart").intern();      
String s7 = new StringBuilder("shkstart").toString().intern();

 System.out.println(s5 == s6);   jdk7/8true

在这里插入图片描述
在这里插入图片描述
面试题的拓展

 //执行完下一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
 String s3 = new String("1") + new String("1");//new String("11")
 //在字符串常量池中生成对象"11",代码顺序换一下,实打实的在字符串常量池里有一个"11"对象


 String s4 = "11";  
 String s5 = s3.intern();
        // s3 是堆中的 "ab" ,s4 是字符串常量池中的 "ab"
        System.out.println(s3 == s4);//false
        // s5 是从字符串常量池中取回来的引用,当然和 s4 相等
        System.out.println(s5 == s4);//true

.intern()诺字符串常量池中未存在,则生成,已存在则返回地址,将引用地址赋值给新对象,所以s3==s4//false)

总结String的intern()的使用:
Jdk1.6中,将这个字符串对象尝试放入字符串常量池。
如果常量池中有,则并不会放入。返回已有的在字符串常量池中对象的地址
如果没有,会吧此对象复制一份,放入字符串常量池中,并返回字符串常量池的对象地址

Jdk1.7起,将这个字符串对象尝试放入字符串常量池。
如果字符串常量池中有,则并不会放入。返回已有的字符串常量池中的对象的地址
(若未赋值给新的对象,则原对象的地址值依旧为堆空间中的,具体参考上面的s3==s4)
如果没有,则会把对象的引用地址复制一份,放入字符串常量池,并返回字符串常量池中的引用地址

练习:

		String s = new String("a") + new String("b");//new String("ab")
        没有“ab”,而是一个引用指向new String(“ab”)
//在上一行代码执行完以后,字符串常量池中并没有"ab"
		/*
		1、jdk6中:在字符串常量池(此时在永久代)中创建一个字符串"ab"
        2、jdk8中:字符串常量池(此时在堆中)中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回
		*/
        String s2 = s.intern();

        System.out.println(s2 == "ab");//jdk6:true  jdk8:true   
        System.out.println(s == "ab");//jdk6:false  jdk8:true    

在这里插入图片描述
在这里插入图片描述
调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串则返回池中的字符串地址。否则,该字符串对象将对象添加到池中,并返回对该字符串对象的地址

	String x = "ab";  //常量池中已经拥有,不会放入
    String s = new String("a") + new String("b");//new String("ab")

    String s2 = s.intern();

    System.out.println(s2 == "ab");//jdk6:true  jdk8:true   
    System.out.println(s == "ab");//jdk6:false  jdk8:false  

在这里插入图片描述

intern() 的效率测试(空间角度)

使用intern()测试执行效率:空间使用上
结论:对于程序中大量存在存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省内存空间。

直接 new String :由于每个 String 对象都是 new 出来的,所以程序需要维护大量存放在堆空间中的 String 实例,程序内存占用也会变高

使用 intern() 方法:由于数组中字符串的引用都指向字符串常量池中的字符串,所以程序需要维护的 String 对象更少,内存占用也更低

大的网站平台,内存需要存储大量字符串信息。如社交网站等待,这时候如果用字符串都调用intern()方法,就会明显降低内存的大小。

G1 中的 String 去重操作

堆上存放重复的String对象必然是一种内存的浪费
命令行选项
UseStringDeduplication(bool) :开启String去重,默认是不开启的,需要手动开启。
PrintStringDeduplicationStatistics(bool) :打印详细的去重统计信息
stringDeduplicationAgeThreshold(uintx): 达到这个年龄的String对象被认为是去重的候选对象

String 去重的的实现
涉及到HashTable

当垃圾收集器工作的时候,会访问堆上存活的对象。对每个访问对象都会检查是否是候选的要去重的String对象
如果是要去重的对象,把对象的引用插入到队列中等待后续的处理。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象
使用HashTable来记录所有被String对象使用的不重复的char数组。当去重的时候,会检查这个HashTable,来看堆上是否存在一个一模一样的char数组
如果存在,String对象会被调整引用那个数组,释放对原数组的引用,终会被垃圾收集器回收掉
如果查找失败,char数组就会被插入到hashTable,这样以后的时候就可以共享这个数组了。

垃圾回收概述

在这里插入图片描述
Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要程序员手动的收集。
垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。

关于垃圾收集有三个经典问题:

哪些内存需要回收?
什么时候回收?
如何回收?

垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。

什么是垃圾?

垃圾是指在运行程序中没有任何指针指向的对象(没有被引用的对象),这个对象就是需要被回收的垃圾

如果不及时对内存中的垃圾进行清理,name这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出(内存泄露)
(不及时清理,空间被占用,甚至会OOM)

为什么需要GC?

对于高级语言来说,一个基本认识是如果不进行垃圾回收,内存迟早会被消耗完,因为不断地分配内存空间为背景下回收,就像不停地生产生活垃圾而从来不打扫一样。

除了释放没有的对象,垃圾回收也可以清楚内存里的记录碎片。碎片整理将所占用的堆内存移到一端,以便JVM将整理出的内存分配给新的对象

随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证程序的正常运行。而经常造成Stw的GC又跟不上实际的需求。所以才会不断地尝试对GC进行优化

早期垃圾回收

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

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

这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃

(内存泄露概念:这个方法不用了,却没办法被GC回收,最终应用程序崩溃)

Java 垃圾回收机制

自动内存管理,无需手动参与内存的分配与回收,这样减低内存泄露和内存溢出的分险
没有垃圾回收器,java也会和c++一样,有各种指针,泄露问题

自动内存管理机制,将程序员从繁琐的内存管理中释放出来,可以更专业地专注于业务开发

应该关心哪些区域的回收?

垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。
其中,java堆是垃圾收集器的工作重点
从次数来说:
频繁收集yong区
较少收集old区
局部不懂perm区(或元空间)

垃圾回收相关算法

标记阶段:引用计数算法

垃圾标记阶段:对象存活判断
在堆里存放着几乎所有的java对象实例,在GC执行垃圾回收之前需要区分出内存中哪些是存活对象,哪些是已死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放其所占的内存空间。

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

引用计数算法

对每个对象保存一个整形的引用计数器。用于记录对象被引用的情况

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

优点:实现简单,垃圾对象便于辨别;判定效率高,回收没有延时性
缺点:需要单独的字段存储计数器,这样的做法增加了存储空间的开销
每次赋值都需要更新计数器,伴随着假发和减法操作,在增加了时间开销
引用计数器有一个严重的问题,即无法处理循环引用的情况。只是一个致命的缺陷,导致在java的垃圾回收器中没有使用这类算法
(容易导致内存泄露(该回收的对象没被回收))
(面试时说内存泄露-------引用计数算法)

循环引用问题:
在这里插入图片描述
小结
引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。
具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。
Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
Python如何解决循环引用?

手动解除:很好理解,就是在合适的时机,解除引用关系。
使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。
标记阶段:可达性分析算法

(能解决循环引用造成的内存泄露)
相当于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄露的发生

相较于引用计数算法,这里的可达性分析就是java、c#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。

所谓“GC Roots”根集合就是一组必须活跃的引用
基本思路:(被GC roots 直接或间接连接的为非垃圾对象)
可达性分析算法是以根对象集合(GC Roots)为起点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径被称为引用链(Reference Chain)
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以被标记为垃圾对象
在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才算存活对象

GC Roots可以是哪些元素?

虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用的局部变量和方法参数

本地方法栈内JNI(本地方法)

方法区中类静态属性引用的对象
比如:java类的引用类型静态变量(类变量,static修饰的)
方法区中常量引用的对象
比如:字符串常量池(String Table)里的引用

被同步锁synchronized持有的对象

Java虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutofMemoryError),类加载器

除了这些固定的GC Roots集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC roots集合。比如分代收集局部回收

如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。

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

堆空间的周边,比如:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,都可以作为GC Roots进行可达性分析

注意
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
这点也是导致GC进行时必须“Stop The World”的一个重要原因。即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

对象的 finalization 机制

finalize() 方法机制

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

当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。

finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
在这里插入图片描述
对象的finalization机制
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点

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

从功能上来说,finalize()方法与C++中的折构函数比较相似,但是java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法在本质上不同于C++中的折构函数。

finalize()方法对应一个finalize线程,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收

生存还是死亡?

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

如果从所有的根节点都无法服务到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并不是“非死不可”的,这个时候他们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象的三种状态

可触及的:从根节点开始,可以找到这个对象。
可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
(finalize方法 对象自救,只能一次)
不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。

以上3种状态,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收

具体过程

判断一个对象ObjA是否可回收,至少要经历两次标记过程:

1.如果对象ObjA到GC Roots没有引用链,则进行第一次标记。
2.进行筛选,判断此对象是否有必要执行finalize()方法
	①如果对象ObjA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过了,
	则虚拟机视为“没有必要执行”,ObjA被判断为不可触及的。
	②如果对象ObjA重写了finalize()方法,且还未执行过,那么ObjA会插入到F-Queue队列中,
	由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行
	③finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记时,
	ObjA会被移除“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,
	finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,
	一个对象的finalize方法只会被调用一次。

清除阶段:标记-清除算法

执行过程 (从根节点开始遍历,标记出可达对象(不被回收的),清除非可达对象)
当堆中的有效空间(available memory)被耗尽的时候,就会停止程序(STW),然后进行两项工作,第一项则是标记,第二项则是清除
标记:collector(迭代器)从引用根节点开始遍历,标记所有被引用的对象。一般在对象的Header中记录为可达对象。
清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

缺点:
效率不算高;(要遍历两次
进行GC时,会STW
这种方式清除出来的空闲内存不是连续的,产生内存碎片。需要维护一个空闲列表

注意:何为清除
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否足够,如果够,就存放

清除阶段:复制算法

将活着的内存空间分成两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
(新生代里面就用到了复制算法,Eden区和from区存活对象整体复制到to区)
(句柄池也是如此)
优点:
没有标记和清除过程,简单实现,运行高效
复制过去后保证空间的连续性不会出现“碎片”问题(指针碰撞)

缺点:
此算法的缺点也是很明显的,就是需要两倍的内存空间(空间浪费
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护refion直接对象引用关系,不管是内存占用或者时间开销也不小。
(改变存活对象的引用地址)

特别的:
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
(如果同年龄的对象大小超过空间的一半,移到old区里)

清除阶段:标记-压缩算法

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的(eden 朝生夕死)。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本很高。时

标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,标记压缩算法由此而生。(指针碰撞)

执行过程:
第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间

标记-压缩算法与标记-清除算法的比较
标记—压缩算法的最终效果等同于标记-清楚算法执行完后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩算法

二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并发的风险决策。

可以看到,标记的存活对象将会被整理,按照内存地址一次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

优点:
消除了标记-清除算法当中,内存区域分散的缺点,(使其内存空间连续,指针碰撞),当需要给新对象分配内存是,JVM只需要持有一个内存的起始地址即可。
消除了复制算法中,内存浪费的缺陷

缺点:
从效率上来说,标记-整理算法要地狱复制算法。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
移动过程中,需要全程暂停用户应用程序。(STW

垃圾回收算法小结

对比三种清除阶段的算法
从效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
而为了尽量兼顾三个指标(速度、空间开销、移动对象),标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记阶段,比标记清除多了一个整理内存的阶段

标记清除标记整理复制
速率中等最慢最快
空间开销少(但会堆积碎片)少(不会堆积碎片)通常需要存活对象的2倍空间(不堆积碎片)
移动对象

分代收集算法

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

在java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

目前几乎所有的GC都采用分代收集算法执行垃圾回收的

年轻代

特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁
这种情况复制算法的回收这里,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此适用于年轻代的回收。而复制算法内存利用率不高的问题,通过两个Survivor的设计得以缓解

老年代

特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁
这种情况存在大量存活率高的对象,复制算法明显不合适。一般是由标记-清除或是标记-整理的混合实现

    Mark阶段的开销与存活对象的数量成正比
    Sweep阶段的开销与所管理区域的大小成正相关
    Compact阶段的开销与存活对象的数据成正比

以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Modify Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理

分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。

增量收集算法

上述的算法中,在垃圾回收过程中,引用软件将处于一种STW,在STW状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收完成。如果垃圾回收时间过长,引用程序被挂起很久,将严重影响用户体验或系统的稳定性
(上述算法都会产生STW,使所有线程被挂起,暂停其他工作,影响系统稳定性)

基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾回收线程只收集一点的内存空间,接着切换到引用程序线程。依次反复,直到垃圾收集完成

总的来说,增量收集算法的基础仍然是冲突的标记-清除和复制算法。增量算法通过对线程间冲突的妥善处理,允许垃圾收集线程分阶段的方式完成标记、清理或复制工作

缺点:
使用指针方式,由于垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程和上下文切换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
(上下文切换频繁,性能下降,吞吐量下降)

分区算法

在相同条件下,堆空间越大,一次GC所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是这个堆空间,从而减少一次GC所产生的停顿
(将堆划分为许多小区间,每次回收相若干个(根据STW的时间长短),减少GC所产生的STW)

分代算法将安装对象的生命周期长短划分为两个部分,分区算法将整个堆空间分成连续的不同大小区间region

每个区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个区间
G1使用这种方式,有维护一个列表,实现有限的stw时间进行最高效的回收

垃圾回收相关概念

System.gc() 的理解

在默认情况下,通过System.gc() 或者是RunTime.getRunTime().gc()的调用,会显示触发Full GC
同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

然而System.gc()调用附带一个免责声明,无法保证堆垃圾收集器的调用

可以通过System.gc()调用来决定JVM的GC行为。一般情况下,垃圾回收是自动进行的,无需手动触发,否则太过麻烦了。在特殊情况下,汝正在编写一个性能基准,在运行之间调用System.gc()

内存溢出与内存泄漏

内存溢出

大多数情况下,GC会进行各种年龄段的垃圾回收,是在不行就Full GC

OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存
(FUll GC后内存依旧不够)

没有空闲内存的情况
Java虚拟机的堆内存设置不够
代码中创建了大量大对象,且长时间不能被垃圾收集器回收(存在被引用)

对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见。尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:PermGen space”。
随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息则变成了:“java.lang.OutofMemoryError:Metaspace”。直接内存不足,也会导致OOM。

这里面隐含着一层意思是,在抛出OutofMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。
  例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等
  在java.nio.Bits.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。

当然,也不是在任何情况下垃圾收集器都会被触发的
  比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutofMemoryError。

内存泄漏

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露。
(具体情况,指针未被完全断开

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄露”。

尽管内存泄露并不会立刻引起程序崩溃,但是一旦发生内存泄露,程序中可用内存就会逐步蚕食,直至耗尽所有内存,最终出现OutOfMemory异常导致程序崩溃。
(存储空间并不是物理内存而是虚拟机大小,这个虚拟机内存大小取决于磁盘交换区设定的大小。)
在这里插入图片描述
举例:
1.单例模式
单例模式的生命周期和应用程序是一样长的,所以单例程序中,如果持有外部对象的引用的话,那么这个外部对象是不能被回收掉的,则会导致内存泄露的产生

2.一些提供close的资源未关闭导致内存泄露
数据库连接(DataSource.getConnection()),网络连接(Socket)和io连接必须手动close,否则是不能被回收的。

代码:

// 使用了单例模式
public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);   //持有外部对象的引用
        }
        return instance;
    }
}

正确的方式应该改为下面这种方式
public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
         // 使用Application 的context 
          this.context = context.getApplicationContext();
  }
    public static AppManager getInstance(Context context) {
          if (instance != null) {
                instance = new AppManager(context);
          }
          return instance;
    }
}

或者这样
...
context = getApplicationContext();
...
//获取全局的context - - >return返回全局context对象
public static Context getContext(){
    return context;
}
public classAppManager{   
    private static AppManager instance;
    private Context context;
    private AppManager() {
         // 使用Application 的context
          this.context = MyApplication.getContext();
}
    public static AppManager getInstance() {
          if (instance != null) {
                instance = new AppManager();
          }
          return instance;
    }
}

Stop the World

简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应。

可达性分析算法中枚举根节点(GC Roots)会导致所有JAVA执行线程停顿。
  分析工作必须在一个能确保一致性的快照中进行
  一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

被STW中断的应用程序线程会在完成GC之后修复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以需要减少STW的发生

(除GC线程外,其他线程都停止,确保一致性)

STW事件和采用哪款GC无关,所有的GC都有这个事件。
哪怕是号称低延时的CMS也不能完全避免STW情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能缩短了暂停时间

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停止。

开发中不要用System.gc();会导致STW的发生。

垃圾回收的并行与并发

并发的概念

在操作系统中,一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都在同一个处理器上运行。

并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,CPU处理的速度非常快,只要时间间隔处理得当。即可让用户感觉是多了应用程序同时在进行。(CPU切换
在这里插入图片描述

并行的概念

当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程。两个进程互补抢占CPU资源,可以同时进行,称之为并行

决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
在这里插入图片描述
并发与并行的对比
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一个时间点上(或同一时刻)同时发生了。

并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的。
只有在多CPU或一个CPU多核的情况下,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的

垃圾回收的并发与并行

并行(parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
  如:ParNew、Parallel Scavenge 、Parallel Old;
串行(Serial):
  相较于并行的概念,单线程执行。
  如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程
在这里插入图片描述
并发(Concurrent):
  指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。
  如:CMS、G1
在这里插入图片描述

安全点与安全区域

安全点概念:(能够进行GC回收的位置)
程序执行时并非所有的地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”

Safe Point的选择很重要,如果太少可能导致GC等待时间太长,如果太频繁可能会导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用循环跳转异常跳转

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
抢先式中断:(目前没有虚拟机采用)
首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点
主动式中断
设置一个中断标值,各个线程运行到 Safe Point 的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起

安全区域(Safe Region)
Safe point机制保证了程序执行时,在不太长的时间内就会偶到进入GC的SafePoint。但是,程序“不执行”的时候,例如Sleep状态或Blocked状态,这时候线程无法响应JVM的中断请求。这种情况下,就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段中,对象的引用关系不会发送变化,在这个区域中的任何位置开始GC都是安全的。可以把Safe Region看做是被扩展的SafePoint。

安全区域的执行流程
1.当线程运行到Safe Region的代码时,首先标识已进入Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region 状态的线程;
2.当线程即将离开safe Region 时,会检查JVM是否已经完成GC,如果完成,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region 的信号为止;

引用概述

Reference子类中只有终结器引用是包内可见的,其他3种引用都可以在应用程序中直接使用

强引用(Strong Reference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj =new Object()”这种引用关系。无论任何情况下只要强引用关系还存在,垃圾收集器就引用不会回收掉被引用的对象
(即使OOM也不会回收)
软引用(Soft Reference):在系统将要发生内存溢出之前,将会把这些对象列入回收服务之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常
(内存不足回收)
弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会收掉被弱引用关联的对象。
(发现即回收)
虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收器回收时收到一个系统通知
(对象追踪回收)

具体使用场景?(面试题)
软,弱引用是在缓存场景下,虚引用是在对象追踪的时候,强引用使用频率最高

强引用(Strong Reference)——不回收

普通系统99%以上都是强引用,也就是最常见的普通对象引用,也就是默认的引用类型

当在java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用

强引用的对象都是可触及的(可达的),垃圾收集器永远不会回收掉被引用的对象

对于一个普通的对象,如果没有其他的引用关系,只要超过引用的作用域或者显式地将相应(强)引用赋值为null,就是可以当做垃圾被收集了,当然具体回收时机还是看垃圾收集策略。

相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成java内存泄露的主要原因之一

代码(例子):

StringBuffer str = new StringBuffer ("Hello");
StringBuffer str1 = str;

局部变量str指向stringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是stringBuffer实例的强引用

对应内存结构:
在这里插入图片描述
在这里插入图片描述
本例中的两个引用,都是强引用,强引用具备以下特点:
强引用可以直接访问目标对象。
强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向的对象

强引用可能导致内存泄露

软引用 ——内存不足即回收

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
(第一次回收,回收掉不可达的对象)

软引用通常用来实现内敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

垃圾收集器在某个时刻决定回收软可达的对象时,会清理软引用,并可现在地把软引用存放到一个引用队列

类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。

软引用:内存足够时不会回收软引用的可达对象;内存不足时,会回收软引用的可达对象

软引用例子(代码):

Object obj = new Object();// 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用

弱引用——发现即回收

弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收只被弱引用关联的对象。

但是,由于垃圾回收器的线程通常优先级别很低,因此,并不一定能快速发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以追踪对象的回收情况。

软引用、弱引用都非常适合来保存那些可有可无的缓存数据。当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而气到加速系统的作用。

弱引用例子(代码):

// 声明强引用
Object obj = new Object();
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //销毁强引用,否则弱引用无法生效

弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易。更快被GC回收

面试题:你开发中使用过WeakHashMap吗?
底层涉及到弱引用WeakReference,使得内存数据在内存不足时,得以及时回收

虚引用——对象回收跟踪

所有引用类型中最弱的一个

一个对象是否存在虚引用,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎一模一样的,随时都可能被垃圾回收。

不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:一个对象被收集器回收时收到一个系统通知
虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
(虚引用必须和引用队列一起使用,当对象还有虚引用时,将虚引用加入引用队列,然后等待GC回收对象时一起回收)

Object obj = new Object();  // 声明强引用
ReferenceQueue phantomQueue = new ReferenceQueue();  // 声明引用队列
// 声明虚引用(还需要传入引用队列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null; 

(一旦GC虚引用对象会添加到引用队列中,等待回收)

终结器引用(了解)

由于实现对象的finalize()方法,也可以称之为终结器引用
无需手动编码,其内部配合引用队列使用
在GC时,终结器引用入引用队列。由Finalizer线程通过终结器引用对象调用它的finalize()方法,第二次GC时才回收被引用的对象

垃圾回收器

垃圾回收器分类

按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器
在这里插入图片描述
Serial
Parallel

串行回收指的是在统一时间内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至结束。

  • 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中
  • 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器
    (可以理解为一个多线程(Serial),一个多线程(parallel))

和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用吞吐量,不过并行回收与串行回收一样,采用独占式,使用了“Stop-the-World”机制。

按工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
独占式垃圾回收器(STW)一旦运行,就停止应用程序中的所有用户线程,知道垃圾回收过程完全结束。

按碎片处理方式分,可以分为压缩式垃圾回收器和非压缩式垃圾回收器
压缩式垃圾回收器会在回收完成后,堆存活对象进行压缩整理,消除回收后的碎片。在分配对象空间使用指针碰撞方式
非压缩式的垃圾回收器不进行这步操作,分配对象空间使用空闲列表

按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。

评估 GC 的性能指标

吞吐量:运行用户代码的时间(程序的运行时间)占总运行时间的比例
(总运行时间:程序的运行时间+内存回收的时间)

垃圾收集开销:吞吐量的补数,垃圾收集所用时间(内存回收的时间)与总运行时间的比例

暂停时间(STW):执行垃圾收集时,程序的工作线程被暂停的时间

收集频率:相当于应用程序的执行,收集操作发生的频率。

内存占用:java堆区所占的内存大小。

快速:一个对象从诞生到被回收所经历的时间。

这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中两项

这三项里,暂停时间(STW)的重要性日益显著。因为随着硬件发展,内存占用多谢越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果

简单来说,主抓两点:
吞吐量
暂停时间

吞吐量(throughput)
吞吐量就是CPU用于运行用户代码的时间与CPU总耗时的比值,即:
吞吐量=运行用户代码时间(运行用户代码时间+垃圾收集时间)

比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

吞吐量 vs 暂停时间

高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快

低暂停时间(低延时)较好,是从用户最终的角度来看,不管是GC还是其他原因导致一个引用被挂起始终是不好的。这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断用户体验。因此,具有较低的暂停时间是非常重要的,特别是对于一个交互式应用程序(就是和用户交互比较多的场景)。

不幸的是“高吞吐”和“低延时”是也对相互竞争的目标(矛盾)。
如果选择以吞吐量优先那么必然需要降低内存回收的执行频率,但只有会导致GC需要更长的暂停时间来执行内存回收。
相反的,如果选择以低延时优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起年轻代内存的缩减和导致程序吞吐量的下降。

在设计(或使用)GC算法时,必须要缺点目标:一个GC算法只能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折中

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

不同的垃圾回收器概述

垃圾收集机制是java的招牌能力,极大地提高了开发效率(面试热点)

垃圾收集器发展

随Jdk1.3一起的是串行方式的SerialGC ,它是第一款GC。ParNew 是Serial收集器的多线程版本
Parallel GC和Concurrent Mark Sweep GC跟随者jdk1.4一起发布
Parallel 在jdk1.6之后成为了HotSpot默认的GC
Jdk1.7版本中,G1可用。
Jdk1.9中,G1替换了CMS
Jdk10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
Jdk11(分水岭)引入ZGC:可伸缩的低延迟垃圾回收器
Jdk12 增强G1。
Jdk13 增强ZGC
Jdk14 删除CMS 。扩展ZGC在macOS和window上的应用

串行回收器:Serial 、Serial Old
并行回收器:ParNew、Parallel Scavenge、Parallel Old
并发回收器:CMS、G1
在这里插入图片描述
7款经典回收器与垃圾分代之间的关系
新生代垃圾回收器:Serial、ParNew、Parallel Scavenge
老年代垃圾收集器:Serial Old、Parallel Old 、CMS
整堆垃圾回收器:G1;

垃圾收集器的组合关系

在这里插入图片描述
ParNew和Parallel都是并行垃圾回收器,底层GC框架不同
导致Parallel与CMS无法共同使用

Serial 回收器:串行回收

Serial收集器是最基本、历史最悠久的垃圾回收器。Jdk1.3之前回收新生代唯一的选择
作为HotSpot中Client模式下的默认新生代垃圾回收器。

Serial收集器采用复制算法、串行回收和“Stop-The-World”机制的方式执行内存回收。

除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。``Serial Old收集器同样采用了串行回收和“Stop-The-World”机制,只不过内存回收算法使用的是`标记-压缩算法

Serial old 是运行在Client模式下默认的老年代垃圾回收器
Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge 配合使用
②作为老年代CMS收集器的后背垃圾收集方案

这个收集器是一个单线程的收集器,“单线程”的意义:他只会使用一个CPU(串行)或一条线程收集去完成垃圾收集工作。更重要的是在垃圾收集时,必须暂停其他工作线程,知道它收集结束(STW)

在这里插入图片描述
Serial回收器的优势:简单而高效(与其他收集器的多线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
  运行在Client模式下的虚拟机是个不错的选择。

在用户的桌面应用场景中,可用内存一般不大,可以在短时间内完成垃圾收集,只要不频繁发生,使用串行回收器是可以接受的。

在HotSpot虚拟机中,使用 -XX:+UseSerialGC 参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial Old GC

总结:
指针垃圾收集器了解即可,现在已经不用串行的了。而且在限定单核cpu才可以使用。(现在都不在是单核的了)

对于交互较强的应用而言,指针垃圾收集器是不能接受的。一般在java web应用程序中是不会采用串行垃圾回收器

ParNew 回收器:并行回收

(Serial 的多线程版本)
Par是Parallel的缩写,New 只能处理新生代

ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、“Stop-the-World”机制。

ParNew是很多JVM运作在Server模式下新生代的默认垃圾回收器

对于新生代,回收次数频繁,使用并行方式高效。
对于老年代,回收次数少,使用串行方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源)
在这里插入图片描述
ParNew 回收器与 Serial 回收器比较
由于ParNew收集器基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?不能
ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速收集,提升程序的吞吐量。
但是在单CPU环境下,ParNew收集器不比Serial收集器更高效。虽然Serial是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的额外开销。(多个线程去抢单个CPU执行权,产生额外开销

除Serial外,目前只有ParNew能和CMS配合工作

设置 ParNew 垃圾回收器
在程序中,开发人员可以通过选项”-XX:+UseParNewGC”手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。

(未来版本中不在使用,从jdk1.9开始)

Parallel 回收器:吞吐量优先

HotSpot的年轻代除了引用ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法并行回收“STW”机制

ParNew收集器不同,Parallel Scanvenge收集器的 目标则是达到一个 可控的吞吐量(ThroughPut),它也被称为吞吐量优先的垃圾收集器

自适应调节策略也是Parallel Scavenge与ParNew一个重要的区别。

高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务(后台运行,没有太多交互)。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算应用程序
(适用于后台,服务台的垃圾收集器(不用考虑用户体验),例如订单处理,工资支付等)

Parallel 收集器在jdk1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器。

Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收“STW”机制

在程序吞吐量优先的应用场景中,Parallel 收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错。
在这里插入图片描述
Java8中默认的垃圾回收器就是Parallel垃圾回收器
(jdk9中默认垃圾回收器为G1)

Parallel Scavenge 回收器参数设置
-XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。
-XX:+UseParallelOldGC : 手动指定老年代都是使用并行垃圾回收器
  分别适用于新生代和老年代
  默认jdk8是开启的。默认开启一个,另外一个也会被开启(互相激活)

-XX:ParallelGCThreads : 设置年轻代并行收集器的线程数。一般最好与CPU数量相等,以避免过多的线程数量影响垃圾收集性能。
  在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量。
  当CPU数量大于8个,ParallelGCThreads的值等于3+[(5*CPU_Count)/8]

-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW时间)。单位是毫秒
  为了尽可能把停顿时间控制在XX:MaxGCPauseMillis 以内,收集器在工作时会调整java堆大小或者其他一些参数。
  对于用户来说,停顿时间越短越好。但是在服务端,注重的是高并发,整体的吞吐量。所以Parallel更适合服务端
该参数使用需谨慎

-XX:GCTimeRatio垃圾收集时间占总时间的比例,即等于 1 / (N+1) ,用于衡量吞吐量的大小。
  取值范围(0, 100)。默认值99,也就是垃圾回收时间占比不超过1。
  与前一个-XX:MaxGCPauseMillis参数有一定矛盾性,STW暂停时间越长,Radio参数就容易超过设定的比例。

-XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge 收集器具有自适应调节策略
  在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
  在手动调优比较困难的场合,可以直接使用指针只适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机直接完成调优工作。

CMS 回收器:低延迟

在jdk1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作

CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
  目前很大一部分的java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

CMS的垃圾收集算法采用标记-清除算法,并且也会“Stop-The-World”

CMS无法与Parallel Scavenge配合工作,所以在jdk1.5中使用CMS来收集老年代的时候,新生代只能现在ParNew或者Serial收集器中的一个。

CMS 工作原理(过程)

在这里插入图片描述
初始标记、并发标记、重新标记、并发清理、重置线程
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。

初始标记阶段:(STW)
在这个阶段中,程序中所有的工作线程都将会因为“STW”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成后就恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快
(标记GC Roots的直接关联对象)

并发标记阶段:
从GC Roots的直接关联对象开始遍历整个对象图的过程,整个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
(从GC Roots的直接对象开始标记GC Roots的间接对象)

重新标记阶段:(STW)
由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,以用户程序继续运作而导致标记参数变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
(标记并发阶段,由于工作线程继续工作而产生的偏差)

并发清除阶段:
清除标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也可以与用户线程并发执行。
(清除标记阶段所标记的垃圾对象)

尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和重新标记这两个阶段中仍然需要执行“Stop-The-World”机制暂停程序中的工作线程,不过暂停时间不会太长,因此可以说目前所有的垃圾收集器都做不到完全不需要“Stop-The-World”,只是尽可能地缩短暂停时间。

由于最耗时的并发标记阶段和并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。

另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收的过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阙值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Modify Failure”失败,这是虚拟机就启动后备方案:临时启动Serial Old收集器来程序继续老年代的垃圾回收,这样停顿时间就更长了。
(堆达到阙值时CMS就启动了,要确保有足够的空间,否则“Concurrent Modify Failure”(垃圾收集速度比内存占用速度慢),Serial Old进行清理)

CMS收集器的垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免的产生内存碎片,那么CMS在位新对象分配内存空间时,将无法使用指针碰撞技术,只能选择空参列表执行内存分配。

为什么 CMS 不采用标记-压缩算法呢?
用标记-压缩算法的话,原线程必须停止工作(STW),标记整理算法将内存重新整合,
(将对象进行偏移,修改对象地址),而此时工作线程正在运行,所以Mark-Compact标记整理算法 只适合于“STW”这种场景下使用
(工作线程执行的过程中对象地址不能更改)

CMS 的优点与弊端
优点

低延迟
并发收集

弊端
会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC
(会产生内存碎片,进而full GC提前了)
CMS收集器对CPU资源非常敏感,在并发阶段,虽然不会导致用户停顿,但因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
(吞吐量低)
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法堆这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而在下次GC时释放这些未被回收的内存空间。
(浮动垃圾:并发标记阶段产生的新垃圾对象

CMS 参数配置
-XX:+UseConcMarkSweepGC:手动指定使用CMS收集器执行内存回收任务。
开启该参数后会自动将-XX:+UseParNewGC 打开。即:ParNew(Young区)+CMS(Old区)+Serial(Old区备选方案)的组合

-XX:CMSInitiatingOccupanyFraction:设置堆内存使用率的阙值,一旦达到该阙值,便开始进行回收。
  Jdk5及以前版本的默认值为68,即当老年代的空间使用率达到68%时。Jdk6即以上的版本默认值为92%

如果内存增长比较缓慢,则可以设置一个稍大的值,大的阙值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显的改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阙值,以避免频繁触发老年代的串行回收器。一次通过该选项可以有效降低Full GC的执行次数

-XX:+UseCMSCompactAtFullCollection:用于指定在执行完FUll GC之后堆内存空间进行压缩整理,以避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的唯一就是STW时间变得更长了。

-XX:CMSFullGCsBeforeCompaction:设置在执行多少次Full GC之后对内存空间进行压缩整理。

-XX:ParallelCMSThreads: 设置CMS的线程数量
CMS默认启动的线程数是 (ParallelGCThreads+3)/4,ParallelGCThreads指的是年轻代并行收集器的线程数,可以当做CPU最大支持的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会变得非常糟糕
  在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量
  当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU_Count]/8]
小结
如果需要最小化地使用内存和并行开销,Serial GC ;
如果需要最大化应用程序的吞吐量。Parallel GC ;
如果需要最小化GC的中断或停顿时间。CMS GC

JDK 后续版本中 CMS 的变化

Jdk9新特性:CMS被标记为Deprecate
如果对JDK9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC 来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。
Jdk14新特性:删除CMS垃圾回收器(JEP363)移除了CMS垃圾收集器
如果在JDK14中使用XX:+UseConcMarkSweepGC的话,JVM不会报错,只是给出一个Warning信息,但是不会exit。JVM会自动退回默认GC方式启动JVM

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值