Java基础JVM虚拟机
前言:
📣📣📣本篇博文全文大约5万字左右,如果你觉得对你有帮助请给博主一个三连喔,就是对我的最大肯定,最近在学习It老齐的图解轻松学JVM,视频讲解的非常的详细,所以花了三天的时间进行了整理,通过本篇博文进行记录。
本篇博文目录:
-
-
- 一.JVM相关基本概念
-
- 1.JVM与混合语言编程
- 2.JVM发展及种类
- 二.JVM的组成
-
- 1.字节码
-
- (1) 什么是字节码
- (2) 字节码要素
- (3) 字节码指令简介
- (4) 字节码指令分类概述
- 2.类加载执行过程
-
- (1) 加载阶段
- (2) 连接Linking阶段
- (3) 初始化Initialization阶段
- (4) 类加载器种类
- (5) 双亲委派模型
- 3.运行时数据区的组成
-
- (1) 程序计数器
- (2) 虚拟机栈
- (3) 本地方法栈
- (4) 堆Heap
- (5) 方法区
- 三. Grabage Collection(GC垃圾回收机制)
-
- (1) 对象的创建和销毁
- (2) 三种垃圾回收算法
- (3) GC垃圾收集器
- (4) JVM优化
- (5) JVM监控命令
- (6) 调优软件Arthas
-
一.JVM相关基本概念
1.JVM与混合语言编程
( JVM并不只为Java而开发,可理解为是一个跨平台开发的平台,不同的语言通过相应的编译器生成规范一致的.class字节码文件,JVM虚拟机就会去执行该字节码文件
)
2.JVM发展及种类
JVM是一钟规范标准,只要自己编写的虚拟机符合该标准,并且取得相应认证(Oracle TCK验证),就可以认为是一个合格的虚拟机。
目前虚拟机按厂商进行划分可以划分为二类SUN公司与其他厂商:
SUN(Oracle公司)虚拟机各个版本信息如下:
- Sun Classic VM 1.0
- Sun Exact VM 1.2
- Sun Hotspot VM
其他厂商虚拟机信息如下:
- JRockit VM
- IBM J9 VM
- Taobao VM
Oracle/SunJDK与OpenJDK的异同:
二.JVM的组成
1.字节码
(1) 什么是字节码
- java源码经过javac编译生成的二进制(文件),称为字节码(文件)
- JVM通过字节码保证平台无关特性
- Java并不是唯一生成class文件的语言
- Class是结构紧凑的二进制流,其格式固定,要求严谨
(2) 字节码要素
字节码的组成结构:
魔数
魔数就是区分文件类型的依据
例子:
OxCAFEBABE是Java字节码文件的魔数,保存在前4个字节
实操:使用winhex软件查看文件的魔术:
文件版本号
- 5~6字节是次要版本号
- 7~8字节是主要版本号,Java 8 = 52.0
实操查看JVM版本(从下图可知Java版本为1.5):
我们也可以通过 javap -v -l -c
命令进行查询(下图查询的字节码文件是通过Java8编写):
注意: 字节码版本高于JVM版本,产生Unsupported ClassVersionError
为了更好的观察字节码文件可以使用IDEA JClassLib插件,安装过程如下:
构建项目:
先选择要查看的字节码文件
选择View --> Show Bytecode With Jclasslib
常量池
通过IDEA JClassLib插件观看字节码中常量池信息:
在一般信息这里可以看到常量池计数为84
常量池中显示的下标从1开始到83结束,但是在一般信息中却显示有84个信息,这是因为有一个0下标为状态位预留的位置。
ConstantPoolSample类中的字符串信息如下(勾选不重复字符串)
我们看看常量池中字符串信息(刚好有六个):
并且这六个字符串就是我们在ConstantPoolSample类中勾选的不重复字符串
但是在保存的时候字符串并没有保存对应的字面量(字面量就是final修饰的数值和相关的字符串的值如: ),而是保存对应的地址(一种关联关系)如下图中的
cp_info #58
该地址下就存放了该字符串对应的详细数据:
符号引用就是上面这种关联关系:
ConstantPoolSample类中与类相关的信息如下图:
在常量池中类信息包含显示和隐式,并且也是采用关联关系
如字符串拼接实际上采用的是StringBuilder类进行字符串拼接
ConstantPoolSample中的字段信息
常量池中也对应了相关字段信息:
该字段也是采用符号引用
数据类型对应的也是一个符号引用
这里的D表示的就是double
ConstantPoolSample类中的方法信息如下:
常量池中方法的信息如下:
相关类名,名字和描述符都是采用符号引用
还存在一些底层调用的方法如StringBuilder进行拼接调用的方法:
还包括构造方法,信息如下:
访问标志
- 类访问标志
ACC_SUPER+ACC_PUBLIC ==>0x0020+0x0001=0x0021
- 字段访问标志
ACC_PRIVATE+ACC_FINAL==>0x0002+0x0010=0x0012
- 方法访问标志
ACC_PUBLIC+ACC_STATIC===>0x0001+0x0008=0x0009
类/父类索引与接口集合
- 类/父类索引
由于ConstantPoolSample没有使用关键字extends继承其他类,所以默认父类为Object
- 接口集合
ConstantPoolSample没有使用接口所以接口数为0
这里编写二个接口A,B并且让ConstantPoolSample去实现二个接口
然后再进行编译再次查看一般信息,此时接口数就变为2:
可以在接口中进行查看:
字段/方法/属性表
- 字段表 - 描述接口或类中声明的字段(类变量 - static修饰和实例变量)
- 方法表 - 描述接口或类的实例方法与静态方法
- 属性表 - 为类/字段/方法提供更为详细的辅助信息
(3) 字节码指令简介
• 字节码指令是包含在字节码的指令
• 字节码指令将源码编译时由编译器生成保存在Method描述中
• 字节码与平台无关,运行时JVM读取后翻译各平台底层指令
• 字节码指令总数不超过256个
字节码指令格式:
字节码指令 [参数列表]
例如:
- 执行StringBuilder对象的append方法
invokevirtual #8 //cp_info#8 : java/lang/StringBuilder.append
- 实例化新的StringBuilder对象
new #6 //cp_info#6 :
IDEA JClassLib插件中查看字节码指令:
(4) 字节码指令分类概述
加载与存储指令中前面的字母分别表示数据类型( 挺重要
):
字节码指令图:
2.类加载执行过程
• 类加载子系统负责从文件或者网络加载Class字节流
• 类加载子系统会读取字节码中的信息,运行时存储到JVM内存
• 任何Class要被类加载子系统加载,都要符合JVM字节码规范
类加载过程如下:
(1) 加载阶段
• 读取字节码二进制流
• 解析字节码二进制流的静态数据转换为运行时JVM方法区数据
• 生成类的java.lang.Class对象,放入堆中,作为方法区的访问入口
• 在加载类过程中,必然会触发父类加载
示意图:
字节码的常见来源
• 编译后本地.class文件
• 网络传输获取二进制流
• Jar/War包中解压后读取
• 动态运行生成,JDK动态代理/CGLib
Class实例何时被创建
-
new 实例化
A a= new A() -
反射
Class clzA = Class.forName(“com.itlaoqi.A”) -
子类加载时父类同时加载
-
JVM启动时,包含main方法的主类
-
1.7的动态类型语言支持
https://www.infoq.cn/article/jdk-dynamically-typed-language/
(2) 连接Linking阶段
• 验证Verify:确保字节码符合虚拟机要求
• 准备Prepare: 为字段赋予初始值
• 解析Resolve: 符号引用转换为直接引用
验证阶段(针对字节码二进制流进行处理):
准备阶段:
为类变量static赋予初始值
例子:
数据类型默认初始值:
解析阶段:
• 将字节码符号引用转换为直接引用(包括类解析,字段解析,方法解析,接口解析)
• 说人话:将字节码的静态字面关联转换JVM内存中的动态指针关联
(3) 初始化Initialization阶段
• 初始化阶段是执行类构造器方法 ()
的过程
• ()
方法用于完成类的初始化操作
• ()
方法并不需要显式声明,由Java编译器自动生成
备注:加载/验证/准备/解析是由虚拟机主导与代码无关;初始化则是通过代码生成clint,完成类初始化过程。
初始化知识点:
• 初始化阶段对类(静态)变量赋值与执行 static
代码块
• 子类初始化过程会优先执行父类 ()
• 没有类变量及 static{}
代码块就不会产生 ()
• -XX:+TraceClassLoading
查看类加载过程
• ()
方法默认会增加同步锁,确保 ()
只执行一次
知识点1:初始化阶段对类(静态)变量赋值与执行 static
代码块
代码详情:
看看加载阶段的指令,确实对静态变量进行赋值了:
知识点2:子类初始化过程会优先执行父类 ()
运行效果:
知识点3:没有类变量及 static{}
代码块就不会产生 ()
知识点4: -XX:+TraceClassLoading
查看类加载过程
点击ok,然后运行ClassInitSample项目:
知识点5: ()
方法默认会增加同步锁,确保 ()
只执行一次
(4) 类加载器种类
( 加载器关系为上下级,而非继承
)
- 启动类加载器
什么是沙箱机制:
上图来源于:https://blog.csdn.net/weixin_41490593/article/details/99412315
代码实例:
package com.itlaoqi.classloader;import sun.security.ec.SunEC;/** * 类加载器案例 */public class ClassLoaderSample { public static void main(String[] args) { //启动类加载器,因为启动类加载器使用C语言编写,没有被JVM管理,所以启动类加载器返回null ClassLoader bootstrapClassLoader = Object.class.getClassLoader(); System.out.println(bootstrapClassLoader);//null }}
运行效果( 启动类加载器,因为启动类加载器使用C语言编写,没有被JVM管理,所以启动类加载器返回null
):
- 扩展类加载器
代码实例:
package com.itlaoqi.classloader;import sun.security.ec.SunEC;/** * 类加载器案例 */public class ClassLoaderSample { public static void main(String[] args) { //扩展类加载器 ClassLoader extClassLoader = SunEC.class.getClassLoader(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader }}
SunEC类就在…/jre1.8/lib/ext文件下:
运行效果:
- 应用程序(系统)类加载器
代码实例:
package com.itlaoqi.classloader;import sun.security.ec.SunEC;/** * 类加载器案例 */public class ClassLoaderSample { public static void main(String[] args) { //对于用户自定义类来说:默认使用应用程序类加载器进行加载 ClassLoader appClassLoader = ClassLoaderSample.class.getClassLoader(); System.out.println(appClassLoader);//sun.misc.Launcher$AppClassLoader }}
运行效果:
- 自定义加载器
面试题:Class实例在JVM是唯一的吗?
自定义ClassLoader三要素:
- 继承自ClassLoader,重写findClass()
- 获取字节码二进制流
- defineClass加载生成Class实例
自定义加载器1代码:
package com.itlaoqi.classloader.custom;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;/** * 自定义ClassLoader三要素: * 1. 继承自ClassLoader,重写findClass() * 2. 获取字节码二进制流 * 3. defineClass加载生成Class实例 */public class MyClassLoader1 extends ClassLoader { private final String CLASS_PATH = "c://ClassSample"; protected Class<?> findClass(String name) { try { //获取字节码二进制流 FileInputStream in = new FileInputStream(this.CLASS_PATH); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int len = -1; while ((len = in.read(buf)) != -1) { baos.write(buf, 0, len); } in.close(); byte[] classBytes = baos.toByteArray(); //加载Class字节码 return defineClass(classBytes, 0, classBytes.length); } catch (Exception e) { e.printStackTrace(); } return null; }}
ClassSample位于C盘下:
自定义加载器2代码:
package com.itlaoqi.classloader.custom;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;/** * 自定义ClassLoader三要素: * 1. 继承自ClassLoader,重写findClass() * 2. 获取字节码二进制流 * 3. defineClass加载生成Class实例 */public class MyClassLoader2 extends ClassLoader { private final String CLASS_PATH = "d://ClassSample" ; protected Class<?> findClass(String name) { try { FileInputStream in = new FileInputStream(this.CLASS_PATH) ; ByteArrayOutputStream baos = new ByteArrayOutputStream() ; byte[] buf = new byte[1024] ; int len = -1 ; while((len = in.read(buf)) != -1){ baos.write(buf , 0 , len); } in.close(); byte[] classBytes = baos.toByteArray(); return defineClass( classBytes , 0 , classBytes.length) ; } catch (Exception e) { e.printStackTrace(); } return null ; }}
ClassSample位于D盘:
二个不同的加载器加载相同实例:
代码如下:
package com.itlaoqi.classloader.custom;public class Application { public static void main(String[] args) throws ClassNotFoundException { ClassLoader c1 = new MyClassLoader1() ; //利用自定义加载器1加载对象\ //调用ClassLoader.loadClass()加载字节码会自动调用findClass方法 Class<?> clz1 = c1.loadClass("ClassSample"); System.out.println(clz1.getClassLoader() + "|hashcode:" + clz1.hashCode()); ClassLoader c2 = new MyClassLoader2() ; //利用自定义加载器1加载对象 Class<?> clz2 = c2.loadClass("ClassSample"); System.out.println(clz2.getClassLoader() + "|hashcode:" + clz2.hashCode()); Class<?> clz3 = c2.loadClass("ClassSample"); System.out.println(clz2.getClassLoader() + "|hashcode:" + clz3.hashCode()); System.out.println("结论:同一个Class被不同的类加载器加载后在JVM中产生的类对象是不同的"); System.out.println("推导:在同一个类加载器作用范围内Class实例加载时才会保持唯一性"); }}
运行效果( 同一个Class被不同的类加载器加载后在JVM中产生的类对象是不同的,在同一个类加载器作用范围内Class实例加载时才会保持唯一性
):
(5) 双亲委派模型
加载类时加载器逐级将加载任务向上委派至引导类加载器.然后逐级向下尝试加载,直至加载完成.
优点:
• 双亲委派机制保护了类不会被重复加载(子类加载,优先加载父类,Object类是所有类的父类,避免了每次都需要加载object)
• 加载机制提供沙箱机制,禁止用户污染java开头核心包
实例验证( 加载机制提供沙箱机制,禁止用户污染java开头核心包
):
运行效果:
3.运行时数据区的组成
线程私有
- 程序计数器 - 存储线程执行位置
- 虚拟机栈 - 存储Java方法调用与执行过程的数据
- 本地方法栈 - 存储本地方法的执行数据
线程共有
- 堆 - 主要存储对象
- 方法区 - 存储类/方法/字段等定义(元)数据
- 运行时常量区 - 保存常量static数据
(1) 程序计数器
为什么会有程序计数器?
使用程序计数器可以记录指令执行位置,比如下图中单线程通过划分多个时间片实现多线程工作,上一个线程执行指令到哪了,就可以通过计数器知道,然后执行下一个地址的指令。
关于执行native方法计数器为空?
使用native方法,我们知道native方法都是通过和操作系统进行绑定的,如windows下的库大多数调用C/C++库方法,C/C++方法都是运行在C语言的运行内存中,和Java运行内存并不是一回事,所以计数器为空。
(2) 虚拟机栈
什么是栈?
栈是一种数据结构,是一种连续紧密存储结构,出入顺序先进后出,二种操作入栈和出栈。
Data1和Data2数据块入栈
Data1和Data2入栈结果如下:
Data2出栈Data3入栈:
Data2出栈Data3入栈效果如下:
什么是虚拟机栈?
① 虚拟机栈(栈)保存方法的调用过程
② 栈说明了程序运行中的瞬时状态
③ 栈是线程私有的,生命周期与线程相同
④ 每次方法的调用,都会产生对应栈帧
实例练习:
程序运行调用main()方法,栈帧main入栈,处于栈底,执行main()方法的代码,调用method1(),栈帧method1入栈,执行method1()代码,调用method2(),栈帧method2入栈执行method2()代码,没有方法执行了开始出栈(
按照先进后出顺序出栈
),栈帧method2出站,然后栈帧method1出栈,最后栈帧main出栈,执行完毕,线程销毁,整个过程就是栈的生命周期。
虚拟机栈的特点
① 线程私有的
② 不会被垃圾回收
③ 栈的生命周期和线程相同
④ 栈深度有限制
设置虚拟机栈的空间
① Java1.5后默认每个栈空间为1MB,之前版本256KM
② Java启动参数: -Xss 数值[k|m|g] 设置栈的空间
③ 栈分配的内存决定了栈的最大深度
虚拟机栈有两种空间设置
① 固定长度(推荐): 达到上限,StackOverflowError
② 动态扩展: 可用内存不足,OutOfMemoryError(不推荐使用
)
实例练习设置固定长度:
执行如下代码:
package com.itlaoqi.runtime.stack;/** * 演示栈溢出StackOverflowError */public class StackOverflowSample { private static long count = 0; public static void main(String[] args) { test(); } public static void test(){ count++; int a,b,c,d,e,f,g,h,i,j,k=0; System.out.println("正在第" + count + "次调用方法"); test(); }}
运行效果:
设置栈的固定长度:
输入数值: -Xss10m( 格式:-Xss 数值[k|m|g] k:KB,m:MB,g:GB
)
再次运行(执行方法次数明显增加了):
局部变量也会影响栈的容量(将上面代码中的局部变量注释掉,次数明显有增加):
栈的容量要么采用默认1MB或者使用512KB
备注:动态扩展会消耗内存容量,如果调用死循环,会使一些原本可以运行的程序造成崩溃。
虚拟机栈的栈帧
栈帧由
局部变量表
,操作栈
,动态链接
,返回地址
四部分组成。
① 局部变量表:局部变量
② 操作数栈:保存中间计算的临时结果
③ 动态链接:将符号引用转为直接引用
④ 返回地址:存放调用方法的程序计数器值
栈帧的组成:
局部变量表
局部变量表按定义顺序存储两块内容:存储方法参数和存储方法内的局部变量
局部变量表特点
① 线程私有,不允许跨线程访问,随方法调用创建,方法退出销毁
② 编译期间长度已确定,局部变量元数据存储在字节码中
③ 局部变量表是栈帧最主要的存储空间,决定了栈的深度
实例练习:
测试代码如下:
public class LocalVariableTableSample { public LocalVariableTableSample(){ this.slotSample1(); } public static String staticMethod(String name,int offset){ String ret = "hello " + name; int count = 100 + offset; return ret; } public String instanceMethod(String name,int offset){ String ret = "hello " + name; int count = 100 + offset; return ret; } public void slotSample1() { int a = 1; float b = 0f; boolean c = true; long d = 100; double e = 100; String f = ""; } public void slotSample2() { int a = 1; if(1==1){ int b = 0; b = a + 1; } int c = 100; } public static void main(String[] args) { LocalVariableTableSample.staticMethod("Lily" , 100); LocalVariableTableSample instance = new LocalVariableTableSample(); instance.instanceMethod("Andy" , 200); }}
观察main栈帧(一个方法的调用对应一个栈帧):
上图中的LineNumberTable叫做行号表,说明了字节码指令和源码行号的对应关系
上图中的LocalVariableTable在编译期间就保存在字节码中,长度固定,方法调用时随栈帧创建并载入,LocalVariableTable中单个变量的序号(index)叫做Slot( 存储局部变量的单位为Slot(变量槽)
)
上图中的局部变量对应源码中如下:
局部变量的作用域由起始PC和长度决定,通过字节码指令查看
比如name,offset,起始PC为0长度为27(0-26),对应字节码如下:
又比如ret变量,起始PC为20长度为7(20-26),如下:
LocalVariableTable在构造方法与实例方法中,0号槽位(Slot)默认为this,指向当前类实例
static方法没有this关键字,所以在static方法中不能够使用this关键字
槽(Slot)按类型区分,32位以内类型(int/float/char/…/引用类型)占用1个Slot,64位类型(long/double)占用2个Slot:
上图对应下图:
当槽位有空余时,后产生的局部变量会重用之前的槽位,此特性称为"槽复用"
实例:
字节码局部变量表
代码如下:
start PC=0从字节码局部变量表可知,该solotSample2是一个实例方法,0卡槽为this
Start PC =2,从字节码局部变量表可知,卡槽1为a
start PC=4,从字节码局部变量表可知,卡槽2为b
由于length=4所以当start PC=7的时候b字段销毁
继续执行start PC=11时,卡槽2(index=2)为c,(这里就是因为卡槽复用所以卡槽2=c),然后执行完毕
操作数栈
字节码指令在执行过程中的中间计算过程存储在操作数栈。
通过字节码指令分析执行过程掌握操作数栈知识:
源代码:
public class OperandStackSample { public int compute1(){ short i = 10; int j = 18; int k = i + j; return k; }}
字节码指令表:
compute1()方法是一个实例方法,所以局部变量表中索引0为this
执行指令 0 bipush 10 将整型进行操作数栈入栈
2 istore_1 栈顶 出栈
保存至局部变量表1号槽
3 bipush 18和5 istore_2指令和上面操作一致,不做解释:
6 iload_1
将 1
号槽局部变量压入栈顶
7 iload_2:
8 iadd:iadd将栈顶与第二位做加运算存至栈顶
9 istore_3和10 iload_3:
11 ireturn :ireturn将栈顶数据出栈进行返回
控制台输出28
64位数据类型占两个栈深度( 在局部变量表中64位数据类型占二个Slot,在操作数栈中64位数据类型占二个栈深度
):
下图栈深度为4
动态链接
将保存在字节码文件中
符号引用
转为运行时的内存直接引用
。
符号引用就是通过字面量进行描述该数据存放的位置,该数据(元数据,存放在运行时常量区中),而栈帧中的动态链接就是对应的内存指针,就是对元数据的直接引用。
方法返回地址
保存该方法调用者的程序计数器值,用于方法正常执行后后续执行。
执行下图s1方法,当进入方法体时执行第一条打印语句,执行完之后遇见了this.s2();进入到s2()方法中,当s2()方法被执行完后,再次回到s1中执行剩下的代码,s2()方法执行完后是怎么知道s1中剩下代码从什么时候开始执行的呢?,其实就是通过方法返回地址确认后续执行。
(3) 本地方法栈
native本地方法:
① 一个native方法就是一个Java调用非Java代码的接口
② 在定义一个native方法时,并不提供实现体
③ native的可以调用其他语言接口实现对操作系统更底层的操作
本地方法栈图如下:
本地方法栈特点:
① 在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定
② Sun HotSpot虚拟机把本地方法栈和虚拟机栈合二为一
(4) 堆Heap
什么是堆Heap?
① 堆Heap是JVM最核心的内存区域,存放运行时实例化的对象实例
② 堆在JVM启动时就被创建,空间也被分配.是JVM内存的主要占用区域
③ 堆是线程共享的,堆内存在物理上可以是分散的,在逻辑上是连续的(堆中包含线程私有的缓冲区(TLAB)用于提高JVM的并发处理效率)
引用类型与堆的关系
① 引用类型本质是指针,指向存放在堆中的对象实例地址
② 堆是垃圾回收(GC)的重点区域,方法结束后堆对象不会被立即清除,在GC时才会被清理
执行如下代码实际上就是在堆上开辟了一个空间,存放B类对象,然后在变量表中b变量(栈帧)指向堆中的B类对象。
public void test(){B b = new B();}
堆结构
新生代
:是用来存放新生的对象,该区域对象会被频繁GC
老年代
:新生代保存的稳定对象放入老年代,老年代不会频繁执行GC
元空间
:内存的永久保存区域,主要存放Class元数据,几乎不会GC
堆( JDK1.8
)的结构如下图所示:
堆空间
默认堆空间的大小
初始内存大小:物理电脑内存大小 / 64
最大内存大小:物理电脑内存大小 / 4
执行如下代码可以在控制台输出初始内存和最大内存信息:
public class HeapSpaceSample { public static void main(String[] args) { //返回Java虚拟机中的堆内存总量 long usedMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; //返回Java虚拟机试图使用的最大堆内存量 long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; System.out.println("堆当前占用 : " + usedMemory + "M"); System.out.println("堆最大内存 : " + maxMemory + "M"); /*try { Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); }*/ }}
运行效果:
我的电脑内存可用为7.85GB
通过上面的公式,算出预期的初始内存大小为125.6MB,最大内存大小为2009.6MB。
备注: 操作系统自身会占用一些空间,JVM分配内存的时候也会拿出来一些作为保留,所以实际分配的内存比我们预期的内存要少一些
年轻代与老年代的占用比例
① 年轻代固定占用1/3
② 老年代固定占用2/3
控制台打印GC详细信息:设置 -XX:+PrintGCDetails
参数
运行效果:
设置堆空间大小的参数
设置参数(m:MB,g:GB):
① -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
② -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
设置建议:
① 开发中建议将初始堆内存和最大的堆内存设置成相同的值。
② Java整个堆大小设置建议,Xmx 和 Xms设置为老年代FullGC后存活对象的3-4倍
设置初始内存大小上限和最大内存上限为1024MB
运行效果:
VisualVM安装使用
VisualVM安装使用教程:VisualVM安装,插件安装,各个面板信息讲解
对象分配一般过程
① 绝大多数刚创建的对象会存入新生代的Eden伊甸园区
② Eden与S0/S1的内存分配比例是8:1:1
新生代进入老年代过程
- 一般情况
当Eden第1次满MinorGC对Eden/S0/S1进行GC:当实例化对象的时候(有new操作时),优先将对象放入Eden区,当Eden区的对象满时,先将还存在引用的对象通过复制交换算法进入到S0中设置age=1,最后对Eden和S1中未引用对象进行GC操作( Eden满时,针对年轻代进行的垃圾回收称为YGC/MinorGC
)。
GC之后:
Eden第2次满,对Eden/S0/S1进行MinorGC:Eden第2次满时,Eden将有引用关系的对象转移至S1区,设置对象age=1,原S0(From区)对象转移至S1(To区),且age+1,对Eden和S0进行GC操作。
GC之后:
Eden第3次满,对Eden/S0/S1进行MinorGC:Eden第3次满时,Eden将有引用关系的对象转移至S0区,设置对象age=1,原S1(From区)对象转移至S0(To区),且age+1,对Eden和S1进行GC操作。
GC之后:
n次Minor GC后当对象age超过15(阈值)对象晋升(Promotion)至老年代:
- 特殊情况(GC整个流程图)
Minor GC / Full GC / Major GC分别是什么?
( 任何GC行为都会触发STW(Stop The World全局停顿)
)
① Minor GC(YGC) 针对年轻代进行回收,执行效率高。
② Full GC,全堆回收,针对年轻代/老年代/方法区进行全面收集,执行效率低下,会导致系统长时间停滞,减少Full GC是JVM优化的重点。
③ Major GC,针对老年代回收,目前只有CMS收集器才存在Major GC。
为什么要对象分代(年轻代,老年代)?
① 堆中绝大多数对象”朝生夕灭” 出于性能考虑,
② 按照年龄age按区域进行划分,缩小内存扫描的范围
③ 针对不同特性的对象,采用不同的垃圾收集,最大程度提高执行效率
代码实例练习,GC日志分析:
public class HeapObjectSample { /** * VM参数:-Xms60M -Xmx60M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * -Xms60M -Xmx60M 堆内存总大小60mb * -Xmn10M 强制设置新生代10mb,剩余50mb分给老年代 * -XX:SurvivorRatio=8 设置Eden与Survivor比例为8:1:1 ,即eden为8mb / S0与S1各1mb * -XX:+PrintGCDetails 打印详细GC日志 * * 日志分析: * [GC (Allocation Failure) [PSYoungGen: 8178K->1002K(9216K)] 8178K->4928K(60416K), 0.0018061 secs] * GC (Allocation Failure) : Allocation Failure说明触发GC的原因是Eden空间不足 * 8178K->1002K(9216K):GC前年轻代空间->GC后年轻代空间(年轻代最大占用空间) * PS:为什么年轻代最大空间是9216K,不是10240(10mb) * 答:年轻代组成为 Eden(8mb)+ 2*(Survivor(1mb)) ,但两个S区只有一个有数据,所以在内存分配时只需分配1个即:8mb+1mb=9216k * 8178K->4928K(60416K):GC前堆占用空间->GC后堆占用(堆最大占用空间) * 0.0018061 secs: 本次GC使用时间 * * [Full GC (Ergonomics)[PSYoungGen: 8192K->8177K(9216K)] [ParOldGen: 51184K->51184K(51200K)] 59376K->59361K(60416K), [Metaspace: 9320K->9320K(1058816K)] * Ergonomics(GC收集器策略执行) 年轻代 老年代 元空间(方法区) * [Full GC (Allocation Failure) ... * Allocation Failure: 堆空间分配失败,会抛出OOM */ public static void case1() { List list = new ArrayList(); while (true) { try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } list.add(new byte[1024 * 50]); } } public static void main(String[] args) { case1(); }}
设置VM参数:-Xms60M -Xmx60M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
- -Xms60M -Xmx60M 堆内存总大小60MB
- -Xmn10M 强制设置新生代10MB,剩余50MB分给老年代
- -XX:SurvivorRatio=8 设置Eden与Survivor比例为8:1:1 ,即eden为8MB / S0与S1各1MB
- -XX:+PrintGCDetails 打印详细GC日志
运行效果:
分析执行情况:
GC (Allocation Failure):
- GC (Allocation Failure) : Allocation Failure说明触发GC的原因是Eden空间不足
- 8182K->1016K(9216K):GC前年轻代空间 -> GC后年轻代空间(年轻代最大占用空间)
- 8182K->6838K(60416K):GC前堆占用空间->GC后堆占用(堆最大占用空间)
- 0.1999019 secs: 本次GC使用时间
为什么年轻代最大空间是9216K,不是10240(10mb)?
答:年轻代组成为 Eden(8mb)+ 2*(Survivor(1mb)) ,但两个S区只有一个有数据,所以在内存分配时只需分配1个即:8mb+1mb=9216k。
Full GC (Ergonomics) :
- Full GC (Ergonomics): Ergonomics(GC收集器策略执行)
- Full GC (Allocation Failure):堆空间分配失败,会抛出OOM
visualvmGC执行情况如下:
(5) 方法区
① 方法区是线程共享区域,是物理分散存储而逻辑为整体的内存区域
② 方法区保存的内容:类加载器信息/类信息(包含字段/方法)/常量/即时编译器编译后的代码缓存/静态变量(1.6版本前,之后存放在堆中)
③ 方法区是个概念,JVM对方法区如何实现做更多要求.
永久代与元空间的区别
规范说明方法区是堆的逻辑部分,在实现中与堆内存无关,称为”非堆Non-heap”。
设置元空间
运行时常量区
方法区的历史变化
方法区是观念,HotSpot独有的永久代是方法区的实现.1.8被淘汰
- JDK6
- JDK7
- JDK8
new对象永远放在堆中
( new 对象的实例不管在JDK1.6之前还是1.7之后都是放在堆中
)
三. Grabage Collection(GC垃圾回收机制)
(1) 对象的创建和销毁
创建对象的几种方式
- 用new语句创建对象
- 运用反射手段(调用Java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。)
- 调用对象的clone()方法。
- 运用反序列化手段(调用java.io.ObjectInputStream对象的readObject()方法。)
对象销毁
什么条件下对象才是垃圾?
当对象没有任何引用的时候,就代表对象无用了.
引用链路断开,后续对象也会成被标记为垃圾
如何发现对象已经是垃圾?
- 引用计数(Reference Count)算法
引用计数是指采用计数器说明引用对象个数,当计数器=0时则代表
是垃圾
引用计数无法解决循环引用!
- 根搜索算法(Root Searching)
也叫”可达性分析算法”,从GCRoot触发,有引用的对象都是”不可回收的”,其他可
标记后再回收,是JVM默认算法。
(2) 三种垃圾回收算法
- Mark-Sweep 标记清除算法
- Coping 复制(交换)算法
- Mark-Compact 标记压缩算法
Mark-Sweep 标记清除算法
Coping 复制(交换)算法
Mark-Compact 标记压缩算法
(3) GC垃圾收集器
可分为分代收集器和不分代收集器
分代收集器(根据年轻代和老年代进行分类)
Serial 收集器
Parallel Scavenge(PS)收集器
ParNew 收集器
SerialOld 收集器
Parallel Old收集器
Concurrent Mark Sweep(CMS)
设置GC收集器组合
GC收集器表如下:
查看当前GC收集器组合:
java -XX:+PrintCommandLineFlags -version
设置GC收集器组合(程序运行时app.jar就会使用的收集器为年轻代采用Serial,老年代采用SerialOld):
java -jar -XX:+UseSerialGC app.jar
不分代收集器
G1收集器
年轻代回收Minor GC
新生代+老年代回收 Mixed GC
初始标记( 标记GCRoot与第一层对象
)
并发标记( 并发完成GCRoot引用链会产生并发问题
)
最终标记( 确认所有垃圾
)
标记-压缩算法回收STW
G1开辟一块最多5%堆空间的内存用于标记压缩的数据交换,过程产生STW, STW 200ms内最多回收10%垃圾最多的区域,回收后检查老年代是否低于45%, 未达标继续再来一遍,最多8次,8次未达标Serial Old(Full GC)。
低延迟收集器(STW小于10ms的收集器)
低延迟收集器:
- ZGC
- Shenandoah
各种拦击收集器并发程度对比:
ZGC
Shenandoah
Epsilon收集器( 不收集任何内存,在不需要内存回收的场景中使用
)
(4) JVM优化
JVM调优建议
① 大多数情况JVM生产环境考虑调整下面三方面:
- 最大堆和最小堆大小
- GC收集器的选择
- 新生代(年轻代)大小
② 在没有全面监控、收集性能数据之前,调优就是扯淡
③ 99%的情况是你的代码出了问题,而不是JVM参数不对
JVM选项规则
① java -version 标准选项,任何版本JVM/任何平台都可以使用
② java -Xms10m 非标准选项,部分版本识别 java
③ -XX:+PrintGCDetails 不稳定参数,不同JVM有差异,随时可能会被移除,+代表开启/-代表关闭
JVM优化选项
① 1.8+优先使用G1收集器,摆脱各种选项烦恼
② -Xms与-Xmx设置相同,减少内存交换
③ 评估Xmx方法: 第一次起始设置大一点,跟踪监控日志,调整为堆峰值*2~3即可
④ 最多300毫秒STW时间,200~500区间,增大可减少GC次数,提高吞吐
⑤ -Xss128k/256k 虚拟机栈空间一般128K就够用了. 超过256k考虑优化,不建议超过 256k
⑥ G1一般不设置新生代的大小,G1新生代是动态调整的
参数设置推荐如下:
java -jar -XX:+UseG1GC -Xms2G -Xmx2G -Xss256k -XX:MaxGCPauseMillis=300 -Xloggc:/logs/gc.log -XX:+PrintGCTimeStamps -XX:+PrintGCDetails test.jar
(5) JVM监控命令
(6) 调优软件Arthas
Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。
Arthas用户文档地址: https://arthas.aliyun.com/doc/
文档学习目录情况: