> 文档中心 > Java基础JVM虚拟机

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公司)虚拟机各个版本信息如下:
Java基础JVM虚拟机

  • Sun Classic VM 1.0

在这里插入图片描述

  • Sun Exact VM 1.2

在这里插入图片描述

  • Sun Hotspot VM

在这里插入图片描述

其他厂商虚拟机信息如下:
Java基础JVM虚拟机

  • JRockit VM

在这里插入图片描述

  • IBM J9 VM

在这里插入图片描述

  • Taobao VM

在这里插入图片描述
Oracle/SunJDK与OpenJDK的异同:
在这里插入图片描述

二.JVM的组成

在这里插入图片描述

1.字节码

(1) 什么是字节码
  • java源码经过javac编译生成的二进制(文件),称为字节码(文件)
  • JVM通过字节码保证平台无关特性
  • Java并不是唯一生成class文件的语言
  • Class是结构紧凑的二进制流,其格式固定,要求严谨
(2) 字节码要素

字节码的组成结构:
在这里插入图片描述
魔数

魔数就是区分文件类型的依据

例子:
OxCAFEBABE是Java字节码文件的魔数,保存在前4个字节
Java基础JVM虚拟机

实操:使用winhex软件查看文件的魔术:
在这里插入图片描述

文件版本号

  • 5~6字节是次要版本号
  • 7~8字节是主要版本号,Java 8 = 52.0

在这里插入图片描述

实操查看JVM版本(从下图可知Java版本为1.5):
在这里插入图片描述
我们也可以通过 javap -v -l -c 命令进行查询(下图查询的字节码文件是通过Java8编写):

在这里插入图片描述

注意: 字节码版本高于JVM版本,产生Unsupported ClassVersionError
Java基础JVM虚拟机

为了更好的观察字节码文件可以使用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

在这里插入图片描述

类/父类索引与接口集合

Java基础JVM虚拟机

  • 类/父类索引

在这里插入图片描述
由于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赋予初始值
例子:
Java基础JVM虚拟机数据类型默认初始值:
在这里插入图片描述
解析阶段:
• 将字节码符号引用转换为直接引用(包括类解析,字段解析,方法解析,接口解析)
• 说人话:将字节码的静态字面关联转换JVM内存中的动态指针关联

在这里插入图片描述

(3) 初始化Initialization阶段

• 初始化阶段是执行类构造器方法 () 的过程
() 方法用于完成类的初始化操作
() 方法并不需要显式声明,由Java编译器自动生成

备注:加载/验证/准备/解析是由虚拟机主导与代码无关;初始化则是通过代码生成clint,完成类初始化过程。

初始化知识点:
• 初始化阶段对类(静态)变量赋值与执行 static 代码块
• 子类初始化过程会优先执行父类 ()
• 没有类变量及 static{} 代码块就不会产生 ()
-XX:+TraceClassLoading 查看类加载过程
() 方法默认会增加同步锁,确保 () 只执行一次

知识点1:初始化阶段对类(静态)变量赋值与执行 static 代码块
代码详情:
在这里插入图片描述
Java基础JVM虚拟机

看看加载阶段的指令,确实对静态变量进行赋值了:
在这里插入图片描述
知识点2:子类初始化过程会优先执行父类 ()
运行效果:
Java基础JVM虚拟机

知识点3:没有类变量及 static{} 代码块就不会产生 ()

在这里插入图片描述
知识点4: -XX:+TraceClassLoading 查看类加载过程
在这里插入图片描述
在这里插入图片描述
点击ok,然后运行ClassInitSample项目:
在这里插入图片描述
在这里插入图片描述

知识点5: () 方法默认会增加同步锁,确保 () 只执行一次

在这里插入图片描述

Java基础JVM虚拟机

(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 ):
Java基础JVM虚拟机

  • 扩展类加载器

在这里插入图片描述
代码实例:

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文件下:
在这里插入图片描述

运行效果:
Java基础JVM虚拟机

  • 应用程序(系统)类加载器

