1. JVM概述 1.1. Java程序结构 1.2. JVM作用 JVM全称是Java Virtual Machine-Java虚拟机
JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行 1.3. JVM规范和实现 JVM规范: Oracle官网:JDK 21 Documentation - Home <Java虚拟机规范>由Oracle制定,内容主要包含了Java虚拟机在设计和实现时需要遵守的规范,主要包含class字节码文件的定义、类和接口的加载和初始化、指令集等内容。 <Java虚拟机规范>是对虚拟机设计的要求,而不是对Java设计的要求,也就是说虚拟机可以运行在其他的语言比如Groovy、Scala生成的class字节码文件之上。 JVM常见实现: 2. JVM功能 2.1. 功能-编译和运行 编译字节码文件:实时编译字节码中文件中的指令,成为机器码之后交由计算机运行 2.2. 功能-内存管理 内存分配:JVM会自动为对象,方法等分配内存 垃圾回收:JVM中的垃圾回收机制会自动的回收不被引用的对象 2.3. 功能-即时编译 即时编译的作用:字节码指令可以实时解释为不同操作系统的机器码,这样就支持了跨平台运行 即时编译优化:对于热点代码的机器码会保存在系统内存,再次执行可直接调用,提升执行效率 优化的原因:Java不做优化的性能是不如C,C++的,因为Java需要实时编译字节码指令再交给机器运行,而C,C++可以直接将源文件编译为可执行文件 即 3. JVM组成 3.1. 组成-运行时数据区 运行时数据区域的作用:负责管理JVM运行时使用到的内存,比如创建对象和销毁对象 运行时数据区结构 3.2. 组成-类加载器 类加载器的作用:将.class字节码文件加载到运行时数据区,JVM分配在堆中分配一块空间存放字节码文件中的类和接口 3.3. 组成-执行引擎 执行引擎的作用:将字节码文件中的指令翻译成机器码,同时使用及时编译器 优化性能 执行引擎的组成及作用: 即时编译器:将热点代码的机器码存入操作系统内存 解释器 :实时翻译字节码中的指令为机器码垃圾回收器:回收未被引用的对象 3.4. 组成-本地接口 本地接口:调用本地已实现的接口,如JVM中由c,c++提供的方法 4. JVM字节码文件 4.1. 字节码文件-组成 4.1.1. 组成-基础信息 基础信息作用:包含了魔数,字节码对应Java版本号,访问标识,类/父类/接口的索引 4.1.1.1. 基础信息-魔数 如何确定文件类型? 文件是通过文件的前几个字节(称为文件头)来确定文件的类型,而不是通过文件扩展名来确定 软件要打开文件首先要校验文件头,如果不匹配则报错 常见文件的文件头: 魔数的作用:作为字节码文件的头文件标识,固定为0xCAFEBABE 4.1.1.2. 基础信息-主副版本号 主版本号的作用:用来标识大版本号,判断当前字节码文件对应Java版本号是否与当前版本号兼容,一般是大版本号兼容小版本号 副版本号的作用:主版本号相同时区分不同小版本的标识 JDK1对应45,JDK2对应46,JDK6对应50,JDK8对应52 4.1.2. 组成-常量池 常量池的作用:保存字符串常量,类名,接口名,字段名,用于字节码指令的符号引用,可以避免相同内容的重复定义 常量池工作流程:常量池中的常量都有一个从1开始的编号,在字节码指令中通过编号可以快速找到对应的数据,这个过程叫做符号引用 4.1.3. 组成-方法 方法的作用:存放字节码指令,其Code属性存放字节码指令具体内容 4.1.3.1. 方法-工作流程 方法的工作流程:按照字节码指令从上至下依次执行,需要用到虚拟机栈中的两个区域:操作数栈和局部变量表 iconst_0:将常量0压入栈中. istore_1:将栈中的数据弹出存入局部变量表1号位置. iload_1:将局部变量表1号数据拷贝压入栈中. iconst_1:将常量1压入栈中. iadd:将栈中数据弹出,执行完累加操作后,把结果压入栈中. istore_2:将栈中的数据弹出存入局部变量表2号位置. iconst_0:将常量0压入栈中. istore_1:将栈中的数据弹出存入局部变量表1号位置. iload_1:将局部变量表1号数据(0)拷贝压入栈中. iinc 1 by 1:将局部变量表1号数据加1 istore_1:将栈中的数据(0)弹出覆盖局部变量表1号位置. return:返回结果0, ++i:先执行iinc后执行iload,先加后入再弹,最终数据+1 i++:先执行iload后执行iinc,先如后加再弹,最终数据不变 i++; i+=1;:仅有一行iinc运算,效率高 i=i+1;:有ioad,iconst,iadd,istore四行运算,效率低 4.1.4. 组成-字段 字段的作用:保存类中定义的属性信息,包括属性的访问修饰符、属性名称、属性类型等等 4.1.5. 组成-属性 属性的作用:保存类的相关属性,比如源代码文件名,内部类的列表等等 4.2. 字节码文件-查看工具 字节码文件中保存的是Java代码编译后的内容,并以二进制方式存储,无法直接用记事本打开 4.2.1. javap javap的作用:javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容. javap的适用场景:javap使用方便,在jdk环境中即可使用,适合在服务器上查看字节码文件 javap的用法: 查看指定的字节码文件:javap -v xx.class > xx.txt,字节码文件和输出位置需要指定全路径名 javap所有用法: 4.2.2. jclasslib jclasslib-idea插件版使用 idea下载插件jclasslib 点击Java源代码文件后点击视图展示字节码文件 修改Java源码后需要进行一次编译(ctrl+shift+f9)才能看到修改后的字节码文件 4.2.3. 阿里Arthas Arthas介绍:Arthas是一款线上监控诊断产品,通过全局视角实时查看应用load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。 Arthas官网:简介 | arthas Arthas使用: 启动Arthas:java -jar arthas-boot.jar --telnet-port 9998 --http-port -1 加载运行时字节码到指定目录:dump -d D:/arthas/data/ org.pickstar.arthas.Demo 反编译运行时字节码为Java源码:jad org.pickstar.arthas.Demo 5. JVM类的生命周期 5.1. 类的生命周期-概述 类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过 类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析 5.2. 类的生命周期-加载阶段 加载阶段的作用:加载字节码文件,给类分配内存空间 加载阶段的步骤: 类加载器根据类的全限定名以二进制流方式将字节码文件加载到运行时数据区(字节码文件来源不同:本地文件,动态代理生成,网络传输) JVM将加载完毕的字节码信息保存到方法区中,JDK8保存在元空间中 方法区生成一个instanceKlass对象,其中保存了类所有的信息,还包含实现多态的信息等 同时堆中生成一个类似instanceKlass对象的java.lang.Class对象,通过Class对象反射获取类的信息以及存储静态字段的数据,这样就限制了开发者的访问范围 5.3. 类的生命周期-连接阶段 5.3.1. 连接阶段-验证阶段 验证阶段的作用:JVM验证字节码文件的正确性,包括语法校验、语义校验、字节码验证等步骤。验证过程保证被加载的类是合法、符合规范的,没有安全方面的问题 验证阶段验证部分: 文件格式验证:比如文件是否以OxCAFEBABE开头,主次版本号是否满足当前JaVa虚拟机版本要求。 元信息验证:例如类必须有父类 验证程序执行指令的语义:比如方法内的指令执行到一半强行跳转到其他方法中去 符号引用验证:例如是否访问了其他类中private的方法等 5.3.2. 连接阶段-准备阶段 准备阶段的作用:JVM为被加载的类的静态变量分配内存,并设置默认初始值(用户指定的值赋值发生在初始化阶段),被final修饰的静态变量会在准备阶段直接赋值 常见Java类型的初始值 5.3.3. 连接阶段-解析阶段 解析阶段的作用:JVM将常量池内的符号引用转换为直接引用,以便于之后的访问和调用 5.4. 类的生命周期-初始化阶段 初始化阶段的作用:JVM对类初始化,包括静态变量赋值,静态代码块执行等,同时保证初始化过程是线程安全的 初始化阶段执行的字节码方法部分:初始化阶段会执行字节码文件中clinit部分的字节码指令 5.4.1. 初始化阶段-工作流程 初始化阶段工作流程:字节码指令执行顺序跟Java代码编写顺序一致 iconst_1:将常量1压入栈中. putstatic#2:将栈中的数据弹出给常量池中的静态变量赋值 iconst_2:将常量2压入栈中. putstatic#2:将栈中的数据弹出给常量池中的静态变量赋值 5.4.2. 初始化阶段-触发情况 类初始化阶段被触发的情况: 访问一个类不被final修饰的静态变量或静态方法 调用Class.forName(String className)指定类 使用new创建一个类的实例 执行main方法的当前类 访问一个类被final修饰但是需要执行指令才能得出结果的静态变量 --XX:+TraceClassLoading,打印已经初始化的类 5.4.3. 初始化阶段-无clinit方法情况 类初始化阶段触发但没有clinit方法的情况: 没有静态代码块和静态变量赋值语句 有静态变量的声明,但没有复制语句 有静态变量的声明,但用final关键字修饰(会在准备阶段赋值) 创建该类的对象数组不会触发该类的初始化 5.4.4. 初始化阶段-父子类初始化情况 子类不触发情况:直接访问父类的静态变量是不会触发子类初始化的 父子都触发情况:子类的初始化clinit方法调用之前会先调用父类的clinit方法 5.5. 类的生命周期-面试题 5.5.1. 1 判断触发类的初始化情况:该类中执行main方法,所以该类会进行舒适化阶段 查看该类的字节码:字节码命令会告诉你代码块和构造方法的执行顺序 clinit字节码(初始化方法):在类的初始化时执行,初始化阶段执行静态代码块和给静态变量赋值 init字节码(构造方法):在类每次被创建实例时执行,执行代码块和构造方法,从字节码指令中可以看出,代码块打印C是先于构造方法打印B的 main字节码:由于该类调用main方法先进行初始化,在执行打印A,然后创建两次Demo实例,也就意味着调用两次代码块和构造方法 最终的结果就是DACBCB 5.5.2. 2 调用B的clinit方法之前会先父类A的clinit方法 a的值从0,到1,最后到2,最终打印结果也是2 直接访问B类的静态变量时不会进行子类的初始化 打印结果为1 6. JVM类加载器 6.1. 类加载器-概述 类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据 类加载器的作用:类加载器接受字节码文件 类加载器的编写语言:大部分类加载器是Java编写的,小部分是C++ 类加载器的应用场景: 6.2. 类加载器-执行流程 类加载器执行流程:类加载器获取字节码文件后执行JNI(Java Native Interface)本地接口调用JVM中C++编写的方法,由这些方法生成方法区对象和生成堆上的Class对象 6.3. 类加载器-分类(JDK8) 类加载器分类:类加载器一般分为两类,一是Java代码实现的,二是JVM底层源码实现的 类加载器不同版本:类加载器的设计在JDK8版本前后的差距大,本篇研究JDK8的默认类加载器 6.3.1. JVM底层实现的类加载器 JVM底层实现的类加载器作用:加载Java程序运行时的基础类,比如java.lang.String,确保其可靠性 JVM底层实现的类加载器实现语言:类加载器的源代码位于JVM源码中,与虚拟机的底层语言是一致的,比如Hotspot使用C++,那此虚拟机的实现语言也是C++ JVM底层实现的类加载器: 启动类加载器BootStrap:加载Java中最核心的类 6.3.1.1. 启动类加载器 启动类加载器(BootStrap ClassLoader)概述:是由Hotspot虚拟机提供的,使用C++编写的类加载器 启动类加载器的作用:加载JDK默认安装目录/jre/lib下的jar包,为Java程序提供基础环境 启动类加载器加载用户jar包的两种方式: 放入/jre/lib下进行扩展(不推荐):尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载 使用参数扩展:使用-Xbootclasspath/a:jar包目录/jar包名进行扩展 6.3.2. Java代码实现类的加载器 Java代码实现类加载器: 扩展类加载器Extension:允许扩展Java中的通用类 应用程序类加载器Application:加载开发者编写的应用类 Java代码实现类加载器的作用:加载Java运行时的通用列和开发者编写的自定义的类 Java代码实现类加载器的扩展性:JDK中默认提供了多种处理不同渠道的类加载器,开发者也可以创建自定义类加载器完成扩展,Java中所有的类加载器都需要集成ClassLoader抽象类 6.3.2.1. 扩展类加载器 扩展类加载器(Extension Class Loader)概述:JDK中提供的,使用Java编写的类加载器,源码都位于sun.misc.Launcher中,是一个静态内部类 扩展类加载器的作用:默认加载Java安装目录/jre/lib/ext下的jar包,继承自URLClassLoader,可以通过URL获取目录下或者指定的jar包进行加载 扩展类加载器加载用户jar包的两种方式: 放入/jre/lib/ext下进行扩展(不推荐):尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载 使用参数扩展:用-Djava.ext.dirs=jar包目录,进行扩展,这种方式会覆盖掉原始目录,需要先把原始目录(C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext)添加再用;追加目录 windows系统用;追加 macos/linux系统用:追加 6.3.2.2. 应用程序类加载器 应用程序加载器(Application Class Loader)概述:JDK中提供的,使用Java编写的类加载器,源码位于sun.misc.Launcher中,是一个静态内部类 应用程序加载器的作用:默认加载classpath下的类 6.4. 类加载器-Arthas查看类加载器 使用Arthas监控查看类加载器 启动Arthas:java -jar arthas-boot.jar 选择正在运行中的java程序 查看类加载器信息:classloader DelegatingClassLoader:用于提底层反射效率的类加载器 使用Arthas监控当前运行中的类的类加载器 启动Arthas:java -jar arthas-boot.jar 选择正在运行中的java程序 查看类加载器信息,按类加载实例进行统计:classloader -l 查看实际加载的jar包:classloader -c hash值 6.5. 类加载器-双亲委派机制 6.5.1. 双亲委派机制-作用 保证类加载的安全性:避免恶意代码替换JDK中的核心类库(比如:java.lang.String),确保核心类库的完整性和安全性 避免类的重复加载:避免一个类被反复加载 6.5.2. 双亲委派机制-工作流程 双亲委派机制的工作流程:当类加载器接收到加载类的任务时,会自底向上查找父类加载器是否加载过该类,是则结束这个过程,否则自顶向下进行加载该类 自底向上工作流程:每个类加载器都有父类加载器,在类加载过程中,每个类加载器会检查自身是否加载了该类,是则直接返回该类,否则将加载请求委派给父类加载器 自底向上工作优点:只要一个类加载器加载过该类就会直接返回,避免了重复加载 自顶向下工作流程:父类加载器会检查当前类是否在自己的加载目录中,是则加载后返回类对象,否则交给子类去加载 自顶向下的优点:实现从上到下的加载优先级 6.5.3. 双亲委派机制-父加载器 父加载器辨析:每个Java实现的类加载器中保存了一个成员变量叫"父"(parent)类加载器,可以理解为它的上级,但不是继承关系 应用程序类加载器的父类加载器:扩展类加载器 扩展类加载器的父类加载器:null,启动类加载器是JVM源码中的,Java代码是获取不到的,但是代码逻辑上启动类加载器的父类加载器还是启动类加载器 启动类加载器的父类加载器:启动类加载器是用C++编写的没有父类加载器 6.5.4. 双亲委派机制-面试题 启动类加载器加载,根据双亲委派机制,它的优先级是最高的 不能,因为启动类加载器在程序启动时已经加载了JDK提供的String类,接收到委派请求时会直接返回加载好的String类 6.5.5. 双亲委派机制-代码主动加载一个类 使用Java代码可以主动加载一个类,有两种实现方式: 调用Class.forName方法使当前类的类加载器去加载该类 调用getClassLoader方法获取当前类的类加载器,再用类加载器的loadClass方法让该类加载器加载指定的方法 6.6. 类加载器-打破双亲委派机制 6.6.1. 打破委派-ClassLoader原理 ClassLoader的原理就存在于四个核心方法中 loadClass: 作用:类加载的入口,内部实现双亲委派机制,内部会调用findClass 代码逻辑: 使用synchronized加锁,防止多线程情况下出现类被多次加载 使用findLoadClass方法判断这个类是否被当前类加载器加载过 未被加载过,则判断父类加载器是否为空 不为空,则由父类加载器再判断是否加载过这个类,依次类推 为空,则由启动类加载器去加载 已被加载过,则直接返回这个类 执行完向上委派动作,但类任未被加载,则由当前类加载器加载 使用URLClassLoader的findClass方法获取特定目录下的Class字节码文件,获取文件对象 调用重载方法传入resolve=false,不执行连接的过程 findClass:由类加载器子类实现,获取二进制数据时调用defineClass,比如URLClassLoader会根据文件路径去获取类文件的二进制数据 defineClass:对类名进行校验,调用JVM方法将字节码信息加载到JVM运行时数据区 resolveClass:执行类生命周期中的连接阶段 6.6.2. 打破委派-打破的场景 Tomcat打破双亲委派机制的原因:Tomcat程序中运行多个Web应用时,如果Web应用中出现相同限定名的类,Tomcat要保证这些类能够正常加载并运行且保证他们是不同的类,Tomcat就需要打破双亲委派机制 Tomcat打破双亲委派机制的方式:Tomcat为每一个Web应用提供了自定义类加载器来加载对应的类,这样就实现了应用之间的类的隔离 6.6.3. 打破委派-自定义类加载器 6.6.4. 打破委派-线程上下文类加载器 线程上下文类加载器应用场景:JDBC(为例),JNDI JDBC概述:JDBC提供DriverManager来管理jar包中引入的数据库驱动,这样就能在Java中操作不同的数据库 JDBC打破委派困境:DriverManager位于rt.jar包中,由启动类加载器加载,但依赖中的MySQL驱动实现类需要应用类加载器去加载 JDBC-SPI机制:SPI(Service Provider Interface)JDK内置服务发现机制,通过SPI快速找到Driver接口的实现类,类似Spring中的依赖注入 JDBC工作原理:MySQL为例 启动类加载器加载位于rt.jar包的DriverManager 初始化DriverManager时,通过SPI机制加载jar包中的数据库驱动实现类,将此实现类注册到DriverManager中交由他管理 DriverManager利用SPI机制会去加载META-INF/services路径下的java.sql.Driver文件,将MySQL实现了Driver接口的驱动实现类的全限定名写入文件就可以被加载并管理 SPI中利用了线程上下文类加载器(一般是应用类加载器)去加载获取的Driver驱动类并创建对象 DriverManager使用ServiceLoader去加载Driver实现类,ServiceLoader中获取线程上下文类加载器去加载实现类 这种由启动类加载器加载的类去委派应用程序类加载器去加载类的方式打破了双亲委派机制 但是JDBC案例中都使用的是JDK提供的类加载器还是会走双亲委派流程,并没有重写loadClass,也可以说没有打破双亲委派机制 6.6.5. 打破委派-OSGi模块化 OSGi作用:OSGi是模块化框架,解决早起JDK所有核心类都放在rt.包下难以管理的问题,它实现同级之间的类加载器委托加载.还使用类加载器实现了热部署功能 6.7. 类加载器-JDK8前后的类加载器 JDK8之前版本:扩展类加载器和应用程序类加载器的源码都位于rt.jar包下的sun.misc.Launcher.java中,这两个加载器都实现了URLClassLoader,也可以说JDK8之前类加载器是按照类的位置去加载的 JDK8之后版本:JDK9引入了module概念,类不在放在jar包中加载,而是放在一个个jmod文件中,从jmod文件中加载文件,类加载器发生很多变化: 启动类加载器由Java编写,位于jdk.internal.loader.ClassLoader类中 Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码文件 启动类加载器依然无法通过Java代码找到保持了统一 扩展类加载器被替换成了平台类加载器(Platform Class Loader) 平台类加载器遵循模块化方式加载字节码文件,继承关系从URLClassLoader变为BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件,平台类加载器的存在更多是为老板的设计方案兼容,自己没有特殊逻辑 7. JVM运行时数据区 7.1. 运行时数据区-总览 运行时数据区的概念:是JVM运行Java程序过程中管理内存的区域 运行时数据区的组成总览 线程不共享的含义:每个线程创建后,内部都会创建自己的程序计数器,Java虚拟机栈,本地方法栈对应的数据,自己维护数据,不共享,安全性高,当线程回收时这些区域的内存也随着回收 线程共享的含义:每个线程都可以访问方法区和堆中的数据,是共享的,是线程不安全的 7.2. 运行时数据区-查看内存对象 JDK自带的hsdb工具查看JVM内存信息,工具位于JDK安装目录下lib文件夹中的sa-jdi.jar文件中 启动命令:java -cp sa-jdi.jar sun.jvm.hotspot.HSDB 7.3. 运行时数据区-程序计数器 7.3.1. 程序计数器-作用 程序计数器的作用:程序计数器(Program Counter Register)也叫PC寄存器,内部存放了接下来要执行的字节码指令的地址,交给解释器执行,可以实现分支,跳转,异常等逻辑,在多线程情况下,JVM需要程序计数器记录CPU切换前的地址并继续执行,程序计数器只存储一个固定长度的内存地址,不会内存溢出 7.3.2. 字节码指令执行流程 字节码指令执行的流程:字节码指令和偏移量最初保存在字节码文件中,类加载器将字节码文件读取到内存之后,字节码指令也就保存在内存中原文件中的偏移量也被替换成内存地址,每一行字节码指令都有自己的地址,字节码指令最终要交给解释器去执行,所以解释器就必须知道需要解释的字节码指令在哪,这个地址就由程序计数器交给解释器 程序计数器在物理上的实现是寄存器,是整个CPU里读取速度最快的单元
程序计数器也就是Java对物理硬件的屏蔽和抽象
7.4. 运行时数据区-Java虚拟机栈 7.4.1. 栈-概述 栈的介绍:Java虚拟机栈采用栈的数据结构 来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存 栈的组成: 栈:一个线程运行所需要的内存空间,一个栈由多个栈帧组成 栈帧:一个方法运行所需要的内存空间 栈的生命周期:栈随着线程的创建而创建,而回收会在线程销毁时进行 栈的执行流程: 栈帧压入栈内执行方法 执行完毕释放内存 若方法间存在调用,那么会压入被调用方法入栈,执行完后释放内存,再执行当前方法,直到执行完毕,释放所有内存 7.4.2. 栈帧-组成 7.4.2.1. 栈帧-帧数据 帧数据的作用:包含动态链接,方法出口,异常表的引用 动态链接:保存的是符号引用到内存地址的映射关系,主要保存的是其他类的属性或方法,字节码指令运行时可以根据动态链接快速获取到运行时常量池的数据 方法出口:方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址 异常表:异常表存放的是代码中异常的处理信息,包含了try代码块和catch代码块执行后跳转到的字节码指令位置。 7.4.2.2. 栈帧-操作数栈 操作数栈作用:存储方法执行过程中需要计算的操作数以及操作结果. 计算过程:从操作数栈中弹出操作数、执行运算操作,并将结果重新压入操作数栈中 操作数栈的生命周期:当方法执行时,创建一个对应的栈帧,栈帧中包括了该方法操作数栈,当方法执行结束时,对应的栈帧也会被销毁,在编译期就可以确定操作数栈的最大深度 7.4.2.3. 栈帧-局部变量表 局部变量表的作用:存储方法执行过程中所有的局部变量,字节码文件编译时就确定了局部变量表的内容 字节码文件中的局部变量表:每个局部变量表有编号和生效范围 栈帧中的局部变量表:栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot),long和double类型占用两个槽,其他类型占用一个槽。 局部变量表保存的内容:实例方法的this对象(0号位置),方法的参数,方法体中声明的局部变量。 局部变量表的运行优化机制:局部变量表中的槽是可以复用的,一旦某个变量失效了(代码块中的变量执行完毕),当前槽就可以再次使用 局部变量表的生命周期:当方法执行时,创建一个对应的栈帧,栈帧中包括了该局部变量表,当方法执行结束时,对应的栈帧也会被销毁 7.4.3. 栈-执行流程 执行流程演示:
栈内随着方法调用,栈帧一个个被压入栈内,随着方法执行完毕,方法一个个被弹栈而出,定义的变量的内存空间也随着方法执行完毕释放 7.4.4. 栈-内存溢出 栈内存溢出的情况:Java虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出,此时会出现StackOverflowError的异常 栈帧过多:递归调用,死锁,死循环 栈帧过大 7.4.5. 栈-设置栈内存大小 设置栈大小参数:-Xss或-XX:ThreadStackSize=1024 语法:-Xss栈大小或-XX:ThreadStackSize=栈大小 单位:字节(默认,必须是1024的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB) 栈最大最小值:jdk8中最大180k,最大1024mb 7.4.6. 栈相关问题 7.4.6.1. 垃圾回收是否涉及栈内存? 不涉及,每次方法执行完毕之后,栈内存就会直接释放,并不会堆积内存,垃圾回收的是堆内存中不使用的内存
7.4.6.2. 栈内存分配越大越好吗? 在程序执行时,可以通过-Xss size指定栈内存的大小,默认大小为1024KB也就是1M,一个线程使用一个栈内存,如果栈内存设置过大,那么单个线程占用的内存就会过大,在总内存空间一定的情况下,这样反而减少了线程数量,降低了性能
7.4.6.3. 方法内的局部变量是否线程安全? 要判定一个变量是否为线程安全,就要查看这个变量是为多个线程共有的还是一个线程私有的.
要判断方法中的成员变量是否为线程安全,要查看这个成员变量是否脱离了方法的作用范围,有两种情况是线程不安全的:
变量作为参数传入方法中,这样其他线程就可能访问到该变量,所以不是线程安全的 变量作为返回值返回,这样其他线程可能拿到该变量并操作,所以不是线程安全的 7.5. 运行时数据区-本地方法栈 本地方法栈的作用:Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧。 本地方法栈的栈空间:在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来 7.6. 运行时数据区-Java堆 7.6.1. 堆-概述 堆的作用:存放对象的内存空间,它是空间最大的一块内存区域.栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。 堆的特点: 线程共享:堆中的对象都需要考虑线程安全的问题 垃圾回收:堆有垃圾回收机制,不再引用的对象就会被回收 7.6.2. 堆-空间 堆空间的值:used,total,max,如果不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64.在实际应用中一般都需要设置total和max的值。 used:当前已使用的堆内存 total:java虚拟机已经分配的可用堆内存 max:java虚拟机可以分配的最大堆内存 随着堆中的对象增多,当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆。如果堆内存不足,java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等。 7.6.3. 堆-设置大小 要修改堆的大小,可以使用虚拟机参数-Xmx(max最大值)和-Xms(初始的total)。 语法:-Xmx值-Xms值 单位:字节(默认,必须是1024的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB) 限制:Xmx必须大于2MB,Xms必须大于1MB 7.6.4. 堆内存溢出 程序运行时不断地创建新的对象就会出现堆内存溢出OOM
java.lang.OutOfMemoryError: Java heap space
7.6.5. 堆内存诊断 jsp工具:查看当前系统有哪些Java进程 jmap工具:查看堆内存占用情况,使用时加上参数-heap javaid,查看指定进程的堆内存占用情况 jconsole图形化界面查看内存占用情况 jvisualvm图形化界面查看内存占用情况 7.7. 运行时数据区-方法区 7.7.1. 方法区-概述 方法区的概述:方法区是存放基础信息的位置,线程共享,主要包括: 类的元信息:保存了所有类的基本信息 运行时常量池:保存了字节码文件中的常量池内容 静态常量池:字节码文件通过编号查表的方式找到常量 运行时常量池:当静态常量池加载到内存中后,可以通过内存地址快速查找常量池中的内容 7.7.2. 方法区-实现 方法区概念:方法区是Java虚拟机规范上的虚拟设计理念,每款JVM实现各不相同 不同版本的实现: JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。 JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。 7.7.3. 方法区-内存溢出 JDK7的方法区内存溢出:永久代内存溢出,Java.lang.OutofMemoryError:PermGen space,设置永久代大小参数-XX:MaxPermSize=8m JDK8的方法区内存溢出:超过操作系统内存上限后,才会出现元空间内存溢出,有可能影响其他程序的内存,java.lang.OutofMemoryError:Metaspace,设置元空间大小参数XX:MaxMetaspaceSize=8m 7.7.4. 方法区-字符串常量池 7.7.4.1. 字符串常量池-概述 字符串常量池的作用:用于存储在代码中定义的常量字符串 字符串常量池和运行时常量池的关系:早期字符串常量池是运行时常量池的一部分存储位置也是一致的,后期将字符串常量池和运行时常量池做了区分 7.7.4.2. 字符串常量池-工作流程 7.7.4.2.1. 案例1 ------源代码------
------执行结果------
s3 == s4:s3指向的是字符串常量池中的ab字符串,s4指向的是堆中的ab字符串对象,所以两者地址不相等为false s3 == s5:s3指向的是字符串常量池中的ab字符串,s5的字符串a和字符串b的拼接在编译期直接完成,指向的是字符串常量池中的ab字符串,所以两者地址相等为true s3 == s6:s3指向的是字符串常量池中的ab字符串,s6的值是s4未放入字符串常量池而返回的字符串常量池中ab字符串,所以两者地址相等为true ------字节码------
------代码解析------
ldc #2 <a>:从常量池中获取字符串a的地址压入操作数栈 astore_1:将操作数栈中的值(a)弹出放入局部变量表1号位置(变量s1的位置) ldc #3 <b>:从常量池中获取字符串b的地址压入操作数栈 astore_2:将操作数栈中的值(b)弹出放入局部变量表1号位置(变量s2的位置) ldc #4 <ab>:从常量池中获取字符串ab的地址压入操作数栈 astore_3:将操作数栈中的值(ab)弹出放入局部变量表1号位置(变量s3的位置) new #5 <java/lang/StringBuilder>:创建StringBuilder对象 dup invokespecial #6 <java/lang/StringBuilder.<init>>:初始化StringBuilder为空字符串"" aload_1:加载局部变量表中的1号位置的值(a)到操作数栈 invokevirtual #7 <java/lang/StringBuilder.append:调用StringBuilder的append拼接刚刚操作数栈加载的字符串a aload_2:加载局部变量表中的1号位置的值(b)到操作数栈 invokevirtual #7 <java/lang/StringBuilder.append:调用StringBuilder的append拼接刚刚操作数栈加载的字符串b invokevirtual #8 <java/lang/StringBuilder.toString:调用StringBuilder的toString方法将StringBuilder对象转换成String对象存入堆内存中 astore 4:弹出操作数栈中的数据(ab字符串对象)放入局部变量表中的4号位置(变量s4的位置) ldc #4 <ab>:从常量池中获取字符串(ab)的地址压入操作数栈 astore 5:弹出操作数栈中的数据(ab)放入局部变量表中的4号位置(变量s5的位置) aload 4:加载局部变量表中的4号位置的值(ab)到操作数栈 invokevirtual #9 <java/lang/String.intern>:调用String的intern方法尝试将ab字符串加入到字符串常量池中,判断字符串常量池中是否有ab字符串,有则直接返回的字符串常量池中的ab字符串到操作数栈中,无则将4号位置的引用(ab字符串对象)转换到字符串常量池中(引用变为字符串常量池中)并返回字符串常量池中的引用到操作数栈中 intern方法会主动将调用方法的字符串对象放入字符串常量池中,判断字符串常量池中是否有这个字符串常量池. JDK8中有则直接返回字符串常量池中的字符串常量,无则将当前堆中的字符串对象引用转到字符串常量池中(堆中的字符串对象之后会被垃圾回收,因为无人引用了),然后返回字符串常量池中的引用 JDK6中有则直接返回字符串常量池中的字符串常量,无则将当前堆中的字符串对象复制一份到字符串常量池中(相当于两份引用,原来的并不会转变到字符串常量池中),然后返回字符串常量池中的引用 JDK1.6中StringTable没有这个对象时会复制对象再放入,本质上是两个对象 astore 6:弹出操作数栈中的数据(ab)放入局部变量表中的6号位置(变量s6的位置) String s1 = new String("abc"):new关键字创建一个abc字符串对象,当代码编译成字节码后,abc字符串会存入静态常量池中,当程序运行字节码读取到内存后,abc字符串就会存入字符串常量池中,由于字符串对象是new关键字创建出来的,所以这个对象会存储在堆内存中,在栈内存中使用s1局部变量去保存堆内存中abc的地址 String s2 = "abc":并没有创建字符串对象,而是直接去字符串常量池中找是否有abc字符串,有则直接返回,无则在字符串常量池中创建一个abc字符串再返回 s1 == s2:s1指向的是堆中的abc字符串对象,s2指向的是字符串常量池中的abc字符串 7.8. 直接内存 7.8.1. 直接内存-概述 直接内存的作用:直接内存(Direct Memory)并不在Java虚拟机规范中存在,并不属于Java运行时的内存区域,在JDK4中引入NIO机制使用到了直接内存,主要解决两个问题: 解决Java堆中的对象垃圾回收时会影响对象的创建和使用的问题 解决IO操作浪费资源的情况,NIO在堆中可以直接访问直接内存,而不是IO的复制一份数据到Java堆中缓存 分配回收成本高,同时读写性能也高 不收JVM内存回收管理 7.8.2. 直接内存-工作流程 Java调用本地方法将CPU状态由用户态转变为内核态(因为Java并不具备磁盘读写的能力) CPU切换至内核态后,会在系统内存和Java堆内存中各开辟一块缓存区域,系统缓存区分批次读取磁盘文件数据后再复制一份到Java堆缓存区中,完成整个磁盘读取后会Java再次调用本地方法将将CPU状态由内核态转变为用户态 CPU切换为用户态后,Java使用IO读取Java堆缓存区中的数据 Java调用本地方法将CPU状态由用户态转变为内核态(因为Java并不具备磁盘读写的能力) CPU切换至内核态后,会直接在系统内存中划分一块儿缓冲区直接内存,Java堆和系统可以直接访问直接内存中的数据,这样就不用复制一份到Java堆中了 给直接内存分配太多空间但没有及时回收就会造成直接内存溢出
OutOfMemory:Direct Buffer Memory
7.8.3. 直接内存-调整内存大小 如果需要手动调整直接内存的大小,可以使用-XX:MaxDirectMemorySize=大小 单位k或K表示千字节,m或M表示兆字节,g或G表示干兆字节。默认不设置该参数情况下,JVM自动选择最大分配的大小。 以下示例以不同的单位说明如何将直接内存大小设置为1024KB: -XX:MaxDirectMemorySize=1m -XX:MaxDirectMemorySize=1024k -XX:MaxDirectMemorySize=1048576 7.8.4. 直接内存分配释放原理 Java中Unsafe对象完成直接内存的分配回收,回收时需要主动调用UnSafe对象的freeMemory方法 Java中的ByteBuffer接口实现类内部使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会有ReferenceHandler线程通过Cleaner的clean方法调用freeMemory方法来释放直接内存 一般JVM调优时,会使用-XX:+DisableExplictGC禁用显示垃圾回收(system.gc()),因为显示垃圾回收是一种full GC,对程序影响大,不仅会回收新生代对象,而且老年代对象一并回收,但是开启禁用之后,直接内存的回收就不会被回收,需要手动去获取UnSafe对象回收直接内存 8. JVM垃圾回收 8.1. 如何判断对象可以被回收 8.1.1. 引用计数法 每当对象被另外一个对象引用时计数+1,当对象的引用计数为0时,则说明当前对象无人引用可以被回收.
如果两个对象循环引用,而又没有其他的对象来引用它们,这样就造成垃圾堆积
8.1.2. 可达性分析算法 JVM中的垃圾回收器采用可达性分析来探索所有存货的对象 先确定根对象(GC Root),再扫描堆中的对象是否直接或间接的被根对象引用,如果没被引用,则可以垃圾回收,反之不收 GC Roots:一般是System Class,Native Stack,Thread,Busy Monitor 8.2. 五种引用 强引用:用不到的就回收 软引用:用不到不回收,内存满了再回收 弱引用:被发现了就回收 虚引用:弱引用+引用队列 终结器引用: 8.3. 垃圾回收算法 JVM根据情况采用不同的垃圾回收算法
8.3.1. 标记清除算法 标记清除算法
介绍:标记清除算法分为标记和清除两个阶段 标记:先沿着GC Roots对象的引用链寻找未被引用的对象,标记起来 清除:然后将这些对象的地址放置到闲置地址列表 优点:效率高速度快 缺点:容易产生内存碎片,清除之后就不会对内存空间做进一步整理 8.3.2. 标记整理算法 标记整理算法
介绍:在标记清除算法的基础上多一步整理内存空间过程 优点:不会产生内存碎片 缺点:移动对象会改变所有引用了该地址的对象,速度慢 8.3.3. 复制算法 复制算法
介绍:开辟两块一模一样的内存空间,先标记未应用的对象后,将已引用的对象复制到未使用的内存空间,清空原来空间,再交换两者的位置 优点:不会产生内存碎片 缺点:占用双倍内存空间 8.4. 分代垃圾回收 8.4.1. 老年代和新生代 堆中存在两块内存区域
新生代:由三部分组成 eden区: survivor From survivor To 老年代 8.4.2. 对象存储和垃圾回收过程 对象首先分配在eden区中 eden区内存不足时会触发minor gc,eden区和survivor From区存活的对象使用复制算法复制到survivor to区中,存活对象寿命+1,并交换From和To区,以此往复 minor gc会触发一次stop the world,暂停其他用户线程,等待垃圾回收完毕,用户线程才恢复运行 当对象寿命达到阈值时会转移到老年代中,最大寿命是15 老年代内存不足时,会先尝试触发minor gc,如果内存仍不足,会触发full gc,stw时间更长,采用的是标记整理算法 --超过新生代大小的大对象会直接晋升到老年代
8.4.3. 相关JVM参数 8.5. 垃圾回收器 8.5.1. 串行垃圾回收器 特点:单线程,垃圾回收时其他线程都暂停 使用场景:堆内存较小,单核CPU 流程图:serial新生代(复制)+serialOld(标记整理) 8.5.2. 吞吐量优先垃圾回收器 特点:多线程,尽可能让每次stop the world回收的垃圾数量多,类似多食少餐,并行,多个CPU运行单个垃圾回收线程 使用场景:堆内存较大,多核CPU 流程图: -XX:+UseParallelGC~-XX:+UseParallel0ldGC:开启并行垃圾回收器,这个参数同时影响新生代和老年代,JDK8下默认开启 -XX:+UseAdaptiveSizePolicy:采用动态自适应新生代大小空间分配 -XX:GCTimeRatio=ratio:调整吞吐量大小,垃圾回收时间=1/(1+radio) -XX:MaxGCPauseMillis=ms:调整暂停时间 -XX:ParallelGCThreads=n:控制parallelgc工作时的线程数 8.5.3. 响应时间优先垃圾回收器 特点:多线程,尽可能让每次stop the world的时间尽可能短,类似少食多餐,并发,单个CPU运行垃圾回收线程和用户线程,只有在并发gc清理垃圾速度跟不上垃圾产生速度的时候才会产生full gc 使用场景:堆内存较大,多核CPU 流程图: -XX:+UseConcMarkSweepGC~-XX:+UseParNewGC~SerialOld XX:ParallelGCThreads=n~-XX:ConcGCThreads=threads- XX:CMSInitiatingOccupancyFraction=percent- XX:+CMSScavengeBeforeRemark 8.5.4. G1 定义:garbage first,只有在并发gc清理垃圾速度跟不上垃圾产生速度的时候才会产生full gc 适用场景: 同时注重吞吐量和低响应时间,默认STW时间是200ms 超大堆内存,会将堆划分为多个大小相等的区域 整体上是标记整理算法,两个区域之间是复制算法 相关JVM参数 -XX:+UseG1GC:JDK8以前开启G1垃圾回收器 -XX:G1HeapRegionSize=size -XX:MaxGCPauseMillis=time 8.5.4.1. G1垃圾回收阶段 8.5.4.2. Young Collection 初始创建的对象存放在eden区 经历一次垃圾回收后对象存放在survivor区 对象寿命达到阈值后经过复制算法存放在老年代 8.5.4.3. Young Collection + Concurrent Mark Young GC时会进行GC Root的初始标记 老年代占用堆空间的比例达到阈值时,会触发并发标记 -XX:InitiatingHeapOccupancyPercent=percent(默认45%):调整阈值 8.5.4.4. Mixed Collection 会对eden区,survivor区,老年代的对象进行full gc 优先回收垃圾最多的区域,保证一次回收的垃圾释放最多的空间 最终标记会stw 拷贝存货会stw -XX:MaxGCPauseMillis=ms 8.6. 垃圾回收调优