解释运行-字节码指令编译成机器码
内存管理
即时编译JIT--热点代码进行优化
支持跨平台特性
字节码文件正确的打开姿势
notepad++
无法查看
jclasslib 专业字节码查看工具
字节码文件组成
基础信息:魔数、字节码文件对应的Java版本号,访问标识(public final等等),父类和接口
常量池: 保存字符集常量、类或接口名、字段名,主要在字节码指令中使用
字段:当前类或接口声明的字段信息
方法:当前类或接口声明的方法信息,字节码指令
属性:类的属性,比如源码的文件名内部类的列表等
字节码文件的组成
字节码文件的组成部分_Magic魔数
- 文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容。
- 软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会出错。
字节码文件的组成部分-主副版本号 主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了 45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同 版本的标识,一般只需要关心主版本号。
版本号的作用判断当前字节码版本与运行JDK是否兼容
1.2 版本计算方法:主版本号 - 44 如:JDK8 = 52
案例
解决以下由于主版本号不兼容导致的错误
类文件具有错误的版本 52.0,应为 50.0 请删除该文件或确保该文件位于正确的类路径子目录中。
两种方案: 1.升级JDK版本 2.将第三方依赖的版本号降低或者更换依赖,以满足JDK版本的要求
避免相同内容重复定义,节省空间
- 常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据。
- 字节码指令中通过编号引用到常量池的过程称之为符号引用。
字节码指令位置在Code里面
操作数栈是临时存放数据的地方,局部变量表是存放方法中的局部变量的位置。
案例引入:
public class Test {
public static void main(String[] args) {
int i = 0;
int j = i + 1;
}
}
//对应JVM字节码指令,需要结合下图进行理解
0 iconst_0 //将常量放入[操作数栈]中
1 istore_1 //操作数栈中取出放入到局部变量表1号位置
2 iload_1 //将局部变量表1 中的数据放入操作数栈
3 iconst_1 //将常量1放入操作数栈
4 iadd //操作数栈相加
5 istore_2 //从操作数栈中取出放入局部变量表2号位置
6 return
public class Test {
public static void main(String[] args) {
int i = 0;
int j = i++;
}
}
0 iconst_0
1 istore_1
2 iload_1
3 iinc 1 by 1 //将局部变量表位置1增加1,直接操作局部变量表相加
6 istore_1 //取出操作数栈中的值赋值到局部变量1,
7 return
//结果 i = 0
public class Test {
public static void main(String[] args) {
int i = 0;
i = ++i;
}
}
0 iconst_0
1 istore_1
2 iinc 1 by 1
5 iload_1
6 istore_1
7 return
//结果i = 1
第一个代码的理解图:
局部变量表对应jclasslib位置
字节码常用工具
jdk自带的工具,使用
# 解压jar包
jar -xvf
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。
特性
命令:
# 监控面板 2000ms 一次
dashboard -i 2000 -n 1
# 导出已加载类的字节码到特定目录
dump -d /Users/momoc/Desktop/ com.ismartgo.wxaadmin.App
# 反编译已加载的类源码
jad com.ismartgo.wxaadmin.App
描述一个类加载、使用、卸载的过程
1、是类的加载器根据类的全限定名通过不同渠道以二进制的方式获取字节码信息(本地文件、动态代理生成、网络传输的类)
2、加载完类后,java虚拟机会将字节码信息保存到方法区,生产一个instanceKlass对象,保存类的所有信息,包含特定功能比如多态
3、同时还会在堆中生成一份与方法区中数据类似的Java.lang.Class对象(在Java代码中获取类的信息以及存储静态字段数据,JDK8之后)
instancekclass对堆区的关联关系
查看内存中的对象
推荐使用JDK自带的hsdb工具查看java虚拟机内存信息,JDK下lib文件 sa-jdi.jar
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
验证
是否符合《java虚拟机规范》,四部分
1、文件格式检验,魔数 ,主次版本是否满足
案例
2、元信息验证,如类必须要有父类(super)
3、验证程序执行指令的语义,如果方法内的指令执行中跳转到不正确位置
4、符号引用验证,如是否访问了其他类中的private的方法等
准备
JDK8及之后版本
给静态变量(static)分配内存并设置初始值,如public static int a = 1; 初始值为0,
final修饰的基本数据类型静态变量,准备阶段直接赋值为具体值
解析
将常量池中的符号引用替换成指向内存的直接引用
- 执行静态代码块中的代码,并为静态变量赋值
- 执行字节码文件中clinit部分的字节码指令
案例
操作过程
对调后
结论:clinit方法中的执行顺序与Java中编写顺序是一致
导致类的初始化,clinit方法
- 访问一个类的静态变量或者静态方法, final不会触发初始
- 调用Class.forName(String className)
- new一个该类的对象
- 执行Main方法的当前类
# 打印出加载在并初始化的类
-XX:+TraceClassLoading
不会导致类的初始化情况,clinit方法
- 无静态代码块且无静态变量语句
- 有静态变量的声明,但是没有赋值语句
- 静态变量定义使用final,会在准备阶段初始
面试题
clinit方法,有父类情况
1、子类的初始化clinit调用之前,先初始化父类clinit方法
2、直接访问父类的静态变量,不会触发子类的初始化
注:类的clinit方法仅初始化一次
- Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术
- 只参与加载过程中的字节码获取并加载到内存这一部分
使用场景
JDK8及之前版本默认的类加载器
验证,使用Arthas
# 显示加载器
[arthas@15024]$ classloader
name numberOfInstances loadedCountTotal
# 加载核心类,如String
BootstrapClassLoader 1 2771
com.taobao.arthas.agent.ArthasClassloader 1 1351
# 加载通用的类
sun.misc.Launcher$ExtClassLoader 1 66
# 提升反射性能的加载器
sun.reflect.DelegatingClassLoader 15 15
# 加载第三方和自己编写的的类
sun.misc.Launcher$AppClassLoader 1 4
Affect(row-cnt:5) cost in 2 ms.
启动类加载器 BootstrapClassLoader
- 由Hotspot提供,使用C++编写
- 默认加载java安装目录/jre/lib,比如rt.jar,tools.jar,resources.jar等
//不允许通过Java代码获取Bootstrap ClassLoader
ClassLoader classLoader = String.class.getClassLoader();
//输出Null
System.out.println(classLoader);
使用Arthas验证
[arthas@20156]$ sc -d java.lang.String
class-info java.lang.String
code-source
name java.lang.String
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name String
modifier final,public
annotation
interfaces java.io.Serializable,java.lang.Comparable,java.lang.CharSequence
super-class +-java.lang.Object
# 启动类加载器
class-loader
classLoaderHash null
扩展jar包方式
- 放入jre/lib下进行扩展,不推荐,有校验机制
- 使用参数进行扩展,- -Xbootclasspath/a:E:/devTools/arthas-bin/arthas-boot.jar
扩展类加载器
默认加载Java安装目录/jre/lib/ext下的类文件、
扩展方式:
- 放入/jre/lib/ext下进行扩展
- 使用参数进行扩展, -Djava.ext.dirs=jdk/jre/ext;path/myjar
arthas 查看类的加载列表
# 查看当前虚拟机的类加载器
[arthas@6385]$ classloader -l
name loadedCount hash parent
BootstrapClassLoader 2484 null null
com.taobao.arthas.agent.ArthasClassloader@4caa1fea 1374 4caa1fea sun.misc.Launcher$ExtClassLoader@ca316bd
sun.misc.Launcher$AppClassLoader@18b4aac2 226 18b4aac2 sun.misc.Launcher$ExtClassLoader@ca316bd
sun.misc.Launcher$ExtClassLoader@ca316bd 0 ca316bd null
Affect(row-cnt:4) cost in 7 ms.
[arthas@6385]$ classloader -c 18b4aac2
file:/Users/momoc/Library/Java/JavaVirtualMachines/corretto-1.8.0_292/Contents/Home/jre/lib/charsets.jar
file:/Users/momoc/Library/Java/JavaVirtualMachines/corretto-1.8.0_292/Contents/Home/jre/lib/ext/cldrdata.jar
....
解决一个类到底由谁加载的问题;保证类加载的安全性,包含核心类库,比如java.lang.String;避免重复加载
含义:类加载器接受到加载类的任务是,自底向上查找是否加载过(父类是否已加载,避免重复加载),再由顶向
下进行加载。
问题:
1、如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?
启动类加载器加载,根据双亲委派机制,它的优先级是最高的。
2、Java.lang.String能覆盖么?
不能,会返回启动类加载器加载在rt.jar包中的String类
Java代码主动去加载一个类
方式1:使用Class.forName方法,使用当前类的类加载器去加载指定的类。
方式2:获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载。
父类加载器小细节
应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空,但是在代码逻辑上,扩展类加载器依然会把启动类加载器当成父类加载器处理。 启动类加载器使用C++编写,没有父类加载器。
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
// 父类加载器
private final ClassLoader parent;
验证
# 获取类的加载器父子关系
[arthas@6385]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@ca316bd
+-com.taobao.arthas.agent.ArthasClassloader@4caa1fea
+-sun.misc.Launcher$AppClassLoader@18b4aac2
Affect(row-cnt:4) cost in 5 ms.
面试题:类的双亲委派机制是什么?
1、当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载。
2、应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。
3、双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。
4、classNotFoundException
双亲委派机制核心代码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 父类加载
c = parent.loadClass(name, false);
} else {
//启动类加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//父类加载不到的情况
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
tomcat,每个应用都有自己的独立类加载器,实现应用之间的隔离
第一种方法:自定义类加载器
package classloader.broken;//package com.itheima.jvm.chapter02.classloader.broken;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.ProtectionDomain;
import java.util.regex.Matcher;
/**
* 打破双亲委派机制 - 自定义类加载器
*/
public class BreakClassLoader1 extends ClassLoader {
private String basePath;
private final static String FILE_EXT = ".class";
public void setBasePath(String basePath) {
this.basePath = basePath;
}
private byte[] loadClassData(String name) {
try {
String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
try {
return IOUtils.toByteArray(fis);
} finally {
IOUtils.closeQuietly(fis);
}
} catch (Exception e) {
System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
return null;
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//保证java源的加载
if(name.startsWith("java.")){
return super.loadClass(name);
}
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
BreakClassLoader1 classLoader1 = new BreakClassLoader1();
classLoader1.setBasePath("D:\\lib\\");
Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");
BreakClassLoader1 classLoader2 = new BreakClassLoader1();
classLoader2.setBasePath("D:\\lib\\");
Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");
System.out.println(clazz1 == clazz2);
Thread.currentThread().setContextClassLoader(classLoader1);
System.out.println(Thread.currentThread().getContextClassLoader());
System.in.read();
}
}
默认父类加载器,为AppClassLoader,相关源码
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
两个自定义类加载器加载相同限定类名的类,不会冲突,只有相同类加载器 + 相同限定类名才会认为为一个类。
arthas 验证命令: sc -d
第二种方法: 线程上下文类加载器
JDBC案例 存疑
DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制。
DriverManager使用SPI机制,加载最终jar包对应的驱动类
SPI机制,service Provider Interface,是JDK内置的一种服务提供发现机制
工作原理:
1.在ClassPath路径下的META-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。
2.使用ServiceLoader加载实现类
SPI是如何获取到应用程序类加载器的?
//线程创建时默认的类加载器,为AppClassLoader
Thread.currentThread().getContextClassLoader();
总结
1、启动类加载器加载DriverManager。 2、在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。 3、SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制。
JDBC案例中真的打破了双亲委派机制吗?
整个流程视角:这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制
类加载视角:JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制
第三种方法: OSGi模块化
- 历史上,OSGi模块化框架。它存在同级之间的类加载器的委托加载。OSG还使用类加载器实现了热部署的 功能。
- 热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中。
arthas 热更案例
# 反编译
[arthas@73971]$ jad --source-only com.ismartgo.brandwxa.controller.ul.UlAppSvShopController > ./UlAppSvShopController.java
# 重新编译新代码,
[arthas@73971]$ mc -d /Users/momoc/Desktop/yj /Users/momoc/Desktop/yj/UlAppSvShopController.java
Memory compiler output:
/Users/momoc/Desktop/yj/com/ismartgo/brandwxa/controller/ul/UlAppSvShopController.class
Affect(row-cnt:1) cost in 1349 ms.
# 查看当前类加载器
[arthas@73971]$ classloader -l
name loadedCount hash parent
BootstrapClassLoader 3447 null null
com.taobao.arthas.agent.ArthasClassloader@20671dbd 2103 20671dbd sun.misc.Launcher$ExtClassLoader@3c09711b
lombok.launch.ShadowClassLoader@5541cd0f 242 5541cd0f sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2 8548 18b4aac2 sun.misc.Launcher$ExtClassLoader@3c09711b
sun.misc.Launcher$ExtClassLoader@3c09711b 88 3c09711b null
Affect(row-cnt:5) cost in 9 ms.
# 如果无法编译,则需要指定类加载器
mc -c 18b4aac2 /Users/momoc/Desktop/yj/UlAppSvShopController.java -d /Users/momoc/Desktop/yj/UlAppSvShopController.class
# 热更新文件
[arthas@73971]$ retransform /Users/momoc/Desktop/yj/com/ismartgo/brandwxa/controller/ul/UlAppSvShopController.class
retransform success, size: 1, classes:
com.ismartgo.brandwxa.controller.ul.UlAppSvShopController
注意:
1、程序重启之后,字节码文件会恢复,除非将class文件放入jar包中进行更新。 2、使用retransform不能添加方法或者字段,也不能更新正在执行中的方法。
JDK8及之前的版本
JDK8之后的类加载器
由于JDK9引入了module的概念,类加载器在设计上发生了很多变化。
1、启动类加载器使用Java编写,位于jdk.internalloader.ClassLoaders类中。
Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。 启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。
2、扩展类加载器被替换成了平台类加载器 (Platform Class Loader) 平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了 BuiltinClassLoader, BuiltinClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。
运行java程序过程中的管理的内存区域。
面试题
Java的内存分成几部分?
那些部分内存会溢出?
JDK7和8在内存结构上的区别是什么?
内存调优学习路线
1、了解运行时内存结构
了解JVM运行过程中每一部分的内存结构以及哪些部分容易出现内存溢出
2、掌握内存问题的产生原因 学习代码中常见的几种内存泄漏、性能问题的常见原因
3、掌握内存调优的基本方法 学习内存泄漏、性能问题等常见JVM问题的常规解决方案
也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的的字节码指令的地址。
作用:
-
在加载阶段,虚拟机将字节码文件中的指令读取到内存之后,会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址。如0x000001f248c072c0
-
程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
-
在多线程执行情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继续解释运行。
程序计算器案例
问题:程序计数器在运行中会出现内存溢出吗?
内存溢出:指的是程序在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限。
每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的。
-
采用栈的数据结构来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame) 来保存。
-
执行该方法就入栈,执行完后出栈。
-
Java虚拟机栈随着线程的创建而创建,而回收则会在线程的由于方法可能会在不同线程中执行,每个线程都有自己的虚拟机栈。
栈帧组成
栈帧大小:局部变量表的大小 + 操作数栈大小 + 帧数据
1、局部变量表:局部变量表的作用是在运行过程中存放所有的局部变量
栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot),long和double类型占用两个槽,其他类型占用一个槽。
局部变量表保存的内容有:实例方法的this对象(序号为0),方法的参数,方法体中声明的局部变量。
为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。
总结:
- 局部变量表可以存放this对象(序号0)、方法参数、方法内的定义的变量
- 一个槽四个字节long、double 占两个槽
- 局部变量不生效时,槽可以复用
2、操作数栈:是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
-
栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。
-
在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。
最大深度:操作数栈有多少个数,比如i + 1 需要在操作数栈操作0 + 1。深度为2
3、帧数据:主要包含动态链接、方法出口、异常表的引用
- 当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的內存地址。
- 动态链接就保存了编号到运行时常量池的内存地址的映射关系。
动态链接和解析阶段的区别示例:
假设有两个类:
public class Parent {
public void display() {
System.out.println("Parent display");
}
}
public class Child extends Parent {
@Override
public void display() {
System.out.println("Child display");
}
}
和一个调用方法:
public class Test {
public static void main(String[] args) {
Parent p = new Child();
p.display();
}
}
在这个例子中:
- 类加载阶段:在
Parent
和Child
类首次被加载时,JVM会解析这些类中的符号引用,如方法和字段引用。这是在类加载的解析阶段完成的。 - 运行时的动态链接:在
Test.main
方法中,当p.display()
被调用时,JVM需要确定调用的是Parent
类的display
方法还是Child
类的display
方法。这一决策是在运行时通过动态链接完成的,根据对象的实际类型(即Child
),调用了Child
的display
方法。
方法出口:是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
异常表:存放代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
栈内存
栈溢出: 栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现內存溢出,也就是调用深度过深。
栈内存的默认大小
如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。
linux: X86(64位)1M,PPC:2M
BSD: 64位 2M
solaris: 64位 1M
windwos:操作系统默认值
设置栈内存大小, 可参考官方文档: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
- 要修改Java虚拟机栈的大小,可以使用虚拟机参数-XSs。
- 语法:-Xss栈大小或-XX:ThreadStackSzie=1024
- 单位:字节(默认,必须是 1024 的倍数)、K或者K(KB)、m或者M(MB)、g或者G(GB)
注意事项
-
Hotspot 对栈大小的最大最小值有要求,windows(64)下JDK8 Min:180k,Max:1024m
-
局部变量过多也会影响栈内存的大小
- Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧。
- 在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。
- 本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
- 创建出来的对象都存在于堆上。
- 栈上的局部变量表中,可以存放堆上对象的引用。
- 静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。
堆内存溢出异常:java.lang.OutOfMemoryError
堆空间有三个需要关注的值,used、total、max。 used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大内存。
arthas中的对内存相关功能
-
堆内存used total max三个值可以通过dashboard命令看到。
-
手动指定刷新频率(不指定默认5秒一次):dashboard -i 刷新频率(毫秒)或者memory
total不够用会自动扩展,但不超过max。
当used=max=total,堆内存是否溢出了?不是,堆内存溢出的判断条件比较复杂,在下一章《垃圾回收器》中会详细介绍。
堆大小
默认值:max=1/4,total=1/64
# 设置堆大小
-Xms6G
-Xms1024M
-Xmx1024M
为什么arthas中显示的heap堆大小与设置的值不一样呢?
arthas中的heap堆内存使用了JMX技术中内存获取方式,这种方式与垃圾回收器有关,计算的是可以分配对象的内存,而不是整个内存。
建议设置-Xmx=-Xms,程序启动后可使用总内存就是最大内存,无需向JVM申请。
存放基础信息位置,线程共享,含三大部分
1、类的元信息,保存了所有类的基本信息( instanceKlass)
2、运行时常量池,保存字节码文件中的常量池内容
字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。 当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池
3、字符串常量池(StringTable)
字符串常量池和运行时常量池的关系
StringTable的练习题1,运行时使用StringBuilder连接
练习题2,常量编译阶段直接连接
public static void main(String[] args){
String a = "1";
String b = "2";
String c = "12";
String d = "1" + "2";
// true
System.out.println(c == d);
}
练习3
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
String s1 = new StringBuilder().append("1").append("2").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder().append("ja").append("va").toString();
System.out.println(s2.intern() == s2);
}
结果:false,false
JDK6版本中的intern(),由于字符串常量池不在堆中,所以会拷贝一分字符串到方法区中的字符串常量池。
结果:true,false;
JDK7及之后版本中由于字符串常量池在堆上,所以intern()方法会把第一次遇到的字符串的堆内存地址引用放入字符串常量池。
注意:AmazonCorrettoOpenJDk1.8 实现上与上结果不太一致,结果为true true,想了解自行深入
方法区JDK7/JDK8的小区别
JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。 JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。
arthas查看方法区
- 使用memory打印出内存情况,JDK7及之前的版本查看ps_perm_gen属性
- JDK8及之后的版本查看metaspace属性。
实验-模拟方法区的溢出
通过ByteBuddy框架,动态生成字节码数据,加载到内存中。通过死循环不停地加载到方法区,观察方法区是否会出现内存溢出的情况。分别在JDK7和JDK8上运行上述代码。
ByteBuddy基本使用方法
用于操作生成和操作Java字节码
1、引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>ByteBuddy</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.23</version>
</dependency>
</dependencies>
</project>
2、创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(0)
3、调用visit方法,创建字节码数据
classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, name, null, "java/lang/Object", null);
完整代码,请分别使用JDK1.7和JDK1.8使用
package org.example;
import net.bytebuddy.jar.asm.ClassWriter;
import net.bytebuddy.jar.asm.Opcodes;
import java.io.IOException;
public class Main extends ClassLoader{
public static void main(String[] args) throws IOException {
System.in.read();
Main main = new Main();
int count = 0;
while (true){
String name = "Class" + count;
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, name, null, "java/lang/Object", null);
byte[] byteArray = classWriter.toByteArray();
main.defineClass(name, byteArray, 0 , byteArray.length);
System.out.println(++count);
}
}
}
结论
实验发现,JDK7上运行大概十几万次,就出现了错误。在JDK8上运行百万次,程序都没有出现任何错误,但是内存会直线升高。说明JDK7和JDK8在方法区的存放上,采用了不同的设计。
- JDK7将方法区存放在堆区域中的(PermGen Space)永久代空间,堆的大小由虚拟机参数-XX:MaxPermSize=256m。
- JDK8将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。
- -XX:MaxMetaspaceSize=值将元空间最大大小进行限制。
在JDK 1.4 中引入了 NIO 机制,使用了直接内存,主要为了解决以下两个问题:
1、 Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
2、io操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。
现在直接入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
创建直接内存,ByteBuffer
ByteBuffer direct = ByteBuffer.allocateDirect(size);
Arthas,为direct块
溢出会异常,Java.lang.OutOfMemoryError: Direct buffer memory
调整大小
-XX:MaxDirectMemorySize=1024m
在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。我们称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动收。
内存泄漏:指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。
java内存管理(Garbage Collection : GC)
-
不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。
-
是否回收对象由虚拟机来进行判断
应用场景
1、解决系统僵死的问题
2、性能优化:对垃圾回收器进行合理的设置可以有效地提升程序的执行性能
3、高配面试:常见垃圾回收期、垃圾回收算法、四中引用
判断一个类可以被卸载需要满足下面三个条件
-
此类所有实例对象都己经被回收,在堆中不存在任何该类的实例对象以及子类对象。
-
加载该类的类加载器已经被回收。
-
该类对应的 java.lang.Class 对象没有在任何地方被引用。
#类加载
-XX:+TraceClassLoading
# 类卸载
-XX:+TraceClassUnloading
手动触发回收
调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,体是否需要执行垃圾回收Java虚拟机会自行判断。
System.gc();
引用计数法:为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。
引用内存结构图
如何判断对象的对象可以回收?
图中的实例对象要回收,有两个引用要去除:1.栈中a1变量到对象的引用 2.B对象到A对象的
引用
a1=null,b1.a=null
如果在main方法中最后执行 a1 = null,b1= null,是否能回收A和B对象呢?
可以回收,方法中已经没有使用引用A和B对象
如何判断堆上的对象没有被引用?引用计数法和可达性分析
缺点:
- 每次用和取消引用都需要维护计数器,对系统性能会有一定的影响
- 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收
查看垃圾回收日志
-verbose:gc
可达性分析:算法来判断对象是否可以被回收。可达性分析将对象分为两类: 垃圾回收的**根对象(GC Root)**和普通对象,对象与对象之间存在引用关系
GC ROOT 对象:
(1)线程Thread对象,引用线程栈帧中的方法参数、局部变量等
可达性算法案例分析:分析下面代码中的A实例对象和B示例对象,是如何通过可达性算法判断对象能被回收的?
(2)系统类加载器加载的java.lang.Class对象,引用类中的静态变量
(3)监视器对象,用来保存同步锁synchronized关键字持有的对象
(4)本地方法调用时使用的全局对象
使用arthas,查看GC ROOT对象
(1)heapdump /Users/momoc/test.hprof 将内存快照保存到本地磁盘中
(2)使用MAT工具打开堆内存快照,JDK版本?17?
(3)选择GC ROOT功能查看
总结
- java使用可达性分析算法判断对象是否可以回收
- 四种GC ROOT对象
- 使用MAT打开内存快照方式
可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。
强引用
可达性分析算法判断是否可回收
软引用
弱引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,会将软引用中的数据进行回收。 在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。
无强引用后,即可回收。
GC ROOT对象与SoftReference存在强引用
软引用的执行过程如下: 1将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。
2.内存不足时,虚拟机尝试进行垃圾回收。
3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
4.如果依然内存不足,抛出OutOfMemory异常。
实验,依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>jvm_study</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
</dependencies>
</project>
java代码
package org.example;
import java.lang.ref.SoftReference;
/**
* @author momoc
* @version 1.0
* @className ${NAME}
* @description
* @date ${DATE} ${TIME}
*/
public class SoftReferenceDemo {
public static void main(String[] args) {
//堆内存要设置为200M
byte[] bytes = new byte[1024 * 1024 * 100];
SoftReference<byte[]> softReference = new SoftReference<>(bytes);
//释放强引用
bytes = null;
System.out.println(softReference.get());
byte[] bytes2 = new byte[1024 * 1024 * 100];
System.out.println(softReference.get());
/**
* 结果:
* [B@61bbe9ba
* null
**/
}
}
软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收。如何知道哪些
SoftReference对象需要回收呢?
SoftReference提供了一套队列机制:
1、软引用创建时,通过构造器传入引用队列
2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
3、通过代码遍历引用队列,将SoftReference的强引用删除
代码实验:
package org.example;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
/**
* @author momoc
* @version 1.0
* @className ${NAME}
* @description
* @date ${DATE} ${TIME}
*/
public class SoftReferenceDemo2 {
public static void main(String[] args) {
ArrayList<SoftReference> softReferences = new ArrayList<>();
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[1024 * 1024 * 100];
SoftReference<byte[]> softReference = new SoftReference<>(bytes, referenceQueue);
softReferences.add(softReference);
}
SoftReference<byte[]> ref = null;
int count = 0;
while ((ref = (SoftReference<byte[]>) referenceQueue.poll()) != null){
count++;
}
//count = 9
System.out.println(count);
}
}
弱引用
弱引用的整体机制和软引用基本一致,不管内存够不够都会直接被回收。
在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。
弱引用对象本身也可以使用引用队列进行回收。
虚引用和终结器引用
开发中不会使用!!
虚引用:也叫幽灵引用/影引用,不能通过虚引用对象获取到包含的对象,用于直接内存空间
-
虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。
-
Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
终结器引用:是在对象需要被回收时,对象将会被放置在Finalizer类中的引用队列中,并在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做,如果耗时过长会影响其他对象的回收。
Finalize 只能救活一次,需要理解下面的案例
package chapter04.finalreference;
/**
* 终结器引用案例
*/
public class FinalizeReferenceDemo {
public static FinalizeReferenceDemo reference = null;
public void alive() {
System.out.println("当前对象还存活");
}
@Override
protected void finalize() throws Throwable {
try{
System.out.println("finalize()执行了...");
//设置强引用自救
reference = this;
}finally {
super.finalize();
}
}
public static void main(String[] args) throws Throwable {
reference = new FinalizeReferenceDemo();
test();
test();
}
private static void test() throws InterruptedException {
reference = null;
//回收对象
System.gc();
//执行finalize方法的优先级比较低,休眠500ms等待一下
Thread.sleep(500);
if (reference != null) {
reference.alive();
} else {
System.out.println("对象已被回收");
}
}
}
结果:
finalize()执行了...
当前对象还存活
对象已被回收
1、找到内存中存活对象
2、释放不再存活对象的内存,使得程序能再次利用这部分空间
垃圾回收由单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。
垃圾回收日志参数:-verbose:gc
垃圾回收评价标准
1、吞吐量,吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 =执行用户代码时间/(执行用户代码时间 +GC时间)。吞吐量数值越高,垃圾回收的效率就越高。
2、最大暂停时间STW
3、堆使用效率
不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
总结:上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。 一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。不同的垃圾回收算法,适用于不同的场景。
常见垃圾回收算法
- 标记阶段,将所有存活的对象进行标记。使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
- 清除阶段,从内存中删除没有被标记也就是非存活对象。
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。 缺点:
-
碎片化问题,由于内存是连续的在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
-
分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
- 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间),
- 在垃圾回收GC阶段,将From中存活对象复制到To空间。
优缺点:吞吐量高、无内存碎片、堆内存使用效率低
解决:内存碎片问题
- 标记阶段,将所有存活的对象进行标记。使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
- 整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
优缺点:内存使用效率高、无内存碎片、整理阶段效率不高
内存区域划分:
-
年轻代
-
Eden区
-
幸存区/survivor(s0)
-
S1
-
-
老年代
结构图
分代回收算法流程
- 分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。
- 随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。
- Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。
- 接下来,S0会变成To区,S1变成From区。 当eden区满时再往里放入对象,依然会发生Minor GC。
注意:
- 每次Minor GC 会为对象记录年龄,初值为0,GC完加1。达到条件后对象会晋升到老年代
- 当老年代中空间不足,无法放入新的对象时,先尝试minor gc,如果不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。STW停顿时间较长
- 如果Full GC依然无法回收掉老年代的对象,当对象继续放入老年代时,抛出Out Of Memory异常
- eden和survivor区满了,即时不满足年龄也会放入到老年。
调整内存区域的大小
启动分代GC参数:-XX: +UseSeriralGC 串行垃圾回收器
打印GC详情:-XX:+PrintGcDetail
arthas分代情况,memory
为什么分代GC算法要把堆分成年轻代和老年代?
- 系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收,比如用户获取订单数据,订单数据返回给用户之后就可以释放了。
- 老年代中会存放长期存活的对象,比如Spring的大部分bean对象,启动后就不会被回收
- 在虚拟机的默认设置中,新生代大小要远小于老年代的大小。
分代GC算法将堆分成年轻代和老年代主要原因有
1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。
2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高
3、分代的设计中允许只回收新生代(minorgc),如果能满足对象分配的要求就不需要对整个堆进行回收(fullgc),STW时间就会减少。
垃圾回收器使用组合关系
年轻代-Seiral垃圾回收器
是一种单线程串行回收年轻代的垃圾回收器,
使用:复制算法
作用区域:年轻代
优缺点:单核吞吐量出色,多核会让其他线程阻塞等待
使用场景:硬件配置有限
-XX:+UseSerialGc
老年代-SerialOld垃圾回收器
概述:SerialOld是Serial垃圾回收器的老年代版本,
-XX:+UseSerialGc
采用单线程串行回收 新生代、老年代都使用串行回收器。
使用:标记整理算法
优缺点:单核吞吐量出色,多核会让其他线程阻塞等待
场景:与Serial垃圾回收器搭配使用,或者在CMS特殊情况下使用
GC是具体的垃圾回收器,该图GC非串行回收器
年轻代-ParNew垃圾回收器
ParNew垃圾回收器本质上是对Serial在多CPU下的优化.
-XX:+UseParNewGC
使用多线程进行垃圾回收 新生代使用ParNew回收器,老年代使用串行回收器
算法:复制算法
优缺点:多CPU下STW较短,吞吐量和停顿时间不如G1,JDK9后不建议使用
适用:JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用
老年代-CMS(Concurrent Mark Sweep)
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
-XX:+UseConcMarkSweepGC
# N次FullGC之后,进行内存整理
-XX:CMSFullGCsBeforeCompaction=N
使用:标记清除算法
优点:系统由于垃圾回收出现的STW较短,用户体验好
缺点:
-
碎片化:解决使用参数,-XX:CMSFullGCsBeforeCompaction=N,N次FullGC之后,进行内存整理
-
浮动垃圾:无法在清理过程中,清理新生代
-
退化问题:老年代满了无法分配时,会退化成Seriral Old
适用场景:大型的互联网系统中用户请求数据量大、频率高的场景比如订单接口、商品接口等
执行步骤
1.初始标记,用极短的时间标记出GC Roots能直接关联到的对象。
2.并发标记,标记所有的对象,用户线程不需要暂停。
3.重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。STW
4.并发清理,清理死亡对象,用户线程不需要暂停
年轻代 - Parallel Scavenge 垃圾回收器
Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备省动调整堆内存大小的特点。
-XX:+UseParallelGC
# 最大暂停时间
-XX: MaxGCPauseMillis=n
# 设置吞吐量为n(用户线程执行时间= n/n + 1)
-XX:GCTimeRation=n
# 自动调整内存大小
-XX: +UseAdaptiveSizePolicy
# 打印启动参数,验证自动调整功能
-XX: +PrintFlagsFinal
算法:复制算法
优点:吞吐量高,而且手动可控。为了提高吞吐量;虚拟机会动态调整堆的参数
缺点:不能保证单次STW时间
适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象,比如:大数据的处理,大文件导出
老年代 - Parallel Old
Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。
-XX:+UseParallelOldGC
# 最大暂停时间
-XX: MaxGcPauseMillis=n
# 设置吞吐量为n(用户线程执行时间= n/n + 1),JVM会根据这个大小自动调整堆大小,会减少年轻代内存大小
-XX:GCTimeRation=n
# 自动调整内存大小
-XX: +UseAdaptiveSizePolicy
算法:标记-整理算法
优点:并行收集,在多核CPU下效率较高
缺点:STW长
适用场景:与Parallel Scavenge配套使用
JDK8默认启动参数,可以看到默认使用PS垃圾回收器
momoc@Mac-mini java-common-note % java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=805306368 -XX:MaxHeapSize=12884901888 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_271"
Java(TM) SE Runtime Environment (build 1.8.0_271-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)
Oracle官方建议在使用这个组合PS时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。
G1垃圾回收器,Garbage FIrst,年轻代和老年代
JDK9之后默认的垃圾回收器是G1 (Garbage First)垃圾回收器。
Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小。
CMS关注暂停时间,但是吞吐量方面会下降。
而G1设计目标就是将上述两种垃圾回收器的优点融合:
1.支持巨大的堆空间回收,并有较高的吞吐量。
2.支持多CPU并行垃圾回收。
3.允许用户设置最大暂停时间。
算法:复制算法
优点:
-
堆超过6G的堆回收时,延迟可控
-
不会产生内存碎片
-
并发标记的SATB算法效率高
缺点:JDK8之前不够成熟
相关JVM参数:
# 指定region大小为32M,Region size必须是2的指数幂,取值范围从1M到32M
-XX:G1HeapRegionSize=32m
# 每次垃圾回收时的最大暂停时间毫秒数,默认200ms
-XX: MaxGcPauseMillis=200
# 堆内存占总45会出发MixedGC
-XX: InitiatingHeapOccupancyPercent=45%
# 打开G1回收器
-XX:+UseG1GC
内存结构:
G1的堆被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。
Region=堆空间大小/2048
回收方式:
1、年轻代回收(Young GC),回收Eden和Survivor区,导致STW
执行流程:
1、新创建的对象存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC。
2、标记出Eden和Survivor区域中的存活对象,
3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。Region存活度较低优先清理
4、后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
5、当某个存活对象的年龄到达國值(默认15),将被放入老年代。
6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。
7、多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象Humongous区。采用复制算法来完成。
G1在进行Young GC中会去记录每个Eden区和Survivor区的平均耗时,作为下次回收时的参考依据。
如: -XX: MaxGCPauseMillis=n(默认200)
Region回收个数= 200/回收一个Region
2、混合回收(Mixed GC)
混合回收:初始标记(initial mark)、并发标记 (concurrent mark)、最终标记(remark或者FinalizeMarking)、并发清理 (cleanup) G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高
注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。