在这里插入图片描述
代码实例:

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    }}

运行效果:
Java基础JVM虚拟机

  • 自定义加载器

在这里插入图片描述

面试题:Class实例在JVM是唯一的吗?

自定义ClassLoader三要素:

  1. 继承自ClassLoader,重写findClass()
  2. 获取字节码二进制流
  3. 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开头核心包 ):
Java基础JVM虚拟机
Java基础JVM虚拟机
运行效果:
在这里插入图片描述

3.运行时数据区的组成

Java基础JVM虚拟机
线程私有

  • 程序计数器 - 存储线程执行位置
  • 虚拟机栈 - 存储Java方法调用与执行过程的数据
  • 本地方法栈 - 存储本地方法的执行数据

线程共有

  • 堆 - 主要存储对象
  • 方法区 - 存储类/方法/字段等定义(元)数据
  • 运行时常量区 - 保存常量static数据
(1) 程序计数器

Java基础JVM虚拟机
为什么会有程序计数器?

使用程序计数器可以记录指令执行位置,比如下图中单线程通过划分多个时间片实现多线程工作,上一个线程执行指令到哪了,就可以通过计数器知道,然后执行下一个地址的指令。

Java基础JVM虚拟机
关于执行native方法计数器为空?

使用native方法,我们知道native方法都是通过和操作系统进行绑定的,如windows下的库大多数调用C/C++库方法,C/C++方法都是运行在C语言的运行内存中,和Java运行内存并不是一回事,所以计数器为空。

Java基础JVM虚拟机

(2) 虚拟机栈

什么是栈?

栈是一种数据结构,是一种连续紧密存储结构,出入顺序先进后出,二种操作入栈和出栈。

Data1和Data2数据块入栈
Java基础JVM虚拟机
Data1和Data2入栈结果如下:
Java基础JVM虚拟机
Data2出栈Data3入栈:
Java基础JVM虚拟机
Data2出栈Data3入栈效果如下:
Java基础JVM虚拟机
什么是虚拟机栈?

① 虚拟机栈(栈)保存方法的调用过程
② 栈说明了程序运行中的瞬时状态
③ 栈是线程私有的,生命周期与线程相同
④ 每次方法的调用,都会产生对应栈帧

Java基础JVM虚拟机
实例练习:

程序运行调用main()方法,栈帧main入栈,处于栈底,执行main()方法的代码,调用method1(),栈帧method1入栈,执行method1()代码,调用method2(),栈帧method2入栈执行method2()代码,没有方法执行了开始出栈( 按照先进后出顺序出栈 ),栈帧method2出站,然后栈帧method1出栈,最后栈帧main出栈,执行完毕,线程销毁,整个过程就是栈的生命周期。

Java基础JVM虚拟机
虚拟机栈的特点

① 线程私有的
② 不会被垃圾回收
③ 栈的生命周期和线程相同
④ 栈深度有限制

设置虚拟机栈的空间

① 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();    }}

运行效果:
Java基础JVM虚拟机

设置栈的固定长度:
Java基础JVM虚拟机
Java基础JVM虚拟机
输入数值: -Xss10m( 格式:-Xss 数值[k|m|g] k:KB,m:MB,g:GB )
Java基础JVM虚拟机

再次运行(执行方法次数明显增加了):
Java基础JVM虚拟机
局部变量也会影响栈的容量(将上面代码中的局部变量注释掉,次数明显有增加):
Java基础JVM虚拟机
栈的容量要么采用默认1MB或者使用512KB
Java基础JVM虚拟机

备注:动态扩展会消耗内存容量,如果调用死循环,会使一些原本可以运行的程序造成崩溃。

虚拟机栈的栈帧

栈帧由 局部变量表 , 操作栈 , 动态链接 , 返回地址 四部分组成。
① 局部变量表:局部变量
② 操作数栈:保存中间计算的临时结果
③ 动态链接:将符号引用转为直接引用
④ 返回地址:存放调用方法的程序计数器值

栈帧的组成:

Java基础JVM虚拟机
局部变量表

局部变量表按定义顺序存储两块内容:存储方法参数和存储方法内的局部变量

局部变量表特点

① 线程私有,不允许跨线程访问,随方法调用创建,方法退出销毁
② 编译期间长度已确定,局部变量元数据存储在字节码中
③ 局部变量表是栈帧最主要的存储空间,决定了栈的深度

实例练习:
测试代码如下:

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栈帧(一个方法的调用对应一个栈帧):
Java基础JVM虚拟机
上图中的LineNumberTable叫做行号表,说明了字节码指令和源码行号的对应关系
Java基础JVM虚拟机
Java基础JVM虚拟机
上图中的LocalVariableTable在编译期间就保存在字节码中,长度固定,方法调用时随栈帧创建并载入,LocalVariableTable中单个变量的序号(index)叫做Slot( 存储局部变量的单位为Slot(变量槽) )
Java基础JVM虚拟机
上图中的局部变量对应源码中如下:
Java基础JVM虚拟机
局部变量的作用域由起始PC和长度决定,通过字节码指令查看
Java基础JVM虚拟机
比如name,offset,起始PC为0长度为27(0-26),对应字节码如下:
Java基础JVM虚拟机
又比如ret变量,起始PC为20长度为7(20-26),如下:

Java基础JVM虚拟机

LocalVariableTable在构造方法与实例方法中,0号槽位(Slot)默认为this,指向当前类实例
Java基础JVM虚拟机
Java基础JVM虚拟机
static方法没有this关键字,所以在static方法中不能够使用this关键字
Java基础JVM虚拟机
Java基础JVM虚拟机
槽(Slot)按类型区分,32位以内类型(int/float/char/…/引用类型)占用1个Slot,64位类型(long/double)占用2个Slot:
Java基础JVM虚拟机
上图对应下图:
Java基础JVM虚拟机
当槽位有空余时,后产生的局部变量会重用之前的槽位,此特性称为"槽复用"

实例:
字节码局部变量表
Java基础JVM虚拟机
代码如下:
Java基础JVM虚拟机
start PC=0从字节码局部变量表可知,该solotSample2是一个实例方法,0卡槽为this
Java基础JVM虚拟机

Start PC =2,从字节码局部变量表可知,卡槽1为a
Java基础JVM虚拟机

start PC=4,从字节码局部变量表可知,卡槽2为b
Java基础JVM虚拟机

由于length=4所以当start PC=7的时候b字段销毁
Java基础JVM虚拟机
继续执行start PC=11时,卡槽2(index=2)为c,(这里就是因为卡槽复用所以卡槽2=c),然后执行完毕
Java基础JVM虚拟机

操作数栈

字节码指令在执行过程中的中间计算过程存储在操作数栈。

通过字节码指令分析执行过程掌握操作数栈知识:
源代码:

public class OperandStackSample {    public int compute1(){ short i = 10; int j = 18; int k = i + j; return k;    }}

字节码指令表:
Java基础JVM虚拟机
compute1()方法是一个实例方法,所以局部变量表中索引0为this
Java基础JVM虚拟机
Java基础JVM虚拟机
执行指令 0 bipush 10 将整型进行操作数栈入栈
Java基础JVM虚拟机
2 istore_1 栈顶 出栈 保存至局部变量表1号槽
Java基础JVM虚拟机
3 bipush 18和5 istore_2指令和上面操作一致,不做解释:
Java基础JVM虚拟机
6 iload_11 号槽局部变量压入栈顶

Java基础JVM虚拟机
7 iload_2:
Java基础JVM虚拟机
8 iadd:iadd将栈顶与第二位做加运算存至栈顶
Java基础JVM虚拟机
9 istore_3和10 iload_3:
Java基础JVM虚拟机
Java基础JVM虚拟机

11 ireturn :ireturn将栈顶数据出栈进行返回
Java基础JVM虚拟机
控制台输出28
Java基础JVM虚拟机
Java基础JVM虚拟机
64位数据类型占两个栈深度( 在局部变量表中64位数据类型占二个Slot,在操作数栈中64位数据类型占二个栈深度 ):
Java基础JVM虚拟机
下图栈深度为4
Java基础JVM虚拟机
动态链接

将保存在字节码文件中 符号引用 转为运行时的内存 直接引用

Java基础JVM虚拟机
符号引用就是通过字面量进行描述该数据存放的位置,该数据(元数据,存放在运行时常量区中),而栈帧中的动态链接就是对应的内存指针,就是对元数据的直接引用。
Java基础JVM虚拟机

方法返回地址

保存该方法调用者的程序计数器值,用于方法正常执行后后续执行。

执行下图s1方法,当进入方法体时执行第一条打印语句,执行完之后遇见了this.s2();进入到s2()方法中,当s2()方法被执行完后,再次回到s1中执行剩下的代码,s2()方法执行完后是怎么知道s1中剩下代码从什么时候开始执行的呢?,其实就是通过方法返回地址确认后续执行。
Java基础JVM虚拟机

(3) 本地方法栈

native本地方法:

① 一个native方法就是一个Java调用非Java代码的接口
② 在定义一个native方法时,并不提供实现体
③ native的可以调用其他语言接口实现对操作系统更底层的操作

Java基础JVM虚拟机

本地方法栈图如下:
Java基础JVM虚拟机
本地方法栈特点:

① 在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定
② Sun HotSpot虚拟机把本地方法栈和虚拟机栈合二为一

(4) 堆Heap

Java基础JVM虚拟机
什么是堆Heap?

① 堆Heap是JVM最核心的内存区域,存放运行时实例化的对象实例
② 堆在JVM启动时就被创建,空间也被分配.是JVM内存的主要占用区域
③ 堆是线程共享的,堆内存在物理上可以是分散的,在逻辑上是连续的(堆中包含线程私有的缓冲区(TLAB)用于提高JVM的并发处理效率)

引用类型与堆的关系

① 引用类型本质是指针,指向存放在堆中的对象实例地址
② 堆是垃圾回收(GC)的重点区域,方法结束后堆对象不会被立即清除,在GC时才会被清理

执行如下代码实际上就是在堆上开辟了一个空间,存放B类对象,然后在变量表中b变量(栈帧)指向堆中的B类对象。

public void test(){B b = new B();}

Java基础JVM虚拟机

堆结构

新生代:是用来存放新生的对象,该区域对象会被频繁GC
老年代:新生代保存的稳定对象放入老年代,老年代不会频繁执行GC
元空间:内存的永久保存区域,主要存放Class元数据,几乎不会GC

堆( JDK1.8 )的结构如下图所示:
Java基础JVM虚拟机
堆空间
默认堆空间的大小

初始内存大小:物理电脑内存大小 / 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(); }*/    }}

运行效果:
Java基础JVM虚拟机

我的电脑内存可用为7.85GB
Java基础JVM虚拟机
通过上面的公式,算出预期的初始内存大小为125.6MB,最大内存大小为2009.6MB。

备注: 操作系统自身会占用一些空间,JVM分配内存的时候也会拿出来一些作为保留,所以实际分配的内存比我们预期的内存要少一些

年轻代与老年代的占用比例

① 年轻代固定占用1/3
② 老年代固定占用2/3

控制台打印GC详细信息:设置 -XX:+PrintGCDetails 参数
Java基础JVM虚拟机
运行效果:
Java基础JVM虚拟机

设置堆空间大小的参数

设置参数(m:MB,g:GB):
① -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
② -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
设置建议:
① 开发中建议将初始堆内存和最大的堆内存设置成相同的值。
② Java整个堆大小设置建议,Xmx 和 Xms设置为老年代FullGC后存活对象的3-4倍

设置初始内存大小上限和最大内存上限为1024MB
Java基础JVM虚拟机

运行效果:
Java基础JVM虚拟机
VisualVM安装使用
VisualVM安装使用教程:VisualVM安装,插件安装,各个面板信息讲解
Java基础JVM虚拟机
对象分配一般过程

① 绝大多数刚创建的对象会存入新生代的Eden伊甸园区
② Eden与S0/S1的内存分配比例是8:1:1

Java基础JVM虚拟机
新生代进入老年代过程

  • 一般情况

当Eden第1次满MinorGC对Eden/S0/S1进行GC:当实例化对象的时候(有new操作时),优先将对象放入Eden区,当Eden区的对象满时,先将还存在引用的对象通过复制交换算法进入到S0中设置age=1,最后对Eden和S1中未引用对象进行GC操作( Eden满时,针对年轻代进行的垃圾回收称为YGC/MinorGC )。
Java基础JVM虚拟机
GC之后:
Java基础JVM虚拟机

Eden第2次满,对Eden/S0/S1进行MinorGC:Eden第2次满时,Eden将有引用关系的对象转移至S1区,设置对象age=1,原S0(From区)对象转移至S1(To区),且age+1,对Eden和S0进行GC操作。
Java基础JVM虚拟机
GC之后:
Java基础JVM虚拟机

Eden第3次满,对Eden/S0/S1进行MinorGC:Eden第3次满时,Eden将有引用关系的对象转移至S0区,设置对象age=1,原S1(From区)对象转移至S0(To区),且age+1,对Eden和S1进行GC操作。
Java基础JVM虚拟机
GC之后:
Java基础JVM虚拟机

n次Minor GC后当对象age超过15(阈值)对象晋升(Promotion)至老年代:
Java基础JVM虚拟机

  • 特殊情况(GC整个流程图)

Java基础JVM虚拟机

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日志

Java基础JVM虚拟机
运行效果:
Java基础JVM虚拟机

分析执行情况:
GC (Allocation Failure):

  • GC (Allocation Failure) : Allocation Failure说明触发GC的原因是Eden空间不足
  • 8182K->1016K(9216K):GC前年轻代空间 -> GC后年轻代空间(年轻代最大占用空间)
  • 8182K->6838K(60416K):GC前堆占用空间->GC后堆占用(堆最大占用空间)
  • 0.1999019 secs: 本次GC使用时间

Java基础JVM虚拟机
为什么年轻代最大空间是9216K,不是10240(10mb)?
答:年轻代组成为 Eden(8mb)+ 2*(Survivor(1mb)) ,但两个S区只有一个有数据,所以在内存分配时只需分配1个即:8mb+1mb=9216k。

Full GC (Ergonomics) :

  • Full GC (Ergonomics): Ergonomics(GC收集器策略执行)

Java基础JVM虚拟机

  • Full GC (Allocation Failure):堆空间分配失败,会抛出OOM

Java基础JVM虚拟机
visualvmGC执行情况如下:
Java基础JVM虚拟机

(5) 方法区

① 方法区是线程共享区域,是物理分散存储而逻辑为整体的内存区域
② 方法区保存的内容:类加载器信息/类信息(包含字段/方法)/常量/即时编译器编译后的代码缓存/静态变量(1.6版本前,之后存放在堆中)
③ 方法区是个概念,JVM对方法区如何实现做更多要求.

Java基础JVM虚拟机

永久代与元空间的区别

规范说明方法区是堆的逻辑部分,在实现中与堆内存无关,称为”非堆Non-heap”。

Java基础JVM虚拟机
设置元空间
Java基础JVM虚拟机
运行时常量区
Java基础JVM虚拟机

方法区的历史变化
方法区是观念,HotSpot独有的永久代是方法区的实现.1.8被淘汰
Java基础JVM虚拟机

  • JDK6

Java基础JVM虚拟机

  • JDK7
    Java基础JVM虚拟机
  • JDK8

Java基础JVM虚拟机

new对象永远放在堆中
( new 对象的实例不管在JDK1.6之前还是1.7之后都是放在堆中 )
Java基础JVM虚拟机

三. Grabage Collection(GC垃圾回收机制)

(1) 对象的创建和销毁

创建对象的几种方式

  • 用new语句创建对象
  • 运用反射手段(调用Java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。)
  • 调用对象的clone()方法。
  • 运用反序列化手段(调用java.io.ObjectInputStream对象的readObject()方法。)

对象销毁
Java基础JVM虚拟机
什么条件下对象才是垃圾?
当对象没有任何引用的时候,就代表对象无用了.
Java基础JVM虚拟机
引用链路断开,后续对象也会成被标记为垃圾
Java基础JVM虚拟机
如何发现对象已经是垃圾?

  • 引用计数(Reference Count)算法

引用计数是指采用计数器说明引用对象个数,当计数器=0时则代表
是垃圾

Java基础JVM虚拟机
引用计数无法解决循环引用!
Java基础JVM虚拟机

  • 根搜索算法(Root Searching)

也叫”可达性分析算法”,从GCRoot触发,有引用的对象都是”不可回收的”,其他可
标记后再回收,是JVM默认算法。

Java基础JVM虚拟机

(2) 三种垃圾回收算法

  • Mark-Sweep 标记清除算法
  • Coping 复制(交换)算法
  • Mark-Compact 标记压缩算法

Mark-Sweep 标记清除算法

Java基础JVM虚拟机
Coping 复制(交换)算法
Java基础JVM虚拟机
Mark-Compact 标记压缩算法
Java基础JVM虚拟机

(3) GC垃圾收集器

可分为分代收集器和不分代收集器
Java基础JVM虚拟机

分代收集器(根据年轻代和老年代进行分类)
Serial 收集器
Java基础JVM虚拟机

Parallel Scavenge(PS)收集器
Java基础JVM虚拟机

ParNew 收集器
Java基础JVM虚拟机

SerialOld 收集器
Java基础JVM虚拟机

Parallel Old收集器
Java基础JVM虚拟机

Concurrent Mark Sweep(CMS)
Java基础JVM虚拟机
设置GC收集器组合
GC收集器表如下:
Java基础JVM虚拟机
查看当前GC收集器组合:
java -XX:+PrintCommandLineFlags -version
Java基础JVM虚拟机

设置GC收集器组合(程序运行时app.jar就会使用的收集器为年轻代采用Serial,老年代采用SerialOld):
java -jar -XX:+UseSerialGC app.jar

不分代收集器
G1收集器
Java基础JVM虚拟机
年轻代回收Minor GC
Java基础JVM虚拟机
新生代+老年代回收 Mixed GC
Java基础JVM虚拟机
初始标记( 标记GCRoot与第一层对象 )
Java基础JVM虚拟机

并发标记( 并发完成GCRoot引用链会产生并发问题 )
Java基础JVM虚拟机

最终标记( 确认所有垃圾 )
Java基础JVM虚拟机

标记-压缩算法回收STW

G1开辟一块最多5%堆空间的内存用于标记压缩的数据交换,过程产生STW, STW 200ms内最多回收10%垃圾最多的区域,回收后检查老年代是否低于45%, 未达标继续再来一遍,最多8次,8次未达标Serial Old(Full GC)。

Java基础JVM虚拟机

低延迟收集器(STW小于10ms的收集器)
低延迟收集器:

  • ZGC
  • Shenandoah

各种拦击收集器并发程度对比:
Java基础JVM虚拟机
ZGC
Java基础JVM虚拟机
Shenandoah
Java基础JVM虚拟机

Java基础JVM虚拟机
Epsilon收集器( 不收集任何内存,在不需要内存回收的场景中使用 )
Java基础JVM虚拟机

(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监控命令

Java基础JVM虚拟机

(6) 调优软件Arthas

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。

Arthas用户文档地址: https://arthas.aliyun.com/doc/
Java基础JVM虚拟机
文档学习目录情况:
Java基础JVM虚拟机