JVM
- 一次编译终身运行
- 1.1 JVM和java的体系结构
- 1.1.1 虚拟机与JAVA虚拟机
- 1.1.2 JVM的位置
- 1.1.3 JVM的整体执行流程
- 1.1.4 JAVA代码的执行流程
- 1.1.5 JVM架构模型
- 1.1.6 JVM的生命周期
- 1.1.7 Sun Classic Vm
- 1.1.8 Exact VM
- 1.1.9 Hotspot VM
- 1.1.10 BEA的JRockit
- 1.1.11 IBM的 J9
- 1.1.12 KVM和CDC/CLDC Hotspot
- 1.1.13 Azul VM 和Liquid VM
- 1.1.14 Apache Harmony
- 1.1.15 Microsoft JVM和TaoBaoJVM
- 1.1.16 Dalvik vm
- 2.1 概述类的加载器和和类加载的过程
- 2.1.1 类的加载Loading
- 2.1.2 类加载Linking
- 2.1.3 类加载Initialization
- 2.1.4 类加载器
- 2.1.5 引导类加载器,扩展类加载器-系统类加载器
- 2.1.6 自定义类的加载器
- 2.1.7 双亲委派机制(面试重点1)
- 2.1.8 沙箱安全机制(面试重点2)
- 2.1.9 类加载的主动使用与被动使用
- 3.1 运行时数据区概述
- 3.1.1 线程
- 3.1.2 程序计数器(pc寄存器)(面试3)
- 3.1.2.1 pc寄存区的面试题
- 3.1.3 虚拟机栈
- 3.1.3.1 栈的好处
- 3.1.3.2 栈可能会出现的异常和设置栈的大小
- 3.1.3.3 栈的存储结构和运行原理
- 3.1.3.4 栈帧的内部结构
- 3.1.3.5 局部变量表
- 3.1.3.6 理解局部变量表
- 3.1.3.7 Slot
- 3.1.3.8 静态变量与局部变量的对比
- 3.1.3.9 操作数栈
- 3.1.3.10 理解操作数栈(图解)
- 3.1.3.11 栈顶缓存技术
- 3.1.3.12 动态连接与常量池的作用
- 3.1.3.12 方法的早期绑定和晚期绑定
- 3.1.3.13 四种指令调用区分非虚方法和虚方法
- 3.1.3.14 关于invokedynamic指令
- 3.1.3.15 虚方法表
- 3.1.3.16 方法返回地址
- 3.1.4 本地方法接口
- 3.1.5 本地方法栈
- 3.1.6 堆
- 3.1.6.1 堆空间和GC的简单了解
- 3.1.6.2 堆的内存细分
- 3.1.6.3堆空间大小的设置和查看
- 3.1.6.4 oom异常演示
- 3.1.6.5 新生代和老年代
- 3.1.6.6 对象的分配过程
- 3.1.6.7 对象分配的特殊情况
- 3.1.6.8
- 3.1.6.9 常用调优工具
- 3.1.6.10 MinorGc,MajorGc与FullGc
- 3..1.6.11 GC日志分析
- 3.1.6.12 堆空间的分代思想
- 3.1.6.13 内存分配策略
- 3.1.16.14 TLAB(线程缓冲区)
- 3.1.6.15 堆空间的常用设置
- 3.1.6.17 通过逃逸分析看堆空间的内存分配
- 3.1.6.18 逃逸分析之栈上分配
- 3.1.6.19 同步省略
- 3.1.6.21 代码优化之标量替换
- 3.1.6.22
- 3.1.7 方法区
- 3.1.7.1 方法区的基本概述
- 3.1.7.2 Hostpot中方法区的演练
- 3.1.7.2 设置方法区大小
- 3.1.7.3 方法区的内部结构
- 3.1.7.4 常量池
- 3.1.7.5 运行时常量池
- 3.1.7.6 方法区的执行流程
- 3.1.7.6 JDK 6-7-8的演变细节
- 3.1.7.7 StrigTable为什么要调整
- 3.1.7.8 静态变量的存储位置
- 3.1.7.8 运行数据区的面试题
- 3.1.8 对象的实例化内部布局与访问定位
- 3.1.8.1 字节码角度看对象创建过程
- 3.1.8.2 创建对象的步骤:
- 3.1.8.3 对象的内存布局
- 3.1.8.4 对象的访问定位
- 3.1.9 直接内存的使用
- 3.1.10 执行引擎
- 3.1.10.1 JAVA代码的编译和执行过程
- 3.1.10.2 机器码-指令-汇编-高级语言理解
- 3.1.10.3 解释器
- 3.1.10.4 JIT编译器
- 3.1.10.5 热点代码探测JIT
- 3.1.10.6 设置程序的执行方式
- 3.1.11 StringTable(字符串常量池)
- 3.1.11.1 String底层是Hashtable
- 3.1.11.2 String的字符分配
- 3.1.11.3 String的基本操作
- 3.1.11.4 字符串拼接操作
- 3.1.11.5 深入理解字符串拼接操作
- 3.1.11.6 字符串的拼接和StringBuilder
- 3.1.11.7 intern的使用
- 3.1.11.8 new String()创建了几个对象
- 3.11.1.9 intern()面试题
- 3.1.11.10 intern的对比
- 3.1.11.11 垃圾回收测试
- 3.1.12 垃圾回收
- 3.1.12.1 垃圾标记阶段
- 3.1.12.2 引用计数器
- 3.1.12.3 可达性分析算法与 GCRoots
- 3.1.12.4 对象的finalization机制
- 3.1.12.5 finalize 的对象复活
- 3.1.12.6 运用工具Mat查看Gc Roots
- 3.1.12.7 使用JProfiler查看
- 3.1.12.8 使用JProfiler查看异常
- 3.1.12.9 垃圾清除
- 3.1.12.10 标记清除算法 Mark-Compact
- 3.1.12.11 复制算法(copying)
- 3.1.12.11 标记压缩算法(Mark-Compact)
- 3.1.12.12 三种算法的对比
- 3.1.12.13 分代收集算法:
- 3.1.12.14 增量收集算法
- 3.1.12.15 分区算法
- 3.1.13 垃圾回收相关
- 3.1.13.1 System.gc()
- 3.1.13.2 手动GC的垃圾对于不可达的垃圾回收
- 3.1.13.3 内存溢出的问题
- 3.1.13.4 内存泄露的问题
- 3.1.13.5 Stop the word
- 3.1.13.6 程序的并行和并发
- 3.1.13.7 安全点和安全区域
- 3.1.13.8 JAVA中几种引用的概述
- 3.1.13.9 强引用(Strong Reference)——不回收
- 3.1.13.10 软引用(Soft Reference)——内存不够在回收
- 3.1.13.11 弱引用(Weak Reference)——发现既回收
- 3.1.13.12 虚引用
- 3.1.14 垃圾回收器的概述和分类
- 3.1.14.1 评估GC的性能指标
- 3.1.14.2 吞吐量和暂停时间的对比:
- 3.1.14.3 7款经典垃圾回收器
- 3.1.1.14.4 垃圾回收期的组合关系
- 3.1.14.4 查看默认的垃圾回收器
- 3.1.14.5 Serial回收器 串行回收
- 3.1.14.6 ParNew回收器 并行回收
- 3.1.14.7 Parallel回收器 吞吐量优先
- 3.1.14.7 Parallel回收器的参数设置
- 3.1.14.8 CMS回收器 低延迟
- 3.1.14.9 CMS回收器的优缺点
- 3.1.14.10 CMS垃圾回收器的参数设置
- 3.1.14.11 CMS收集器使用小结
- 3.1.14.12 认识G1垃圾回收器
- 3.1.14.13 G1垃圾收集器的好处和不足
- 3.1.14.14 分区(Region)
- 3.1.14.15 G1垃圾回收期的回收过程
- 3.1.14.16 G1垃圾回收的Remenbred Set
- 3.1.14.17 G1垃圾回收器过程详细说明
- 3.1.14.18 7种经典垃圾回收器总结
- 3.1.14.19 GC的日志
- 3.1.14.19 GC的日志分析
- 3.1.14.20 GC的日志分析2
- 2.0 Class文件的结构
- 2.1.1 字节码文件的跨平台性
- 2.1.2 通过字节码指令看细节(1)
- 2.1.3 通过字节码指令看细节(2)
- 2.1.4 通过字节码指令看细节(3)
- 2.1.5 Class文件本质和内部数据类型
- 2.1.6 Class文件内部结构概述
- 2.1.7 字节码保存到Excel中
- 2.1.7.1 Class文件标识:魔术(magic)
- 2.1.7.2 Class文件标识:版本号
- 2.1.7.3 Class文件常量池
- 2.1.7.4 Class文件常量池计数器
- 2.1.7.5 常量池的字面量和符号引用
- 2.1.7.6 Class文件常量池的解析
- 2.1.7.7 访问标识
- 2.1.8 JAVA -g的操作说明
- 2.1.9 javap的指令
- 2.1.10 字节码
- 2.1.10.1 字节码与数据类型
- 2.1.10.2 加载与存储指令
- 2.1.10.3 操作数栈和局部变量表
- 2.1.10.4 局部变量表的压栈指令
- 2.1.10.5 常用入栈指令
- 2.1.10.6 出栈装入局部变量表指令
- 2.1.10.7 算数指令
- 2.1.10.8 i++和++i
- 2.1.10.9 对象的创建与访问指令
- 2.1.1010 字段访问指令
- 2.1.10.11 数组操作指令
- 2.1.10.12 方法的调用
- 2.1.10.13 方法的返回值
- 2.1.10.14 操作数栈的指令
- 2.1.10.15 比较指令
- 2.1.10.16 条件跳转指令
- 2.1.10.17 比较条件跳转指令
- 2.1.10.18 无条件跳转指令
- 2.1.10.19 抛出异常指令
- 2.1.10.20 异常处理和异常表
- 2.1.10.21 同步控制指令
- 2.1.11 类的生命周期概述
- 2.1.11.1加载完成的操作以及二进制文件的获取
- 2.1.11.2 类模型和class实例的位置
- 2.1.11.3 链接阶段之验证阶段
- 2.1.11.4 链接阶段之准备阶段
- 2.1.11.4 链接阶段之解析阶段
- 2.1.11.5 初始化阶段
- 2.1.11.6 类的主动使用和被动使用
- 3.0 性能监控与性能调优
- 3.0.1 JPS查看正在运行的JAVA进程
- 3.0.2 jstat的使用
- 3.0.3 排查oom和内存泄露
- 3.0.4 jinfo
- 3.0.5 jmap
- 3.0.5.1 导出dump映射文件的方式
- 3.0.5.2 显示堆内存的相关信息
- 3.0.6 JDK自带的分析服务
- 3.0.7 GUI图形化界面JVM工具介绍
- 3.0.8 jconsole 的使用
- 3.0.8 jvisualvm
- 3.0.9 MAT
- 3.0.9.1 MAT的Histogram
- 3.0.9.2 分析内存泄露
- 3.0.10 内存泄漏的理解和分析
- 3.0.10.1 JAVA中内存泄漏的8种情况
- 3.0.11 JProfiler
- 3.0.11.1 遥感监测视图中相关检测数据
- 3.0.11.2 内存视图的分析
- 3.0.11.3 Heap Walker
- 3.0.11.4 CPU视图功能说明
- 3.0.11.5 Thread视图的功能
- 3.0.11.6 代码演示
- 3.0.12 Arthas
- 3.0.13 JVM的参数选项-标准参数选项
- 3.0.14 添加JVM参数选项
- 3.0.15 常见的JVM参数
- 3.0.15.1 打印设置的XX选项和值
- 3.0.15.2 堆,栈,方法区垃圾收集器等内存的设置
- 3.0.15.2 GC日志相关参数
- 3.0.15.3 其他参数
- 3.0.15.4 yangGC分析
- 3.0.15.5 FullGC分析
一次编译终身运行
-
一次编译终身运行,.java文件会被编译成为字节码文件,然后会在不同的平台上去解释运行
-
除了JAVA语言,其他的语言可以再JAVA的虚拟机上运行
-
其他的语言只需要提供对应的编译器且遵循JAVA的字节码规范,然后编译成为字节码文件就可以在虚拟机上运行了
-
JAVA虚拟机根本不关心运行在其内部的是何种语言编写的,
它只关心字节码文件
也就是说JAVA的虚拟机拥有语言的无关性,不会与JAVA语言进行绑定,只要其他的编程语言编译结果满足并包含JAVA虚拟机的内部指令集、符号表以及其他的指令信息。
它就是一个有效的字节码文件,就能被虚拟机所识别并运行
JAVA不是世界上最强大的语言,但JVM是世界上最强大的虚拟机
1.1 JVM和java的体系结构
1.1.1 虚拟机与JAVA虚拟机
- 虚拟机:就是一台虚拟的计算机,它是一款软件,用来执行一系列的虚拟计算机指令,大体上虚拟机可以分为
系统虚拟机和程序虚拟机
- Visual,Box,VMware就是系统虚拟机,它们是完全
物理计算机的仿真
提供一个可完整运行的软件操作平台 - 程序虚拟机的典型代表是JAVA虚拟机,
它是为了执行单个计算机的程序而设计,
在JAVA虚拟机中执行的指令,我们又称之为java字节码文件 - 无论是系统虚拟机还是程序虚拟机在上面运行的软件都被限制与虚拟机提供的资源中
JAVA虚拟机 - 是一款用于执行JAVA字节码文件的虚拟计算机,它拥有独立的运行机制,其运行的JAVA字节码文件也未必由JAVA编写
- JVM平台的各种语言可以共享JAVA虚拟机带来的跨平台性,优秀的垃圾回收器。以及可靠的即时编译器
JAVA的技术的核心就是JAVA虚拟机
(JVM,java.Virtual,Machine),所有的JAVA程序都是运行在JAVA虚拟机内部
1.1.2 JVM的位置
- JVM是在操作系统之上的,不同的操作系统上的JVM是有区别的
- 首先.java文件–>.class文件这个过程中会有一个
前端
编译器,比如JAVAC就是一个前端的编译器. - .class文件–JVM上运行会有一个
后端
的编译器,就是JVM
1.1.3 JVM的整体执行流程
- 首先现在默认的是JAVA是运行在HotSpot虚拟机上
- HostSpot是目前目前市面上最高性能的虚拟机代表之一
- 它采用解释器与编译器共存的架构
1.1.4 JAVA代码的执行流程
- 第一次我们写好的JAVA文件通过前端编译器编译为字节码文件
- 然后通过类加载器将.class文件加载进去,进行字节码的效验
- 翻译字节码,执行,和编译器编译执行时共存的,因为电脑是识别不出class文件的,需要编译器重新将.class文件重新编译为电脑可识别的语言(汇编语言)
1.1.5 JVM架构模型
JAVA编译器输入的指令流,基本上是一种基于栈的指令集架构
,另外一种指令集架构则是基于寄存器的指令集架构
具体来说这两种架构之间的特点:
- 基于栈架构的特点
1)设计和实现更简单,适用于资源受限的系统,
2)避开了寄存器的受限难题,使用零地址指令分配,
3)指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈,指令集更小,编译器更容易实现
4)不需要硬件的支持,可移植性好
- 基于寄存器架构的特点
1)典型的应用是x86的二进制指令集,比如传统的pc以及android的Daclik虚拟机
2)指令集架构完全依赖于硬件,可移植性差
3)性能优秀和执行更高效
4)花费更少的指令去完成一项操作
5)在大部分的情况下,基于寄存器架构的指令集,往往都以一地址指令,二地址指令和三地址指令等,而基于栈式的架构确实以0地址值为主
- 代码试验一下
- 进到class字节码文件夹中,执行 javap -v Demo.class命令对字节码进行反编译
- 因为带啊操作比较简单,所以直接就是5,这个直接写int i=5一样,看字节码文件
- 1.生成1个值为5的变量
- 2.保存到操作数(索引)为1的栈中
- 3.返回
---------------.
- 观看字节码文件
- 1.生成一个值为2的变量
- 2.存在栈中(索引)1的位置
- 3.生成一个值为3的变量
- 4.存在栈中(索引)2的位置
- 5.将值为2的变量加载进来
- 6.将值为3的变量加载进来
- 7.进行相加的操作
- 8.返回
- 总结:
由于跨平台的设计,JAVA指令都是根据栈来设计的
,不同平台cpu架构不同,所以不能设计为基于寄存器的,优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令
直至今日,尽管嵌入式平台已经不是JAVA的主流平台了,(准确来说应该是hoststop的宿主环境已经不局限于嵌入式平台了),那么为什么不把架构设为基于寄存器的呢
栈 跨平台性,指令集小,指令多,执行性能比寄存器差
1.1.6 JVM的生命周期
- 虚拟机的启动
JAVA虚拟机的启动时通过类加载器(bootstrap class loader)创建一个初始类(initiail来完成的),这个类是由虚拟机的具体实现指定的
好比如说自定义一个类,这个类执行的时候没有实现接口,就要先执行OBJECT
,好比如想吃苹果了,但是没有苹果树,就需要先种苹果 - 虚拟机的执行
一个运行的JAVA虚拟机有一个清晰的任务,就是执行JAVA进行
程序开始执行的时候它开始运行,程序执行结束的时候,它停止
执行一个所谓的JAVA程序的时候,真真正正的是在执行一个JAVA虚拟机
- 虚拟机的关闭
正常情况下执行完毕后退出
程序在执行过程中,遇到了异常或者错误,强行终止了
由于操作系统出现错误从而导致了程序运行结束
某线程调用了Runtime类或者System.exit的方法,或Runtime类的halt方法,并且JAVA管理器也允许这次exit的操作
除此之外,JNI(JAVA Native Interface)规范描述了用Invocation AP来加载或者卸载,JAVA虚拟机的退出情况
1.1.7 Sun Classic Vm
- JAVA1.0发布的时候,Sun公司发布了一款名为Sun Classicvim的虚拟器,
它是世界上第一款商用的JAVA虚拟机
,JDK1.4的时候就被淘汰了 - 这款虚拟机内部只提供
解释器
- 如果使用JIT编译器就需要进行外挂,但是一旦使用了编译器JIT就会接管虚拟机的执行系统,解释器就不在工作了解释器和编译器就不能配合工作
- 现在的JAVA使用的是Hostpot虚拟机
内置了解释器和JIT编译器
如果只有解释器的话效率会非常低,对于重复的代码会挨个执行,JIT编译器对于执行次数非常多的代码,会把他看做是热点代码,编译成本地指令,对其进行缓存,当你下次用这个热点代码的时候就不用像解释器一样去逐行在解析了
1.1.8 Exact VM
- 为了解决上一个虚拟机存在的问题,JDK1.2的时候,提供了此虚拟机,
- Exact Menmory Management 精准式内存管理
- 虚拟机可以知道内存中的某个位置的数据是什么类型
- 具有现代高性能虚拟机的雏形
- 热点探测,具有编译器和解释器混合的工作模式
- 但是只能在solatis平台上使用,其他平台上还是classic Vm(就是只能在sun公司自己的公司使用,还没等在其他的平台使用,但是英雄气短就被现在的Hotspot所替代)
1.1.9 Hotspot VM
- 最初由一家"Longview Technologies "的小公司设计
- 1997年,此公司被sun公司收购,2009年,Sum公司被甲骨文收购
- JDK1.3的时候,Sun公司正式将HotsPot VM成为默认的虚拟机
- 不管是仍在使用的JDK6还是一直稳定的JDK8,默认的虚拟机都是Hots Pot
- sun /Oracle JDK/和open JDK的默认虚拟机
- 从服务器,桌面到移动端都有应用
- 名称中
Hots pot指的是热点代码探索技术
(就是重复执行的代码为热点代码进行缓存) - 通过计数器找到最具编译价值的代码,触发及时编译或者栈上替换
通过编译器和解释器的协调工作,在最优的程序响应时间与最佳执行性能中取得平衡
1.1.10 BEA的JRockit
专注于服务端的应用
- 它不太关注程序的启动速度,因此
JRokit内部不好包含解释器
,全部代码都靠编译器编译之后在执行 - 使用JRocki产品,客户已经体验到显著的性能提升(一些超过了70%),硬件成本的减少(50%)
- 优势:全面的JAVA运行时解决方案组合
- JRockit面向延迟敏感性应用的解决方案JRockit,Real,Time 提供毫以毫秒或者微妙的JVM响应时间,适合财务,军事指挥,电信网络的需要
- MissionControl服务组件,它是一组极低的开销来监控,管理和分析成产环境下用用程序的工具
Oracle表达了整合两大优秀虚拟机的工作,大致在JDK8中完成,整合的方式是在Hotspot的基础之上,移植JRockit
JRokit也是最快的虚拟机,因为他是专注于服务端的应用,不考虑程序的响应时间,
1.1.11 IBM的 J9
- 全程 IBM Technology for Java Vietual Machine 简称 IT4J 内部带好J9
- 市场定位与Hots pot接近,服务端,桌面应用 嵌入式开发等用途的VM
- 广泛用于IBM的各种Java产品
- 目前
有影响力的三大虚拟机之一
也是号称世界上最快的JAVA虚拟机 - 2017年左右IBM发布了开源J9VM ,命名为openJ9,交给Eclipse基金会管理,也称为Eclipse openJ9
- ps:
也不能称为最快,他在自己产品上运行时最快的,如果是在win10等场景下使用会差很多
1.1.12 KVM和CDC/CLDC Hotspot
- Oracle在JAVA me产品线上的两款虚拟机分别是CDC/CLDC HotsPot Implementation VM
- KVM 是CLDC-HI早起产品
- 目前移动领域地位尴尬,只能手机被Android和ios二分天下
KVM简单,轻量,高度可移植,面向更低端的设备商还维持自己的一片市场’
- 只能控制器,传感器
- 老人手机,经济欠发达的功能手机
- 所有的虚拟机的原则,一次编译到处运行
1.1.13 Azul VM 和Liquid VM
- 前面三大高性能的虚拟机使用在通用的硬件平台上(耦和低)
- 这里Azul VM 和BEA Liquid VM 是
与特定硬件平台绑定,软硬件配合的专用虚拟机(耦和较高)
- Azul是Azul System 公司在Hots Pot基础之上进行大量改进,运用于 Azul System 公司的专有硬件 Vega系统上的Java虚拟机
每个Azul Vm 实例都可以管理至少数十个cpu和数百GB内存的硬件资源,并提供巨大内存范围内实现可控的Gc时间的垃圾收集器,专有硬件优化的线程调度等优秀特性
- 2010年,Azull System 公司开始从硬件转向软件,发布了自己的Zing JVM,可以再通用 X86 平台上提供接近于Vega系统的特性
- Liquid VM
- 高性功能的战斗机,
- BEA公司开发了,直接运行在自家的Hypervisor系统上
- Liquid VM 既现在的JRockit VE(Virtual Rdition)
Liquid vm不需要操作系统的支持,或者说它自己实现了一个专用操作系统的必要功能,如网络调度,文件系统,网络支持等
- 随着JRockit虚拟机终止开发,Liquid VM项目也停止了
1.1.14 Apache Harmony
- Apache也曾经推出过与JDK 1.5和JDK1.6 兼容的Java运行平台Apache Harmony
- 它是IBM和Intel联合开发的开源JVM,受到同样开源的open JDK的压制,Sun 坚决不让Harmony获得JCP认证,最终于2011年退役,IBM转身参与Open JDK
- 虽然目前并没有Apache Harmony被大规模商用案例,但是它的Java类库代码吸纳进了Android SDK
1.1.15 Microsoft JVM和TaoBaoJVM
-
微软为了能在IE3浏览器中支持 Java Applets 开发了Microsfot JVM
-
只能在Window平台下运行,但确是当时Window下性能最好的VM,
-
1997年Sun公司以侵犯商标,不正当罪名指控微软成功配了Sun公司很多钱,微软在Windowxp sp3中抹掉了其VM,现在Window上安装的JDK都是Hostpot
-
TaoBaoJvm
-
由于Alijvm团队发布,阿里,国内使用Java最强大的公司,覆盖云计算,金融,物流,电商等众多领域,需要解决高并发,高可用,分布式等复合问题,有大量的开源产品
-
基于OPenJDK 开发,定制了自己的版本AlibabJVM
,简称AJDK,是整个阿里体系的结石 -
1)创新的Gcim(Gc Invisble heap)技术实现了off-heap
既将生命周期较长的对象,从heap中移到了heap之外,并且Gc不能管理Gcih内部的JAVA对象,降低了Gc的回收频率和Gc的回收效率
-
2)Gcih中的
对象能实现在多个JAVA虚拟机进程实现共享
-
3)使用crc32指令实现JVM intrinsic,降低了Jni的调用开销
-taohaoJVM应用在ali产品上性能最高,硬件严重依赖inter的cpu,损失了兼容性,但是提高了性能
1.1.16 Dalvik vm
2.1 概述类的加载器和和类加载的过程
- 类加载器子系统负责从文件系统或者本地加载Class文件,Class文件在开头会有固定的标识
- ClassLoad只负责class文件的加载,至于他是否运行,由Execution Engine决定的
- 加载的类信息存放于方法区中的内存空间,除了类以为,方法区中还会存放于运行时的常量池信息,可能还包括字符串,字面量和数字常量,(这部分信息是class文件中常量池部分的内存映射)
- 常量池加载到内存中叫做运行时常量池
Car file被编译成为Car.class文件,存在于硬盘中
通过类加载器,加载到内存当中,存放于方法区中,被称为DNA元数据模板
我们通过car.class调用getClassLoad方法可以获得是谁加载的这个类(获取到了加载器)
在内存中调用car的构造器,就可以创建对应的方法,在堆空间中
如果当前这个类没有加载,就需要classload去加载,如果没有加载上就会抛出异常
2.1.1 类的加载Loading
- 通过一个类的全限定名获取此类定义的二进制文件流
- 将这个字节流所代表的今天存储结构转为方法区的运行时数据结构
在内存中生成一个代表这个类的Java.lang.class对象,作为方法区这个类的各种数据的访问入口
- 加载.class文件的方式
-
- 从本地直接加载
-
- 通过网络获取,典型场景 Web Applet
-
- 从Zip压缩包中获取,成为日后jar ,war的基础
-
- 运行时计算生成,使用最多的是动态代理技术
-
- 由其他文件生成,典型的是 JSP应用
-
- 从专有数据库中提取,.calss文件,比较少见
-
- 从加密文件中获取,典型的防class文件被反编译的保护措施
2.1.2 类加载Linking
- 第一个阶段是加载,第二个阶段是Linking(连接阶段)
- 验证(Verify)
- 目的在确保Class的字节流中包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害到虚拟机的安全,主要包含四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
- 对于JAVA的字节码,开头是有CA FE BA BE的标识,每一个class文件都是如此,如果不是会抛出异常
- 准备(Prepare)
- 为类的变量分配内存并且修改默认值,为零值
这里不包含用final修饰的static,因为final在编译的时候就赋值了,准备阶段会显示初始化,
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到JAVA的堆中
- 解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程
- 事实上解析操作引用往往会伴随着JVM在 执行完初始化之后在执行
- 符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在《JAVA虚拟机规范》的class文件格式中,直接引用就是直接指向目标的指针,相对偏移量或者一个间接定位到目标的句柄
- 解析对象主要针对类或接口,字段类方法,接口方法,方法类型等,对应常量池中的CONSTANT_class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info等
- 将常量池内会加载很多的东西,我们自己的类,还有objec类,用到了输出还会加载System
2.1.3 类加载Initialization
-
初始化
-
初始化阶段就是执行类的构造方法<Clinit>的过程
-
此方法不需要定义,是Javac编译器去自动收集所有的类赋值动作和静态代码块合并来的
-
将变量1引进来,是一个静态的变量,然后return
-
加上静态代码块,由1赋值为2,然后结束
-
初始化的操作是顺序执行的
-
增加了一个变量,给出是10,让其在静态代码块中赋值为20
-
因为这个类在Linking阶段就已经加载了,默认值是0,然后在初始化阶段,代码是顺序执行的,先赋值20,在赋值10
-
<Clinit>不同于构造器
,构造器是虚拟机视角下的 init
-
当前代码没有静态代码,也没有静态代码块,是没有clinit的
-
这是idea的一个插件叫jclasslib
-
当我们加上静态代码块就会有Clinit
-
任何一个类声明了都会有构造器,不写就是默认的构造器,构造器执行的时候会先执行object
-
若该类具有父类,JVM会保证子类的 Clinit 执行之前父类的clinit已经执行完毕
-
虚拟机必须保证一个类的Clinit方法在多线程中是被同步加锁的(一个类只会被加载一次)
2.1.4 类加载器
JAVA 支持两种类型的加载器分别为引导加载器(Bootstrap classLoader)和0自定义加载器(User -DefindClassLoad)
- 从概念上来讲自定义加载器一般指的是程序由开发人员自定义的一种类加载器,但是JAVA虚拟机规范却没有这么定义,
而是将所有的派生于抽象类classload的类的加载器都归为 自定义加载器
- 无论加载器的划分,我们程序中常见的只有三个
- 第一个是引导类加载器
- 第二是扩展类加载器
- 第三个是系统类加载器
- 第四个是自定义加载器
- 上面说了
将所有的派生于抽象类classload的类的加载器都归为 自定义加载器
- 是内部类
- 他们之间是一种包含的关系
- 我们自定义类的class是包含与appclassload的
- String类型也是用的bootatopclassload,
Java中所有重要的API用的都是引导类加载器,是不能直接获取到的
2.1.5 引导类加载器,扩展类加载器-系统类加载器
- 虚拟机自带的加载器
- 启动类加载器(bootstrap ClassLoad)
- 这个类是c/c++编写的嵌套在JVM内部
- 它是用来加载JAVA的核心类库,(JAVA/HOME/jre/lib/rt.jar ,resourese.jar ,或者sun.boot.class.path)下的路径用于提供JAVA自身锁需要的类
- 并不继承JAVA.long.classsload,没有父加载器
- 加载扩展类和应用程序类加载器,并指定他们为父加载器
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类
- 扩展类加载器(Extension ClassLoad)
- 由JAVA语言编写,有sun.misc.Launcjer$ExtClassLoad实现的
派生于ClassLoad类
- 父类加载器为启动类加载器
- 从java.net.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库,`如果用户创建的jar放到此目录,则会直接由扩展类加载器加载
- 应用程序加载器(APPClassLoad)
- java语言编写,由sun.misc.Launcher$AppClassLoad实现
- 派生于ClassLoad类
- 父类加载器为扩展类加载器
- 它负责加载环境变量ClassPath或系统属性 java.class.path 指定路径下的类库
该类加载器是程序中默认的类加载器
一般来说JAVA应用的类都是由它来加载完成的- 通过classLoader#getSystemClassLoad()方法可以获取到该类的加载器
- 可以看到启动类加载器会加载哪些核心的类库
2.1.6 自定义类的加载器
- 在Java的日常应用和软件开发中,类的加载几乎是由上述的三个类加载器去配合执行的,在必要的时候可以自定义加载器
- 自定义加载器的好处(隔离加载类,修改类的加载方式,扩展加载源,防止源码泄露)
- 实现步骤:
- 开发人员可以通过继承类的抽象类java.long.classLoad类的方式,实现自己的加载器,满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器的时候,总会去继承ClassLoad类去重写classload()方法,从而实现用户自定义类的加载,但是在JDK1.2之后用户就不建议去附带classload方法,而是建议把自定义类的加载逻辑写在findclass()中,
- 在编写自定义类加载器的时候,如果没有太过于复杂的要求,可以直接继承URLClassLoad类,这样就可避免自己去编写findclass()方法,以及获取字节码流的方式,使得自定义类的加载更加简介
2.1.7 双亲委派机制(面试重点1)
- Java虚拟机对class文件采用的是
按照需加载的方式
,也就是说当需要使用到该类的时候才会将他加载进来,生成class对象,而且加载某个类的文件的时候,JAVA虚拟机采用的是双亲委派的模式
既把请求交给父类处理,它是一种任务的委派模式
- 假设我们自己的项目本地有一个java.lang.String这个类
- 我们的JAVAapi上的String也是 java.lang下的,我们实例化String的时候无法判定到底是执行的哪个,到底是apI中的String还是自定义的String,这里就要说到了
双亲委托
- 我们执行main方法的时候,本类的加载器是appclassload,也就是系统加载器,然后上面是Extclassload,系统加载器,然后是bootstap classload 也就是引导类加载器,执行的时候先执行引导类加载器,让他去执行String str=new String(),如果引导类加载器执行成功了,那么下面就不用在执行了,如果引导类中没有String,那么就向下执行ExtClassLoad,如果ExtClassLoad中没有,就执行Appclassload,这就是双亲委派
- 工作原理:
-
- 如果一个类加载器收到了类加载请求,他不会先去自己加载,而是把这个请求委托给父类的加载器
-
- 如果父类加载器还存在父类加载器,则进一步向上委托,一次递归,最终达到顶层的父加载器,
-
- 如果父类加载器能加载成功那么就成功返回,如果父类加载器不能成功加载,子类加载器才会去尝试加载,这就是双亲委派模式
- 如果父类加载器能加载成功那么就成功返回,如果父类加载器不能成功加载,子类加载器才会去尝试加载,这就是双亲委派模式
- 优势: 避免重复加载,板胡程序的安全,防止核心API被篡改
2.1.8 沙箱安全机制(面试重点2)
- 是对源代码的一种保护,比如创建一个java.lanng.xxx下应该到bootstap classload去加载,但是里面没有这个类,但是确实是java的核心api中的java.long 这样就会报错,这是对java api的一种保护
2.1.9 类加载的主动使用与被动使用
- 主动使用
- 1.创建类的实例
- 2.访问某个类或者接口的静态变量,或者对该静态变量赋值
- 3.调用类的静态方法
- 4.反射
- 5.初始化一个类的子类
- 6.java虚拟机启动的时候被表明是启动类的类
- 7.JDK7开始提供的动态语言技术
- 除了以上7种情况,其他使用java类方式都被看做是
对类的被动使用
,都不会导致类初始化
- 以上是类加载的详细的过程
3.1 运行时数据区概述
- 内存是非常重要的系统资源,是硬盘和cpu的中间仓库,及桥梁,承载着操作系统和应用程序的及时运行,jvm内存布局规定了JAVA在运行内存过程中内存的申请,分配,管理的策略,保证了jvm的高效稳定的运行
不同的jvm对于 内存的划分方式,有着部分的差异
- Java虚拟机中定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机的启动而创建,随机虚拟机的销毁而销毁(生命周期和虚拟机的声明周期相同)
- 另外则是一些与线程是一一对应的,这些线程对应的数据区会随着线程开始和结束而创建和销毁
- 灰色的为单线程私有的,红色的为多个线程共享
每个线程独立,包括程序计数器,栈,本地栈
线程之间共享:堆,堆外内存(永久代或元空间(方法区),代码缓存)
3.1.1 线程
- 线程是一个程序中的运行单元,JVM允许一个应用有多个线程并行执行,
- 在HotsPot jvm中,每个线程都与操作系统的本地线程直接映射,
- 当一个JAVA线程准备好执行以后,此时一个操作系统的本地线程也同时创建,JAVA线程终止执行之后,本地线程也会回收
- 操作系统负责所有线程的安排调度到任何一个可用的cpu上,一旦本地线程初始化成功,它就会调用run()方法
- 如果是使用jconsole或者是任何一个调试工具,后能看到后天有许多线程,这些后台线程不包括main线程和自己创建的线程
- 这些主要的后台线程在Hostpot,jvm里主要是一下几个
- 虚拟机线程: 这种线程的操作是需要JVM操作达到安全点的时候才会出现,这些操作必须在不同的线程中发生的原因是他们都需要jvm的操作到达安全点,这样堆才不会变化,这种线程的执行类型包括“stop-the-word”的垃圾收集,线程收集,线程挂起以及偏向锁撤销
- 周期任务线程: 这种线程是时间周期任务的体现(比如中断),他们一般用于周期性的操作调度执行
- Gc线程: 这种线程对于jvm中不同类型的垃圾收集行为提供了支持
- 编译线程: 这种线程在运行时会将字节码编译为本地代码
- 信号调度线程 这种线程接受信号并发送给jvm,在它内部通过调用适当的方法进行处理
3.1.2 程序计数器(pc寄存器)(面试3)
- JAVA中的程序计数寄存器,(program Counter Register)中,Register的命名源于cpu的寄存器,寄存器存储指令相关的信息,cup只有把指令转载到寄存器中才能运行
- 这里并非是指广义上所指的物理寄存器,或者将其翻译为pc寄存器(或指令计数器),会更加贴切,也就是程序的钩子,这样也不会引起不必要的一些误会,
JVM中的pc寄存器,是对物理pc寄存器的一种抽象模拟
- pc寄存器的作用:
pc寄存器用来存储指向下一条指令的地址,也是要即将执行的代码,由执行音频读取下一行代码(可以理解为pc寄存器是用来存储下一条应该执行什么命令,读取的时候在pc寄存器中读取下一行要执行的指令)
- 它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
- 在jvm的规范中,每个线程都有他自己的程序计数器,是线程私有的,生命周期和线程一样
- 任何时间一个线程都只有一个方法在执行,也就是当前方法, 程序计数器会存储当前线程
JAVA方法的jvm指令地址
或者如果你在执行native方法,则是未指定值(undfned) - 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程回复等基础功能都是依赖于计数器完成的
- 字节码解释器工作的时候通过改变这个计数器的值来选下一条要执行的代码,
- 他是唯一一个在java虚拟机规范中没有任何OutOtMemoryError情况的区域
- 重新编译一下
假设pc集群器存储的是5,然后执行音频去pc寄存器中读到了5,进行对应的操作,(程序的钩子),执行引擎去转换为对应的机器指令,然后调用cpu去执行
- 所以:PC寄存器是来存储我们下一条的指令的,接下来该执行哪一行代码了,直接去pc寄存器中取出即可
3.1.2.1 pc寄存区的面试题
使用pc寄存器字节码指令地址有什么用?为什么使用pc寄存器记录当前线程的执行地址?
- 因为cpu需要不停的切换线程,这个时候切换回来之后就不知道该执行哪个了
- JVM的字节码解释器就需要通过改变pc寄存器的值来明确下一条应该执行什么样子的字节码指令
- pc寄存器为什么设为线程私有?
- 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
3.1.3 虚拟机栈
-
由于跨平台的设计,JAVA指令都是通过栈来设计的,不同平台的cpu架构不同,所以不能设计是基于寄存器的
-
优点是跨平台,指令集更小,编译容易实现,缺点是性能下降,实现同样的功能是需要更多的指令
-
栈是程序运行时的单位
-
栈解决程序的运行问题,既程序如何执行,如何处理这些数据,堆解决的是数据存储的问题,既数据应该怎么放,放在哪
-
做菜举例
-
左边是栈,右边是堆
-
JAVA虚拟机栈是什么?
-
JAVA虚拟机栈(Java Virtual Machine Stack)早期也叫java栈,每个线程在创建的时候都会创建一个java虚拟机栈,其内部保存一个个的栈帧(可以理解为方法),对应的一次次java方法的调用
-
methodA先执行,他是在下面,然后调用了b,那么最上面的(俗称顶栈),也就是b,既然入栈已经完成了,出栈是b先出栈
-
栈的生命周期是和线程一致的,随着线程的创建而创建
-
作用:
-
主管java的程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回
-
局部变量vs 成员变量(属性)
-
基本数据变量vs引用数据变量(类,数组,接口)
3.1.3.1 栈的好处
- 栈是一种快速有效的数据分配存储方式,速度仅次于计数器
- JVM直接对java栈的操作有两个1.每个程序执行伴随着进栈(入栈,压栈)2.执行结束之后的出栈
- 对于栈来说不存在垃圾回收的问题,但是有oom的问题,如果一直死循环递归,就会报错oom的问题
3.1.3.2 栈可能会出现的异常和设置栈的大小
- 栈可能会出现的异常
- Java虚拟机中是允许
Java栈的大小是动态的,或者是固定不变的
- 如果采用固定大小的JAVA虚拟机栈,那么每一个JAVA虚拟机的栈的容量可以再线程创建的时间独立选择,如果线程请求分配的栈容量超过了JAVA虚拟机栈的最大容量系统则会抛出一个
StackOverflowError异常
- 如果java虚拟机栈可以动态扩容,并且在尝试扩展的时候无法申请到足够大的内存,或者在创建新的线程的时候,没有足够的内存去创建对应的虚拟机栈,那么java虚拟机将会抛出一个
OutOfMenmoryError异常
- 自己调用自己超过了栈的最大内存然后挂了
- 我电脑默认的是11415,然后将栈调小,变成了2456
3.1.3.3 栈的存储结构和运行原理
- 栈帧能存储什么?
每个线程都有自己的栈,栈中的数据是以栈帧的方式存在的
在这个线程中,每一个方法都对应的一个栈帧
栈帧是一个内存区,是一个数据集,维护者方法执行之前的各种数据信息 - JVM直接对JAVA的栈的操作只有两个,
入栈/出栈,遵循先进后出,后进先出
, - 在一条活动线程之内,一个时间点只能有一个活动在执行,就是当前的正在执行的栈帧,也就是顶栈,
也可以说是当前栈
,与当前栈相对应的是当前方法区
,定义这个类就是当前方法类
- 执行引擎运行的所有字节码指令,只只针对当前栈帧进行操作
- 如果该方法中调用了其他的方法,对应新的栈帧就会被创建出来,存放在栈的最顶端,称之为当前帧
- 咱们编译一下
- 不同线程中锁包含的栈帧 是不允许相互引用的,既不可以在一个栈帧之中引用另外一个线程的栈帧
- 如果当前方法调用了其他的方法,方法返回之际,当前栈帧会传回此方法的执行结果,给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧,重新成为当前栈帧
- JAVA有两种返回函数的方式,
一种是正常的返回,使用return指令,另外一种是抛出异常,
3.1.3.4 栈帧的内部结构
每个栈帧中存在着局部变量表(Local Variables)
操作数栈(operand Stack)(或表达式栈)
- 动态连接(Dynamic Linking)(或者指向运行时,常量池的引用)
- 方法返回地址(Return Address)(或方法正常退出,异常退出的定义)
3.1.3.5 局部变量表
- 局部变量表也被称为局部变量数组或者本地变量表
定义为一个数字数组,主要用于存储方法参数,和自定义在方法内的局部变量
,这些数据类型包含各种基本数据类型,对象引用以及(return Address)类型- 由于局部变量表是建立在线程的栈上,是线程的私有数据,
因此不存在数据的安全问题
局部变量表所需的容量大小是在编译的时候就确定两个
,并保存在方法的code属性的maximun local variable数据项中,在方法运行期间是不会导致局部变量表的大小的方法嵌套调用的次数由栈的大小决定的,一般来说,栈越大,方法嵌套调用次数越多
对于一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递信息增大的需求,进而函数调用就会占用更大的栈空间,导致其嵌套调用的次数会减少局部变量表中的变量在当前作用域中有效,当方法调用完成之后,随着方法栈的销毁,局部变量表会随之一起销毁
- 局部变量表的长度是3
- 这三个中一个是arg,一个是new的对象地址,一个是定义的num。这个就是main方法中局部变量表包含的内容
- 这里也可以看到信息
3.1.3.6 理解局部变量表
- 拿main方法举例,可以看到名字是main方法,是public staric 的,返回值是void ,参数是String类型的
- 这个是代码的行数,14行定义了一个num的变量,对应的是字节码质量的行号的是8
- 这个是用来描述,局部变量的作用域的
- arg对应的字节码指令是从0开始,长度是15
- 后面的是局部变量表存储的变量的名字和描述
3.1.3.7 Slot
- 也叫槽位,
- 参数的值的存放是在局部变量数组的index0开始的,到长度-1结束
- 局部变量表,其中最基本的单元是
slot(槽)
- 局部变量表中存放编译器可知的各种基本数据类型,,引用类型和returnAddress
- 在局部变量表中
32位以内的类型只占用一个槽位,64位的占用2个,long和double占用两个槽位,其他的占用1个槽位
- jvm会对每一个slot分配一个访问索引,通过这个索引即可成功的访问到局部变量的值,
- 当一个实例方法被调用的时候,它的参数和方法体内部定义的局部变量
将会按照顺序复制到局部变量的每一个slot上
如果需要访问局部变量表中一个64bit的slot,需要使用前一个索引即可
如果当前栈帧是由构造方法或者实例方法去创建的,那么该对象引用的this就会存在于索引0的位置,其余的按照规律向下排序
栈帧中的局部变量表是可以重复利用的,如果一个局部变量超过了当前的作用域,那么在其作用域之后,申明的新的局部变量很有可能会复用过期的slot,从而达到节省资源的目的
3.1.3.8 静态变量与局部变量的对比
- 参数表分配完毕之后,在根据方法体内定义的变量的顺序和作用域分配
- 我们知道类变量表有两次初始化的机会,第一次是在
准备阶段
,执行系统初始化,对变量赋值为0,另一种是在初始化的时候,附上程序员指定的值 - 局部变量不存在于系统初始化的过程,这意味着,一旦定义了局部变量,需要人为初始化,否则不能使用
3.1.3.9 操作数栈
- 栈是可以用数组或者链表来演示的
- 每一个独立的栈帧中,除了包含局部变量表以外,还包含一个
后进先出
的操作数栈,也可以被称之为表达式栈
操作数栈,在方法执行的过程中,根据字节码指令,往栈中写入数据,或者提取数据,既入栈/出栈
,- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后在将他们压入栈
- 比如:执行复制,交换,求和等操作
- 操作数栈
主要用于保存计算机计算过程中的中间结果,同时作为计算过程的计算的临时存储空间
- 操作数栈就是jvm执行的一个工作区,当一个方法开始执行的时候,一个新的栈帧也将会被随之创建出来,
但是这个操作数栈的返回值是空的
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译的时候就确定好了,保存在方法的cod属性中,为max_stack的值
- 栈中任何一个元素都可以是任意的java数据类型
- 1)32位的栈一个栈单位的深度
-
- 64位的栈两个栈的单位深度
- 操作数栈
并非采用访问索引的方式来访问数据的
而是通过标准的入栈——出栈来完成数据的访问操作 如果被调用的方法有返回值的话,其返回值会被压入当前栈帧的操作数栈中
并更新pc寄存器下一条需要执行的字节码指令- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中,需要子啊次验证
- 另外说JAVA虚拟机的
解释引擎就是基于栈的执行引擎
,其中的栈就是指的是操作数栈
3.1.3.10 理解操作数栈(图解)
- 这是一段代码,接下来看一下字节码文件
- ps: //byte,short,char,bolean 都是以int类型类保存的
-
- 看第一条指定是bipush 意思是将值为15的byte类型的数据推到栈中
-
- 第二条指令是将数据出栈,保存在局部变量表1的位置
- 第二条指令是将数据出栈,保存在局部变量表1的位置
-
- 将值为8的数据,在操作栈中入栈
-
- 出栈,存入到局部变量表为2的地方
- 出栈,存入到局部变量表为2的地方
- 将局部变量表位置为1的数据取出,存入到操作数栈中
- 将局部变量表位置为2的数据取出,存入到操作数栈中
- 通过解析字节码指令,将栈中的两个数出栈,然后相加变成新的数,保存在局部变量表3的位置,程序结束
- 对应了上面说的,
在方法执行的过程中,根据字节码指令,往栈中写入数据,或者提取数据,既入栈/出栈,还是临时数据的存放区
3.1.3.11 栈顶缓存技术
- 基于栈式架构的虚拟机所使用的的领地址指令更加紧凑,但完成一项操作的时候必然需要更多的入栈和出栈的操作,同时这也意味着需要更多的指令分配次数,和内存的读/写次数
- 由于操作数是存储在内存中的,因此频繁的执行内存的读/写操作,必然会影响速度,Hots pot的设计者提出了
栈顶缓存技术,将栈顶元素全部缓存在物理cpu的寄存器中,从此降低内存的读写次数,提高执行引擎的执行效率
3.1.3.12 动态连接与常量池的作用
- 动态连接(或者指向运行常量池的引用)
- 每一个栈帧内部包含着一个指向
运行常量池中该栈帧所属方法的引用
,包含这个引用的目的就是为了支持当前代码的方法能实现动态连接
,比如invokeynamic指令 - 在java源文件被编译到字节码文件中,所有的变量和方法引用都能作为符号引用,保存在class文件的常量池中,比如:描述了一个方法调用了其他的方法的时候,就是通过常量池指向方法的符号来表示的,
那么动态连接的作用是将这些符号的引用转为调用方法的直接引用
- 看这些符号就是对应的运行常量池的索引
- 常量池中的符号后面还有符号,就还继续向下寻找
- 这样就获取到了,方法的名字,并且是没有返回值的方法
3.1.3.12 方法的早期绑定和晚期绑定
- 在jvm中,将符号引用转为调用方法的直接引用与方法的绑定机制有关
- 静态连接:
- 当一个字节码文件被装载到jvm内部的时候,如果被调用的
目标方法在编译期间可知,且运行保持不变,那么这种就被称为静态连接
- 动态连接
- 如果
被调用的方法在编译期间无法确定下来
,也就是说,只能在程序运行期间,将调用方法的符号转为直接引用,由于这种引用转换过程具有动态性,也被称为动态连接 - 对应方法的绑定机制为
早期绑定和晚期绑定绑定是一个字段,方法,类在符号中被替换为直接引用的过程,这个过程只能会出现一次
- 早期绑定
- 在编译期间可知
- 晚期绑定
- 在编译期间不可知
package bj.cy.sj;
/**
* @author LXY
* @desc
* @time 2023--01--12--14:27
*/
class Animail{
public void eat(){
System.out.println("动物进食");
}
}
public interface Huntable {
void sleep();
}
class dog extends Animail implements Huntable{
@Override
public void eat() {
System.out.println("狗吃饭");
}
@Override
public void sleep() {
System.out.println("狗睡觉");
}
}
class Cat extends Animail implements Huntable{
@Override
public void eat() {
System.out.println("猫吃饭");
}
@Override
public void sleep() {
System.out.println("猫睡觉");
}
}
class AndimTest{
public void showAndmin(Animail animail){
animail.eat(); //这种就是晚期绑定,因为不知道要传哪个实现类
}
}
- 上述代码,利用了多态,就是晚期绑定,我在编译期间没有确定,我要传哪个实现类,所以是晚期绑定
- 随着高级语言横空出世,
3.1.3.13 四种指令调用区分非虚方法和虚方法
- 非虚方法:如果爱方法编译的时候就确定了调用的版本,这个版本在运行的时候是不可变的,这个就叫做非虚方法(
静态方法,私有方法,final方法,实例构造器,父类构造器都是非虚方法
)
ps: 子类对对象多态性的使用前提:类的继承,和方法的重写 - invokestatic :调用静态方法,接续阶段确定的唯一版本
- invokespecial:调用init方法,私有及父类方法
- invokevirtual:调用所有的虚方法
- invokeinterface:调用接口方法
- 动态指令调用:
- invokedynamic: 动态的解析出需要调用的方法,然后执行
package bj.cy.sj;
/**
* @author LXY
* @desc
* @time 2023--01--12--16:14
*/
public class Fther {
public Fther() {
System.out.println("父类的构造器");
}
public static void showstatice(String str){
System.out.println("父类的showstatice");
}
public final void showfinal(){
System.out.println("父类的showfinal");
}
public void showmetnod(){
System.out.println("父类的普通方法");
}
}
class sun extends Fther{
public sun() {
super();
}
public sun(int age) {
this();
}
public static void sunstatice(){
System.out.println("son statice");
}
private void showtest(){
System.out.println("sun showtest");
}
private void show(){
//子类静态方法
// invokestatic
sunstatice();
//父类静态方法
// invokestatic
super.showstatice("lxy");
//子类私有方法
//9 invokespecial
showtest();
//父类普通方法
//invokespecial
super.showmetnod();
//父类的final方法
//invokevirtua
showfinal();
//父类的普通方法
//invokevirtual
showmetnod();
}
}
3.1.3.14 关于invokedynamic指令
- JVM字节码指令集一直比较稳定,一直到JAVA7才增加了一个invokedynamic指令
这是为了动态语言的支持做了一个改进,
- 但是在java7中并没有提供直接生成invokedynamic的指令,
在java8中lambda表达式的出现,invokedynamic指令的生成在java中才直接展现出来
- Java7中增加的动态语言类型支持的本质是对java虚拟机规范的修改,增加了虚拟机中的方法调用,其最大的受益者是java平台动态语言的编译器
- 动态类型语言和静态类型语言
- 动态类型语言和静态语言的区别的是,是否是编译初期生成的,还是运行时期产生的
3.1.3.15 虚方法表
- 在面向对象过程中,会频繁的使用到动态分配,如果每次动态分配的过程中,都要重新在类的方法,元数据中搜寻合适的目标,可能会影响到执行效率,因此为了
提升性能,JVM采用在类的方法区建立一个虚方法表,使用索引来查找
- 每个类中都有一个虚方法表,表中存放的是各个方法的实际入口,
- 那么虚方发表是在什么时候创建的,
- 虚方发表会在类加载的连接阶段创建并初始化,类的变量啊初始值准备完成之后,JVM会在该类的方发表也会初始化完毕
3.1.3.16 方法返回地址
- 用来存储该方法的pc寄存器的值
- 一个正常的方法结束:正常结束,出现异常结束
- 无论是哪种方式,在方法结束之后,都需要 回到该方法被调用的位置,方法无法正常退出的时候,
调用者的pc寄存器作为方法的返回地址,既调用该方法的指令的下一条指令地址
,而通过异常退出的,返回地址要通过异常表来确定,栈帧中一般不会保存这些信息 - 当一个方法开始执行的时候,只有两种方法可以退出
- 1执行引擎遇到了任意的一个返回指令,会有返回值传递给上层的方法调用者,简称
正常出门
- 字节码中返回指令包含ireturn当返回值是(boolean,byte,char,short和int类型使用),ireturn,freturn,areturn,另外普通的retuen是返回void 的没有返回值的方法,
-
- 在方法执行的时候出现了异常,而且这个方法没有在代码中进行处理,也就是说在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称
异常出门
,方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的数据
- 在方法执行的时候出现了异常,而且这个方法没有在代码中进行处理,也就是说在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称
3.1.4 本地方法接口
- 什么是本地方法:
简单地来说一个Native Method就是一个java代码调用非java代码的接口
,一个Natice Method就是一个这样的java方法,该方法语言的实现右非java语言的实现,比如c,这个特点并非java所特有,很多其他的变成语言都有这一机制,比如在c++中,你可以用extern "c"告知c++编译器去调用一个c的语言- 在定义一个Natice Method的时候,并不需要提供实现类,因为实现类是由JAVA语言在外面实现的,
本地接口的作用是融合不同的编程语言为JAVA所用,他的初中是融合c/c++程序
- 为什么要使用Native Mothod?
- JAVA使用起来非常方便,有些层次感的代码用JAVA使用起来不容易,当我们对效率很在意的时候,问题就来了
- 与Java环境外交互
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因
你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。- 与操作系统交互
- JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成,然而不管怎样,它毕竟不是一个完整的系统,它经常依于一些底层系统的支持。这些底层系统营常是强大的操作系统
。通过信用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分航是用C写的
,还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。 - ** Sun’s Java**
Sun的辉释器是用c实现的,这使得它能像一些警通的C一样与外部交互
,jre大部分是用Java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread的 setPriority(方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用c实现的,并被植入JVM内部,在windows 95的平台上,这个本地方法最终将调用win32 getPriorityl APT。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态键接库(external dynamic link library) 提供,然后被JVM调用。
3.1.5 本地方法栈
JAVA虚拟机栈用于管理JAVA方法的调用,而本地方法栈用于管理本地方法的调用
- 本地方法栈也是线程私有的
- 本地方法是由c语言实现的
- 它的具体做法是Native Method Stack中登记的native方法,在ExecutionEnglish执行加载本地方法库
当某一个线程调用一个本地方法的时候,它就进入到了一个全新且不受限制的世界,它和虚拟机拥有同样的权限
- 本地方法可以通过本地方法接口来
访问虚拟机内部的运行时数据区
- 它甚至可以直接使用本地处理器中的寄存器
- 直接在本地内存的堆中分配任意数量的内存
并不是所有的JVM都支持本地方法,因为JAVA虚拟机中没有明确的规范,本地方法栈的实用语言,具体实现方式,数据结构等
,如果JVM产品不打支持natice方法,也无需要实现本地房发展
- 在Hostpot JVM中,已经将本地方法栈和虚拟机合二为一
3.1.6 堆
- 概述:
一个JVM实例中只有一个运行数据区,一个JAVA线程对应一个进程,一个进程中的多个线程共享堆空间和方法区,每一个线程拥有一套独立的程序计数器,本地方法栈,虚拟机栈
- 一个JVM实例只存在一个堆内存,堆也是JAVA内存管理的核心区域,
- JAVA堆区在JVM启动的时候被创建,其空间大小也被创好了,是JVM管理的最大的一块运行空间
- 堆的大小是可以调节的
- 《JAVA虚拟机规范》中规定,堆可以处于物理上不连续的内存空间中,但在逻辑上被视为连续的
- 所有的线程共享JAVA堆,在这里可以划分线程私有的缓冲区
- 运行一下,打开
- 这是两个进程,就是两个堆空间,证明了每一个进程都有一个堆空间,这里是开了两个进程,一个进程中有一个线程,一个进程是一个堆空间,一个进程中的多个堆空间时共享的
3.1.6.1 堆空间和GC的简单了解
- 《JAVA虚拟机规范中》对JAVA的描述是,所有对象的实例以及数组都应当运行的时候分配到堆中
- 数组和对象,永远不会存在于栈上,因为栈中是保存的引用,这个引用指向了数组和对象在栈中的位置
- 在方法结束后,堆中的对象不会马上被移除,是在垃圾收集器的时候被移除的
- 堆是GC垃圾回收区域的重点执行地方
- 只要是带new的是都在运行的时候存储在堆中了,栈中局部变量表保存的是引用的指向地址
- 栈中是没有垃圾回收的,栈只是负责出栈和入栈
- 栈,出栈了垃圾并不会立马回收,而是等堆满了,发现指向地址也消失了,才会进行垃圾回收
3.1.6.2 堆的内存细分
- 现在垃圾收集器大部分都基于分代收集理论设计:
JAVA7以及之前的对内存上分为三部分:
新生代+养老带+永久区
JAVA8以后堆空间内存逻辑上分为三部分:
新生代+养老带+元空间
- 先不考虑永久代(元空间),这个是可以说方法区的落地实现,等元数据在看
- 在启动一下这个线程,设置的堆大小是10,最大堆打下是10
- 新生区和老年区加一起正好是10,对应的启动的时候设置的参数
- 新生区中是分为Eden区和Survivor这两个
3.1.6.3堆空间大小的设置和查看
JAVA的堆区用于存储JAVA实例,那么堆的大小在JVM运行的时候就已经启动好了大家可以通过-Xms10m -Xmx10m去设置
- -Xms10m 表示其实的堆内存是10m
- -Xmx10m 表示最大堆内存是10m
- 一旦堆内存的大小超过了设置的最大参数,就会抛出outOfMemoryError的异常
- 默认堆空间的大小:
- 初始的内存大小:物理电脑的内存大小/54
- 最大的内存大小:虚拟内存/4
public static void main(String[] args) {
long initlongtime = Runtime.getRuntime().totalMemory() /1024 /1024;
long maxlongtime = Runtime.getRuntime().maxMemory()/1024 /1024;
System.out.println("-Xms"+initlongtime+"m");
System.out.println("-Xms"+maxlongtime+"m");
System.out.println("物理系统大小为"+initlongtime*64.0/1024+"G");
System.out.println("虚拟系统大小为"+initlongtime*4.0/1024+"G");
}
3.1.6.4 oom异常演示
package bj.cy.sj.Thread;
import java.util.ArrayList;
import java.util.List;
/**
* @author LXY
* @desc
* @time 2023--01--14--16:11
*/
public class test3 {
public static void main(String[] args) throws InterruptedException {
List<by> list=new ArrayList<>();
while (true){
Thread.sleep(2000);
list.add(new by(1000*1000));
}
}
}
class by{
byte image[];
public by(int legth) {
this.image = new byte[legth];
}
}
- 通过死循环去需限制的创建对象,对象里面是个byte数组数据都是存在堆空间中的,超过了设置的最大容量直接oom了
- 左边是老年代,有点事eden ,s0,s1
- s0和s1是二选一的状态,每次只会选择一个
3.1.6.5 新生代和老年代
- 存储在JVM中的对象分为两类
- 一类是生命周期较短的瞬时对象
- 另一部分的生命周期比较长,在某种极端的情况下还能与JVM保持一致,
- JAVA堆区,进一步细分的话,可以划分为年轻代和老年代,其中年轻代又可以分为Eden空间Survivorl0空间和Survivorl空间
-参数设置(一般不会轻易设置)
-
配置新生代和老年代的内存占比
-
默认是-xx:NewRatio=2,代表新生代占1,老年代占2,新生代占整个堆的3/1
-
可以修改: -XX:NewRatio=4 ,表示新生代占1,老年代占4,新生代占整个堆的1/5
-
这是默认的占比1/2
-如果生命周期比较长的对象多,就将老年代的占比调大一点 -
也可以通过命令行的方式去查看
-
在Hostpot中,Eden空间和另外两个Survivor的空间占比是8.1.1
-
开发人员可以通过选项“-xx :SurvivorRatio”调整这个空间的比例,比如SurvivorRatio-8
-
但是看到的默认是6.1.1,这是因为有内存自适应,我们把内存自适应关掉还不好使,所以我们要指定一下内存的比例
-
这样就真正是8.1.1了
-Xms600m -Xmx600m -XX:SurvivorRatio=8
几乎所有的对象都是在Eden区被New出来的
- 绝大多数的对象在Eden区就被销毁了
- 可以使用-Xmn设置
3.1.6.6 对象的分配过程
- 为新对象分给空间时一个很复杂的过程,JVM设计者不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完毕之后内存回收之后是否会在内存空间中缠身内存碎片
- 图解过程
- 首先对象时在Edan中创建完成的,当Edan中对象满了之后,由MinorGc进行垃圾回收
- 将垃圾回收完毕之后,将剩余存货的数据放到幸存者区(s0)中
- 然后Eden中数据又满了,垃圾又回收,剩余的(S0)和Eden区中的非0的数据都放到S1中存储
- Eden区的数据满了,回收,将s1中和Eden中的数据放到s0中(每次数据从s1到s0或者从s0到s1中,会有几个计数器,每次移动都会+1)
- 最后按照上面的步骤一直循环,如果有数据执行了15次(默认次数)还没有被回收,那么这条数据将会升华到老年代中
- PS:
Eden区的数据满了会回收,但是s1和s0中的数据满了不会回收,他们等Eden数据满了之后s1和s0是一个被动回收的过程
默认值是15,到了15进老年代,也有可能当进Eden下一步就去了老年代(有些人出生就是罗马),也有数据每到15就进了老年代(表现良好,提前晋升)
s0和s1的作用是复制交换,进行记数的作用
关于垃圾回收频繁的在新生区收菜,很少在养老区收菜,几乎不在永久代(元数据)总收菜
3.1.6.7 对象分配的特殊情况
- 新的对象如果Eden区可以放下就放在这里,如果放不下触发YGC进行垃圾回收
- 如果垃圾回收之后放得下就分配内存,如果放不下就直接超大对象的情况就放到老年代中,老年代如果放不下触发FGC回收老年代的垃圾,如果剩余的空间还是不支持放下这个超大内存,直接OOM
- 在Eden区正常的对象放到s00区或者s1区如果放得下,直接放下,如果放不下直接晋升老年代
3.1.6.8
- 设置的内存是600
-这里左边已经看到Eden区的数据已经满了,此时我们的老年代的数据也已经满了,数据已经不知道在哪里放了直接爆出oom异常 - 右边图看Eden区,数据是此起彼伏的时候,数据在最高点,也就是数据满的时候,记性了一次Ygc垃圾回收,所以数据是跌宕气度的图形
- s1和s2区,是交替的容纳Edean区过来的数据,数据都是相对稳定的,满了就从老年区送
- 老年区的数据一直在增长,每次都堆积了很多对象,知道老年区没有数据了,直接爆出oom
3.1.6.9 常用调优工具
- 安装jprofile和在idea中下载插件,运行的时候可以更直观的看到情况
3.1.6.10 MinorGc,MajorGc与FullGc
- JVM在进行GC的时候,并非每次都对上面三个内存区域一起回收的,大部分的回收都是值得新生代
- 针对与HostPot的实现,它里面的GC按照回收区域又被分未两大类一种是部分收集(Particl Gc),一部分是整堆收集(Full Gc)
- 新生代收集(Minor Gc/Yong GC)只是新生代的垃圾收集
- 老年代手机(Major Gc/Old Gc)只是老年代的垃圾回收
-
目前只有CMS GC会有单独手机老年代的行为
-
注意:很多的时候Major GC和full会混淆使用,需要具体的分辨是来老年代回收,还是整堆回收
- 混合收集:(Nixed Gc)收集整个新生代以及老年代的垃圾,目前只有G1 Gc会有这种行为
- 整堆收集(Full Gc):收集整个JAVA堆中和方法区的垃圾
- 年轻代(Minor Gc)的触发机制
- 当年轻代中Edan区空间不足的时候,就会触发Minor Gc进行回收,
- 因为JAVA中大多数对象都是朝生熄灭的特性,所以Minor gc的回收很频繁,但同时速度也是很快,
- Minor GC会触发STW,暂停其他用户的线程,等其他垃圾回收之后,线程才会运行
Stw:垃圾回收的时候,Stw需要标记哪个是垃圾,进而让GC去回收垃圾
- 老年代(Marjor GC/Full Gc)触发机制
- 只发生在老年代的GC,对象从老年代消失的时候,我们说Marjor GC或者Full Gc发生了
- 出现了Marjor GC,经常会伴随一次的MinorGc (但不是绝对的)
- 当老年代空间不足的时候会尝试触发Minor gc,当空间不足的时候会触发Marjor Gc,
- Marjor gc的速度会比MinorGc的速度慢10倍,STW的时间更长
- 如果Major GC之后空间还不足,就会抛出OOM异常
- Full Gc(触发机制)(后面在细说)
- 触发Full Gc的机制:
- 1.调用System.gc()的时候触发但不是必然执行的
-
- 老年代空间不足
-
- 方法区空间不足
-
- 通过MinorGc之后进入到老年代之后大于老年代可用内存
-
- 在Eden区,想s0或者s1区复制的时候,对象大于to区,把对象转为老年代,但是老年代的空间不足
- ps:
Full gc在开发的时候要尽量避免,这样暂停时间会短一些
3…1.6.11 GC日志分析
- 需要启动的时候打印GC的执行
- 报oom的时候,会进行Full Gc垃圾回收,如果垃圾回收之后空间还是不足,就会oom了
- 上面的YangGC是对新生代进行垃圾回收的
- 堆空间满了,就会触发FullGc进行垃圾回收,如果垃圾回收之后空间还是不足,就会oom了
3.1.6.12 堆空间的分代思想
- 为什么要把JAVA堆分代,不分代还有办法工作吗?
- 不同对象的生命周期不同,大部分的数据都是临时对象,
-
新生代:有Eden,两块大小相同的Survivor(又称from to)构成
-
老年代:存放新生代中经历多次GC仍然存活的对象
- 其实不分代完全可以工作,分代的好处是优化
GC性能
,如果不分代,所有的对象都在一起,就如同把一个学校的人都关到一个教室,GC需要寻找哪些对象没用,这样就会对所有的区域进行扫描,而很多对象都是朝生夕死的,如果分代的话,把新创建的对象都放到某一个地方,当GC的时候,先把这块区域回收,就能腾出了很大的空间
3.1.6.13 内存分配策略
- 针对与不同不同年龄段的对象分配如下
- 优先分配到Eden区
- 大对象直接分配到老年代
-
尽量避免程序中有很多大对象
- 长期存活的对象放到老年代
- 动态对象的年龄判断
-
如果Survivipr空间的一半,年龄大于或者等于该年龄的对象可以直接进入到老年代,无需等到15次(默认15是阈值)
3.1.16.14 TLAB(线程缓冲区)
- 堆区是线程共享数据的,任何线程都可以访问到的
- 由于对象实例在线程中非常的频繁,因此在并发环境下从堆区中划分内存空间时线程不安全的,为了避免多个线程同时操作一个地址,需要使用加锁机制,进而影响分配速度
什么是TLAB?
- 从内存回收而不是垃圾回收的角度,对Eden区进行划分,
JVM对每个线程创建了一个私有的缓冲区
,它包含在Eden空间之中 - 多个线程同时分配内存的时候,使用TLAB可以解决线程不安全的问题,同时还能提高内存的吞吐量,因此我们可以将这种分配称为
快速分配法
- 尽管不是所有的对象实例都能在TLAB中分配内存,
但JVM确实是将TLAB作为内存分配的首选
- 在程序下开发人员可以通过-XX:UseTLAB设置开启的空间
- 默认情况下TLAB的空间很小,
仅占有Eden的1%
,当然可以进行设置TLAB空间的占用大小, - 一旦对象在TLAB区分配失败,JVM就会尝试通过加锁的机制,保证数据一致性,从而在Eden区中分配内存
ps:堆空间并不是完全都是线程私有的,TLAB就是线程私有的缓冲区,为的是保存的数据的安全性
3.1.6.15 堆空间的常用设置
- 最后一个空间分配担保参数:
在发生Minor Gc之前,虚拟机会检查老年代的最大的可用空间是否大于新生代所有对象的总空间
如果大于,则认为Minor Gc是安全的
如果小于:则虚拟机查看-XX:HandlePromotionFailure设置值是否允许担保失败 - 如果是true的情况,会检查
老年代最大可用连续空间是否大于年轻代的平均大小
-
如果大于则尝试一次Minor gc,但是有一定的风险
-
如果小于则进行一个Full GC
- 如果是false的情况,则进行一次full GC
ps: 在jdk7以后XX:HandlePromotionFailure的参数不会再影响虚拟机空间分配的担保策略,变为了只要老年代的连续空间大于新生代空间的总大小,或历次晋升的大小,就会进行Minor Gc,否则进入Full Gc
3.1.6.17 通过逃逸分析看堆空间的内存分配
- 堆是分配对象的唯一地方吗?
- 在《深入理解JAVA虚拟机》中关于堆内存有这样一个描述:随着JIT编译器的发展,与
逃逸分析
逐渐成熟,栈上分配,标量替换等
优化技术会导致一些微妙的变化,所有的对象都分配到堆中,也不是那么绝对了 - 在JAVA虚拟机中,对象时在JAVA虚拟机中分配内存的,这是一个普遍的常识,但是,有一种特殊的情况,
如果一个对象经过逃逸分析之后,没有逃逸出方法区的话,那么他就会被栈上分配,
这样无需在堆内分配内存,也无需进行垃圾回收了,这就是常见的堆外存储技术 - 此时前面提到的,基于opeJDK深度定制的TaoBaoVM,其中创新的GCIH技师实现off-heap,将生命周期比较长的JAVA对象,从heap中移到heap外,并且gc不能管理GCIH内部的对象,从此降低GC的回收频率,提升GC回收效率的问题
- 如何将堆中的数据分配到栈,则需要使用
统一分析手段
- 这是一种可以有效的减少JAVA,程序中同步负载和内存堆中分配压力的跨函数全局数据流的分析算法
- 通过逃逸分析,JAVA Hostpot 编译器就能分析出一个新的对象的引用使用范围,从而决定了是否要将这个对象分配到堆上
- 逃逸分析的基本行为就是分析对象的动态作用域
-
当一个对象在方法去被定义之后,对象只有在方法内部调用,则认为没有发生逃逸
-
当一个对象在方法中被定义之后,它被外部的方法引用,则认为发生了逃逸,例如作为调用参数传递到其他方法中
- 没有发生逃逸分析的对象则可以分配到栈上,随着方法的执行结束,栈空间就会被移除
- 下面的图是发生了逃逸分析,这样的数据是会分配到堆中
- 下面是没有发生逃逸,则可以分配到栈上
- 参数设置:早在jdk7以后,就默认开启了逃逸分析
- 如果是用的早起版本可以通过-XX:+DoEscapeAnalysis 显示开启逃逸分析
- 通过选项-XX: PrintEscapeAnalysis 查看逃逸分析的筛选结果
开发中能使用局部变量就不要再方法外定义
- 因为在局部定义的方法是可以分配到栈上,分配到栈上第一是线程是私有的,是安全的,第二是会随着方法的执行完毕自动结束,避免了垃圾回收提高程序的性能
3.1.6.18 逃逸分析之栈上分配
- 使用逃逸分析,编译器可以对代码做出如下优化:
栈上分配
,将堆分配转换为栈分配,如果一个对象在子程序中被分配,要使该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配同步省略
,如果一个对象被发现只能在一个线程中被省略,对于这个对象的操作是不考虑同步到分离对象或者标量替换
:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分,可以不存在内存中,而是存在于cpu的寄存器中
- 现在是不开启逃逸分析
- 执行时间15 ,可以看到堆中有1000000个对象,
- 花费的时间更少了,总结:能开启逃逸分析,写在方法中的对象没有逃逸成功,可以进行栈上分配,大大提高了性能,还保证了安全
3.1.6.19 同步省略
- 线程同步的代价是相当高的,同步的后果是降低并发和性能
- 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来
判断同步块所使用的锁对象是否只能被一个线程访问,而且没有被发布到其他的线程
,如果没有JIT编译器就会取消代码的同步,这样就能大大的提高并发性和性能,这个取消同步的过程就是同步省略,也叫锁消除
- 因为这个同步代码块所New的object是调用这个方法都会去New一个新的Obj对象,所以这个绿色的代码,写了和没写一样
3.1.6.21 代码优化之标量替换
标量
是指一个无法在分解成更小的数据的类型,JAVA中的原始数据类型就是标量
相反,那些还可以分解的数据是聚合量
,JAVA中的对象就是聚合量,因为它还可以分解成为其他聚合量和标量
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JLT优化,就会把这个对象拆解成若干个,其中包含若干个成员变量来代替,这个过程就是标量替换
3.1.6.22
- 关于逃逸分析的论文在1999年就发布了,但是到jdk7以后才有实现,而且这项技术并不是成熟的
- 其根本原因是
无法确保逃逸分析的性能一定高于他的消耗,虽然经过逃逸分析可以做标量替换,栈上分配,和所消除,到时逃逸分析自身也是需要一系列复杂的分析的,这是一项相对于耗时的操作
- -一个极端的例子,如果逃逸分析之后,发现没有一个对象是不逃逸的,那么这个逃逸分析的过程就浪费了
- 虽然这个技术并不是十分的成熟,但
是编译器的一个重要的手段
- 小结:
- 年轻代是对象生长,诞生,消亡的一个区域,一个对象在这里产生,应用,最后被垃圾回收,结束生命
- 老年代置放的是长生命周期的对象,通过是通过Survivor区域拷贝过来的JAVA对象。当然,也有特殊情况,我们之后普通对象会被分配到TLAB中,如果对象很大,对象会直接分配带Eden其他位置上,如果对象太大不能分配到新生代,那么就会直接分配到老年代
- 只要GC发生在年轻代中,回收年轻对象是MinorGC,当垃圾回收在老年代之中的时候偶,则被称为MaJorGC,或者Full GC,一般的,MinorGC的发生概率要高,年轻代的垃圾回收要频繁与老年代
3.1.7 方法区
- 一个实例化的对象,对象的引用时放在栈中的局部变量表中,New的实例是放在堆中,方法的类型是放在方法区中
- 实例化的对象在堆中会有指针,可以直接指向方法区中对应的数据
3.1.7.1 方法区的基本概述
- 方法区存在哪里?
- 《JAVA虚拟机规范》中提到了,尽管所有的方法区是在逻辑上属于堆的一部分,但是一些简单的实现可能不会选择去进行垃圾收集,或者压缩。但对于 Hostpot来说方法区还有一个名字叫非堆
- 所以将方法区看成是
独立于JAVA堆的内存空间
- 方法区的基本理解:
- 方法区和JAVA的堆一样,是各个线程共享的区域
- 方法区在JVM启动的时候被创建,并且他的实际物理内存和JAVA堆一样都是不可连续的
- 方法区的大小和堆空间一样,可选择固定大小或者扩展
- 方法区的大小决定了系统中保存多少个类,如果系统中定义了太多的类,导致方法区溢出,虚拟机同样会oom
加载大量的JAR包
,tomcat部署的项目多,大量动态的生成反射类都会导致oom- 关闭JVM就会释放这个区域的内存
- 方法区主要存放的是 Class,而堆中主要存放的是实例化的对象
3.1.7.2 Hostpot中方法区的演练
- 在JD7之前,喜欢性的将方法区称之为永久代,JDK8开始,使用元空间替代了永久代
- 本质上,方法区和永久代并不等价,仅是对Hostpot而言的,《JAVA虚拟机规范中》如何实现方法区,不做统一要求,例如:BEA,JRockit/IBM G9 中不存在永久代的概念
- 到了JAVA8之后,完全废弃了永久代的概念,改用了元数据
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间和永久代最大的区别是,
元空间不在虚拟机设置的内存中,而是使用本地内存
- 永久代,元空间,二者并不只是名字变了,数据结构也调整了
3.1.7.2 设置方法区大小
- JDK8设置方法区的大小:
- 元数据大小可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来设置大小
- 默认依赖于平台Win10环境下,默认的大小是21m,XX:MaxMetaspaceSize的值是-1,没有限制
- 在win10下默认是21大概
-
设置元数据的大小和最大的容量 -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
- 设置的是100,计算得出的也是100
3.1.7.3 方法区的内部结构
- 《深入理解JAVA虚拟机》中对方法区可存储的数据如下:
- 它用于存储被加载的类型信息(注解啊,接口,等),常量,静态变量,被编译以后的代码缓存等
- 类型信息:
- 对每个加载的类型,(类class,接口interface,枚举enum,注解annotation),JVM必须在方法区中存储以下类型信息
- 这个类型的完整有效名称(全名=报名,类名)
- 这个类型的直接父类的完整有效名,(对于object或者interface都没有父类)
- 类型的修饰符
- 这个类型接口的一个有序列表
- 域信息:
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明结构
- 域的相关信息包括:域名称,域类型,域修饰符(public private protected statice final 等的某个子类)
- 方法信息:
- JVM必须保存方法的以下信息,同域信息一样也包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法的参数,数量和类型(按顺序)
- 方法的修饰符
- 方法的字节码,操作数栈的数量,局部变量表的大小,
- 异常表(每个异常处理的开始位置,结束位置,代码在程序计数器的偏移位置)
- 编译一下刚才的代码,然后写到txt中
- 打开txt文件
这里保存的类的基本信息,包名,父类的包名,泛型的类型,类的修饰符
- 这个小括号里面就是域信息,
- non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,他们称为类逻辑上的一部分,类变量被类的所有实例共享,不实例化也可以访问
javap -v -p user.class>1234.txt 反编译一哈
- 补充:全局常量statice final
- 被声明为final的变量是不同的,这个在编译的时候就被分配了
3.1.7.4 常量池
- 方法区内部包含了运行时常量池,
- 字节码内部包含了常量池
- 要弄清方法区,需要理解classFile,因为加载类的信息都存在方法区
- 一个JAVA源文件中的类,接口,编译所产生的的字节码文件,而JAVA中的字节码需要数据支持,通常这种数据会很大,以至于不能直接存到字节码文件中,换一种方式,可以存到常量池中,这个字节码包含了指向常量池的引用,在动态连接的时候会用到运行时常量池
- 比如这个代码,很简单,但是里面用了String,System,printSystem,以及Objet结构,下载这个就三行代码就用到了这么多的结构,代码更多的时候用到的就更多,所以需要常量池
- 直接通过字符引用常量池,变为直接引用,这样减少了内存的开销,不用每次都加载很多的东西
3.1.7.5 运行时常量池
-
运行时常量池是方法区的一部分
-
常量池表时class文件的一部分,
用于存放
编译初期各种字面量与符号的引用,这部分被类加载后,存放到方法区中的运行时常量池中
-
运行时常量池在加载类和接口到虚拟机之后,就会创建对应的运行时常量池
-
JVM对每一个已加载的类型或者接口维护一个常量池,池中的数据向索引一样都是通过索引来访问的
-
运行时常量池包含多种不同的常量,包括编译器就已经明确的自变量也包括到运行期间后才能获得方法或者字段的引用,此时不再是常量池中的符号地址了,这里替换为真实的地址
-
方法的调用通过字符地址指向的是常量池,在运行的时候就会直接指向真实的地址了,有了常量池,就可以少加载对象,不用每个都加载很多对象,减少了内存的开销
3.1.7.6 方法区的执行流程
3.1.7.6 JDK 6-7-8的演变细节
- 只有Hostpot才会有永久代,BEA,JRockit,J9来说是不存在永久代的,
- 变化:
JDK6之前: 有永久代,静态变量存放在永久代中
JDK7:用永久代,但是已经逐步替换,字符常量池,静态变量移除,保存在堆中
JDK8:无永久代,类型 信息,字段,方法,变量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆中
- 随着JDK8的到来,再也看不到永久代了,但是并不意味着元数据消失了,这些数据被转移到了一个
与堆不想连的本地区域,这个区域叫做元空间
- 由于类的元数据是分配在本地内存中,元空间的最大可用空间就是系统的可用空间
- 这项改动很有必要
- 1)
为永久代设置空间大小是很难确定的
在某些场景下,如果动态加载类过多,容易产生oom,比如某个实际的web工程中,因为缺点比较多,往往要加载很多类,往往会提升致命的错误。因此元空间和永久代的区别是:元空间是在本地内存中
对永久代调优很难的
3.1.7.7 StrigTable为什么要调整
- 之前是放在永久代中,后来随着jdk7放到了堆空间中,因为永久代的回收效率很低,只有在Full Gc的时候会触发,而且Full Gc是老年代的空间不足,永久代空间不足才会触发,这就导致StringTable的回收效率不高,因此开发中有大量的字符串被创建,回收效率低,要种导致内存不足,放到堆中,可以及时的清理
3.1.7.8 静态变量的存储位置
- 成员变量的变量名是放在堆中
- 静态变量的变量名是放在堆中
- 方法中的变量变量名是放在栈的局部变量表中
- 所有的只要是New的对象都是放在堆中
3.1.7.8 运行数据区的面试题
3.1.8 对象的实例化内部布局与访问定位
3.1.8.1 字节码角度看对象创建过程
- 因为是在main方法中,所以肯定有栈帧,
- new 这个通过常量池找到了object这个类,是否加载,没加载的话加载,加载的话,在堆中给实例化的object去开辟空间
- dup,是一个复制操作,顶栈的为操作句柄,负责调用等等
- invokespcil 调用object的构造器,执行构造器
- 最后将obj压入局部变量表中
3.1.8.2 创建对象的步骤:
-
- 判断对象是都被加载,连接,初始化
- 虚拟机在遇到一条New指令,首先会去检查这个指令的参数在元空间的常量池是否可以定位到一个符号引用,检查这个符号引用的类是否被初始化,如果没有,在双亲委派模式下,使用加载器去加载,如果没有找到类,则会抛出异常,如果找到了生成对应的class文件
-
- 为对象分配内存
-
- 因为堆是共享的,所以需要解决线程安全问题
-
- 属性初始化 赋默认的值
-
- 设置对象的头
-
- 执行构造器,显示赋值
- 对象是在new开始的,在执行完构造器结束的
- 简介点说:对象创建的6个步骤:1.加载类信息。2.分配内存。3.处理并发问题,4.属性初始化问题。5.加载请求头。6. 对象显示赋值,代码块初始化,构造器初始化
3.1.8.3 对象的内存布局
- 哈希值是:栈帧中局部变量表变量,对应的堆空间的id或者指向(引用)
- GC 年龄分代,是s1区和s2区,每次复制交换的计数器
- 类型指针:指向元空间,就是指向对象的所属类型
- 图解:
- 局部变量表是需要指向堆中的对象的实例的,运行时数据区包含的是哈希值,GC年龄分代,等信息
- 类型指针指向方法区中,实例的类型的元信息
- 类中的变量(实例数据)代码中有符合,所以在字符串常量池中拿过来
3.1.8.4 对象的访问定位
- JVM是如何通过栈帧中的对象访问到其他的内部对象实例的
- 换句话说,在局部变量表是如何去找到堆中的实例的呢
访问对象的两种方式:
- 句柄访问:
- 局部变量中的变量,想要连接堆中的对象实例,中间有一个句柄池,句柄池中有到实例对象的指针,通过句柄池去访问
- 但是这种方式如果对象的实例数据发生了改变,句柄池中的数据也会跟着发生改变
- 直接指针
- 对象在堆中分配内存的时候,就已经在局部变量表中存储了直接的地址值,可以直接通过地址值去访问堆中的实体数据
- 堆中的实体数据想访问在方法区中的时候,可以通过实例内部的直接指针,指向方法区(元数据)的元数据
3.1.9 直接内存的使用
- 直接内存不是虚拟机运行数据区的一部分,也不是《JAVA虚拟机规范》中定义的内存区域
- 直接内存是JAVA堆外的,直接向系统申请的内存空间
- 来源于NIO,通过DirectByteBuffer 直接操作本地的内存
- 通常直接内存的访问会由于栈,因为读写性能高
-
以为出于性能考虑,读写频繁的场合会考虑使用直接内存
-
JAVA的NIO库允许使用本地内存,用于数据的缓冲区
- 这样就直接向本地系统申请了1G的运行空间
- 也有可能会发生outOfMemoryErro异常
- 由于直接内存在堆外,因此他的大小不会受限于-Xmx指定的最大内存大小,但是系统的内存是有限的,JAVA堆和直接内存的总和受限于曹组欧系统的最大内存
- 缺点:分配回收成本高
- 不受JVM所管理
- 直接内存可以通过MaxDirectMemorySize设置
- 如果不指定,默认与堆的最大值-Xmx值一样
3.1.10 执行引擎
- 执行引擎是JAVA虚拟机核心的组成部分之一
- “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机上的执行引擎是建立在处理器,缓存,指令集,和操作系统层面的,而
虚拟机的执行引擎则是由软件自行实现的
因为可以不受物理条件制约的定制指令集,与执行引擎的体系结构,能够执行哪些不被硬件直接支持的指令集格式
- JVM的主要任务是
负责装载字节码到其内部
,但字节码并不能直接运行在操作系统上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅是一些能够被JVM所识别的字节码指令,符号表,以及其他的辅助信息 - 那么如果想让一个Java程序动起来,执行引擎的任务是
将字节码的指令解释/编译为对应平台上的本地机器指令才可以
,简单来说JVM中的执行引擎充当了将高级语言翻译为机器码指令的译者
- 执行引擎在执行过程中究竟需要什么样的字节码指令完全依赖于PC寄存器(程序计数器)
- 每当执行完一项指令操作之后,PC寄存器就会更新下一条需要被执行的指令地址
- 当然方法在执行过程中,执行引擎可能会通过存储在局部变量表中的对象引用准确的定位到存储在JAVA堆中的实例信息,以及通过对象头的元数据指针定位到数据的类型
3.1.10.1 JAVA代码的编译和执行过程
- 大部分的程序代码转换为物理机的目标代码或虚拟机能执行的指令集之前都需要经过图上的步骤
- JAVA代码编译时由JAVA的源码编译器来完成的,流程如下
- JAVA的字节码指令是由JVM执行引擎来执行的,流程如下
- 问题:什么是解释器(Interpreter),什么是JIT编译器
- 解释器当JAVA虚拟机启动的时候会根据定义的规范
对字节码采用逐行解释的方式执行
,将每条字节码的指令内容翻译成本地机器的指令 - JIT(Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成本地机器相关的机器语言
ps: 字节码文件的编译执行,是可以通过解释器或者JIT编译器执行的,这样效果是最好的,本地方法区也会缓存JIT编译的缓存
3.1.10.2 机器码-指令-汇编-高级语言理解
- 机器码
- 各种用二进制编码方式的指令,叫做机器码,开始人们就用它编写程序,这就是机器语言,
- 机器语言虽然能够被计算机理解和接受,但是和人和语言差别太大了,不容易被人们理解和记忆,并且编译的时候容易出差错
- 用它写的程序一经输入计算机,CPU直接读取运行,因此和其他语言编程相比,执行速度更快
- 机器指令和Cpu密切相关,所以不同种类的CPU,对应的机器指令也就不同
- 指令
- 由于机器码指令是0和1组成的二进制序列,可读性太差了,于是人们发明了指令,指令就是把机器码中特定和0和1简化为对应的指令
- 由于不同的硬件平台,执行同一个操作,对应的机器码也会不同,所以不同的硬件平台用的是同一种指令,对应的机器码也会不同
- 指令集
- 不同的硬件平台,各自支持的指令是有差别的,因此每个平台所支持的指令,称之为对应平台的指令集
- x86指令集对应的是x86架构平台
- ARM指令集对应的是ARM架构平台
- 汇编语言
- 由于指令和可读性太差,人们又发明了汇编语言
- 汇编语言用
助记符
,代替机器指令的操作码
,用地址符号或标号
代替指令或者操作数的地址
- 在不同的硬件平台只认识指令码,所以用
汇编语言编写的程序还必须翻译成机器码指令
,计算机才能够 识别和执行 - 高级语言
- 为了使计算机编程用户更容易些,后来就出现了各种各样的高级计算机语言,高级语言比机器语言,汇编语言,
更接近人的语言
- 当计算机执行高级语言编写程序的时候,
仍需要将程序解释,编译成为机器码的指令
完成这个过程的程序就是解释程序或者编译程序
3.1.10.3 解释器
JVM设计者的初衷仅仅只是单纯的为了满足JAVA的跨平台性
因此避免采用静态编译的方式直接成成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法
- 解释器真正的所承担的角色是一个运行时的“翻译官”,将字节码文件中的内容,翻译对应本地的平台机器指令
- 当一条字节码指令被解释执行之后,接着再根据PC寄存器中下一条要被执行的指令进行解释操作
在JAVA的发展过程中一共有两套执行器,既现在的字节码解释器
和现在普遍使用的模板解释器
字节码解释器在执行的时候通过纯软件代码
模拟字节码的执行,效率很低下
而模板解释器将每一条字节码和一个模板函数相关练
,模板函数中直接产生这条字节码执行的机器码,从而很大程度上提高了解释器的性能
在HostPot中,解释器主要由,Interoreter和Code模块组成的,
Interperter模块:实现了解释器的核心功能
Code模块:用于管理HostPot vm在运行时候生成的本地机器指令
- 现状
- 由于解释器在设计和实现的过程中非常的简单,因此除了JAVA语言之外,还有很多的高级语言也是与解释器执行的,比如Python,Perl等,但是在今天
基于解释器执行已经沦为了低效的代名词
- 为了解决这个问题,JVM平台支持一种叫做及时编译的技术,及时编译的目的是避免函数的解释执行,而是将
整个函数编译成为机器码,每次函数执行的时候,只执行编译后的机器码就行
这种方式使得执行效率大幅提升
3.1.10.4 JIT编译器
- 代码执行的分类
- 一种是将源文件编译成为字节码文件,然后再运行时通过解释器将字节码指令转为机器码指令
- 二种是编译执行(直接编译为机器码)现代虚拟机为了提高执行的效率,会使用及时编译的技术将方法编译为机器码在执行
- Hostpot 是市面上高性能的虚拟机之一,它采用
解释器和编译器并存的架构
在JAVA虚拟机运行时,解释器和技师编译器相互合作,各自取长补短,尽力去选择适合的方式全程本地代码的时间和直接解释执行的时间 既然虚拟机已经有了JIT编译器了,为什么还需要在使用解释器爱“拖累程序的性能呢”
- 首先明确当程序执行的时候,解释器能够立马去执行,可以再第一时间发挥作用,省去了编译的时间
- 编译器要是想发挥作用需要将代码编译为本地代码,需要耗费一定的时候,到时成功编译为本地代码之后,编译器的执行效率要高
JAVA虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成之后在执行,这样可以省去很多不必要的编译时间,随着时间推移,编译器发挥作用,把越来越多的代码编译为本地代码,获得更多的执行效率,所以他们两个是取长补短的关系
3.1.10.5 热点代码探测JIT
- 概念解释:
- JAVA语言的编译周期其实其实是一段不确定的操作,因为它可能是指一个
前段编译器
把.java文件转为.class文件是前段编译器在执行 - 也可能是指虚拟机的
后端运行编译器
,把字节码转为机器码的过程 - 还可能是指使用
静态提前编译器
,直接把.java文件转为本地机器码的过程 - 热点代码的探索方式:
- 当然是否需要启动JIT编译器将字节码指令变为对应的机器码指令,则需要根据被调动的
执行频率
来决定的,关于哪些需要被编译成本地代码的字节码也被称为热点代码
,JIT编译器在运行时会根据哪些频繁被调用的“热点代码”做出深度优化,将其直接编译为本地机器指令,提升JAVA代码的执行效率 - 热点代码的探测方式
一个被多次调用的代码,或者是一个方法体中内部次数循环较多的循环体被称为热点代码
,这些代码可以通过JIT编译器编译器编译为本地指令,由于这种编译方法在方法的执行过程中也被称为栈上替换- 一个方法究竟需要被调用多少次或者是循环体中有多少次循环才达到这个标准,这需要一个明确的阈值,这里依靠的是
热点代码的探索功能
目前Hostpot虚拟机 VM所采用的的探测方式是基于计数器的探测方式
- 采用基于计数器的热点探测方式,Hostpot会为每一个方法都建立两个不同类型的计数器,分别为
方法计数器和回边计数器
, - 方法调用计数器用于统计方法的调用次数
- 会标技术器则用于统计循环体执行的循环次数
- 方法调用技术器
- 这个计数器用于统计方法被调用的次数,它的more阈值在Client中是1500,在Server是1w次,超过这个值就会触发JIT
- 这个阈值可以通过虚拟机的参数
-XX:CompileThreshold
来认为的设定 - 当一个方法被调用的时候,先检查是否被JIT编译器编译过,如果存在则直接通过JIT编译器来执行,不存在的话,方法计数器+1,然后判断回边技术器和方法计数器之和,是否超过方法调用计数器的阈值,如果超过了阈值则会向编译器提交一个该方法的代码编译请求
- 热度衰减
- 如果不做任何的设置,方法调用技术器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,是
一段时间内,方法被调用的次数
,当超过一定的时间限制
,如果方法的减少次数仍然不足让它提交给即时编译器处理,那这个方法的计数器就会减少一半
这个过程是热度衰减 - 进行热度衰减的过程是虚拟机在进行垃圾收集的时候进行的,可以使用虚拟机参数
-XX:-UseCounterDecay
来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样只要运行时间够长,绝大多数的代码都是可以编译成为本地代码缓存在方法去中 - 另外可以使用
-XX:CounterHalfLifeTime
来设置设置半衰周期时间,单位是秒 - 回边计数器
- 他的作用是统计一个循环的次数,
3.1.10.6 设置程序的执行方式
缺省情况下 Vm是采用解释器和编译器并存的架构,当然开发人员可以根据具体的场景,设置不同的解释器
- Xint:采用解释器
- Xcome: 采用及时编译模式
- Xmixed:采用解释器+编译器
- 默认是混合模式,可以修改
- 代码执行100w循环,混合的和纯编译器的看不出来细微的差别,速度相同
3.1.11 StringTable(字符串常量池)
String的基本特性:
String(字符串)使用的时候用“”表示
String声明为final的,不可被继承
String 实现.Serializable表示字符串是支持序列化的, Comparable标识字符串是可以排序的
String在JDK8的时候用的是char类型的数组,在JDK9改为了byte
- String是通过Char[]来存储数据的,数组的特点:一旦确定长度就是无法修改的
- String具有不可重复性,s1和s2的值一样,在字符串常量池中就指向了一个地址值,所以内存地址是相等的
- 因为String是数组来存储的数据,数组的长度是固定的,无论对字符串做什么操作,添加,修改,替换等操作,都会在字符串常量池中重新生成一个实例值
3.1.11.1 String底层是Hashtable
字符常量池中是不会存储相同的数据的
- String的Strring pool是一个Hashtable,默认值,大小长度是1009,如果放进StringTable的String非常多,就会造成HashTable冲突严重,从而导致链表会很长,而链表长了以后直接会造成的影响是当调用String.intern时会大幅度下降
- 使用 -XX:StringTableSize可设置StringTable的长度
- 在jdk6中StringTable的长度是固定的,就是
1009
长度,所以字符串常量池中字符过多就会导致效率下降StringTable的大小设置没有要求 - 在JDK7中StringTable的长度默认值是
60013
,1009是可设置的最小值
- 调用了方法,因为String的修改是重新生成一个新的字符串,现在调用的还是当前的字符串,所以值不会发生变化
3.1.11.2 String的字符分配
在JDK6以前,字符串常量池以前是放在永久代的
JAVA7中,将字符串常量池
放到了堆中
所有的字符串都是放堆中,和普通对象一样,这样可以让你在进行调优时仅仅调整堆内存的大小就行了
字符串常量池中概念原本使用比较多,但是这个改动使我们都足够的理由让我们重新考虑在JAVA7中使用String.intern()
在java8中,字符串常量池在堆中
大概意思是说在JDK7中,内部字符串不再分配在Java堆的永久代中,而是与应用程序创建的其他对象一起分配在Java堆的主要部分(称为年轻代和老年代)中。
3.1.11.3 String的基本操作
- 下图,相同的内容指向的是同一个内存地址,不可重复
3.1.11.4 字符串拼接操作
- 常量与常量的拼接是在常量池中,原理是编译优化
- 常量池不会存在相同内容的变量
- 只要有一个变量,结果就是在堆中,变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动的将常量池中的没有的字符串对象放到常量池中,有则返回地址值
- 编译初期就已经拼接好了,“a”+“b”+“c”,在编译的时候就是"abc",将其放入常量池
- s2的值是一样的,所以就引用的同一个
只要有一个变量
结果就是new String(),所以地址不一样
3.1.11.5 深入理解字符串拼接操作
- 两个变量所处的位置都是不一样的,肯定就是false
- 同理
3.1.11.6 字符串的拼接和StringBuilder
- 通过效率对比已经看出了StringBuilder比字符串的拼接快了非常多
- 因为每一次的字符串拼接都需要创建很多的对象,所以速度是很慢的
- 在者就是每次创建对象都会占用内存,对象占用的内存比较大,每次垃圾回收也需要时间,
- 优化方面:优化的话调用StringBuilder的构造器有一个参数的构造器,默认是16长度的,如果知道自己拼接的非常多的话,可以适当的扩容
- StringBuilder stringBuilder=new StringBuilder(number);
3.1.11.7 intern的使用
- 这个是native修饰的,之前说过,native修饰的是调用c语言的代码来执行的
- 概念:
- 如果不是双引号使用的String对象,可以使用String的intern()方法,intern方法会在字符池中判断是否存在,如果不存在就将字符放进去,存在就返回已有的地址值
- 通俗来说,是为了确保字符串内存中有一份拷贝,这样可以借阅内存空间,加快字符串的执行速度
3.1.11.8 new String()创建了几个对象
-
new String 创建了两个对象,第一个对象时newstring,第二是是将789放入字符池
-
首先因为是拼接
-
- new StringBuilder
-
- new String()
-
- a,将a放入到字符池
-
- new String ()
-
- b 将b放入常量池
-
- 因为调用了tostring 方法
- 因为调用了tostring 方法
-
所以还有一个new String()
-
- 将a和b变为ab,放入到字符常量池中
-
所以一共是7个对象
-
第一个是false ,第二个是true
-
因为第二个之前常量池已经有了ab,调用intern 方法,返回的就是之前定义的
3.11.1.9 intern()面试题
- 咱们new String的时候,把ab已经放进字符常量池中了,s1的值是堆的地址,s2的值是字符池的地址所以不相等
- 因为中间有拼接操作,会将a和b分别复制到字符常量池中,但是不会将ab放到字符常量池中
- 然后调用inten方法,发现堆中没ab,然后就加载他,加载完毕之后,定义的s2实际上还是上面加载的地址值
- 同理,先是没有加载ab,s2是重新加载了ab
- 最后的总结:
1. new string 是会加载ab的,但是拼接的不会加载
2.s1.intern();的话是把ab加载进s1
3. String intern = s1.intern(); 所以intern 和s1是相等的
4.如果在拼接之前字符池已经有了值,那么在调intern 的时候地址就付赋了 String intern = s1.intern();
- 5
如果在拼接之前没有值,那么在调方法的时候就付给原来变量s1
3.1.11.10 intern的对比
- 通过对比得知,超多的重复字符串用intern的效率是非常高的
- 性能优化还可以减少垃圾回收
3.1.11.11 垃圾回收测试
- 100000,但是存储的是59458 说明发生过垃圾回收
3.1.12 垃圾回收
关于垃圾收集的三个经典问题:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
垃圾收集机制是JAVA的招牌能力,极大的提高了开发的效率
,如今垃圾收集器几乎成为了现代语言的标配,即使经过了长时间的发展,JAVA的垃圾收集机制仍然在不断的演练,不同大小的设备,不同特征的应用场景,对垃圾收集器提出了新的挑战, - 什么是垃圾?
-
垃圾是指在程序运行的时候没有指针指向的对象,这个对象就是需要被回收的垃圾
- 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占用的空间一直保存在应用程序结束,被保留的空间无法被其他对象使用,可能会导致
内存溢出
- 为什么需要GC?
- 对于高级语言来说,如果垃圾不进行回收,那么
内存迟早会被消耗完毕
,因为不断的分配内存空间而不尽兴回收,就好像不断制造垃圾不打扫一样 - 除了释放没有用的对象,垃圾回收也可以清理内存中的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便
JVM将整理出来的内存分配给新的对象
- 随着应用程序所应对的程序越来越庞大,复杂,业务多,
没有GC就不能保证程序的正常进行
,进而造成STW的GC又跟不上实际的需求,才会不断的堆gc进行优化
3.1.12.1 垃圾标记阶段
- 垃圾标记阶段:判断对象是否被回收
- 在堆中几乎放着所有的对象的实例,在GC垃圾回收之前,首先
区分内存中哪些是活着的对象,哪些是死亡的对象
,只有被标记是死亡的对象,GC才会在执行垃圾回收的时候,释放掉其所占用的内存,我们把这个阶段称之为垃圾收集阶段
- 那么在JVM中如何标记一个已经死亡的对象呢?判断对象存货的方式有两种,一种是
引用计数器算法和可达分析算法
3.1.12.2 引用计数器
- 引用计数器比较简单,对每个对象保存一个
引用记数属性,用于记录对象被引用的状况
- 对于一个对象A,只要有对象被引用了,就+1,没有对象引用了就-1,如果对象的值为,那么就标记为垃圾
- 优点:
实现简单,垃圾对象便于辨别,判定效率高,回收没有延迟(随时回收为0的垃圾)
- 缺点:
它需要单独的字段来存储,增加了内存的开销
每次赋值都需要更新计数器,伴随着加法减法的操作,增加了时间的开销
引用计数器有一个严重的问题,就是无法处理循环依赖,这是一条致命的缺点
- 但是JAVA没有用引用计数器
- 最后p没有指向第一个,但是第一个指向第二个,第二个指向第三个,第三个指向第一个这就造成了每一个的计数器都是1,也就是发生了循环依赖,容易造成内存泄露
3.1.12.3 可达性分析算法与 GCRoots
- 相对于引用计数器而言,可达性算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决
引用计数器算法的循环依赖产生的内存泄露的问题
- 所谓“GC Roots”就是指一组活跃的引用
- 可达性分析算法就是以跟对象集合为起始点,按照从上到下的方式
搜索被跟对象即可所连接的目标对象是否可达
- 使用可达性分析算法之后,内存中的存活对象都会被跟对象集合直接或者简介的连接着,搜索的路径为
引用链
- 如果目标对象没有任何的引用链相连,则是不可达的,就意味着该对象已经死亡,是不可达的
- 在可达分析算法中,只有能被跟对象直接或者间接关联的对象才是存货对象
- 在JAVA虚拟机中GC Roots包含一以下几类元素:
- 虚拟机栈中引用的对象(如各个线程被调用的方法使用到的参数,局部变量等)
- 本地方法栈中JNI(本地对象)的引用对象
- 方法区中类静态的引用对象
- 方法区中常量引用的对象(字符串常量池String Table)的引用
- 所有被synchronized持有的对象
- JAVA虚拟机内部的引用(基本数据类型对应的Class对象,一些常驻的对象,如:NullpointerException等),系统类加载器,回调方法,本地代码等等
- 除了这些固定的GC Roots之外,根据用户所选择的垃圾收集器以及当前回收的区域不同,还可以有其他的临时对象的加入,共同构建完成的GC Roots集合
- 如果只是针对JAVA堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机内存的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的所引用,这时候就需要一并将关联的区域对象所引用的,这个时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达分析的准确性
由于Roots采用栈方式存储变量和指针,所以如果一个指针它保存了堆内存中的对象,但是自己又不是放在堆内存中,那么他就是一个Root
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保证一致性的快照中进行,这点不能满足的话分析结果准确性就无法保证
- 这也是导致Gc进行时候必须进行“Stop The Word”的一个重要的原因(需要停掉用户线程)
- 即使是号称(几乎)不停顿的CMS收集器,
枚举根节点的时候也是必须要停顿的
3.1.12.4 对象的finalization机制
- JAVA语言提供了对象中止(finalization)来允许开发人员对
即将销毁的对象做一些逻辑上的自定义处理
- 当垃圾收集器发现没有引用指向一个对象的时候会主动去调用finalization方法
- finalization()允许被重写,
用于对象被回收时候的资源释放
,同行这个方法中进行一些资源的释放和清理的工作,比如关闭文件,套接字和数据库连接等
- 永远不要主动的去调用某个对象的finalization方法,应该交给垃圾回收处理器去调用,原因如下:
- 在finalization()的时对象有可能会被复活
- finalization()方法的执行时没有保障的,它完全有GC去决定,极端情况下如果没有发生GC,finalization()是不会被调动的
- 一个糟糕的finalization()是会严重的影响GC的性能
- 由于finalization()方法的存在,
虚拟机对象坑出现三种状态
- 如果从所有的根节点都无法去访问某个对象,说明这个对象是垃圾,需要被回收,但是事实上,他不是必须要死的,这个时候他们处理缓刑的状态,
一个无法触及的对象,有可能在某一个条件下复活自己
如果是这样,那么对它的回收是不合理的,定义虚拟机对象了能出现的饿三种状态: 可触及的:从根节点开始,可以访问到这个对象
可复活的:对象的引用都被释放,但是在finalization()中又重新复活了
不可触及的:对象的finalization*()被调用了。并没有复活,就会进入到不可触及的状态,是非死不可的,因为finalization方法只能被调用一次
- 判断一个对象是都可以被回收:至少要经历两次标记过程:
- 如果对象obja到GC Roots没有被引用链,则进行一个标记
- 进行筛选,判断对象是否有必要执行finalization方法
-
- 如果obja没有重写finalization()方法,或者已经执行过了一次了,这个对象时不可触及的
-
- 如果obja重写的finalization()方法,且没有调用,那么obja就会插入到F-Queue队列中,由一个虚拟机自动创建的,优先级的finalization线程会触发finalization()方法的执行
-
finalization是对象逃脱死亡的最后机会
稍后就会进行二次标记,如果对象复活了,重新简历的连接,那么就会移除即将回收的集合,如果对象和链再次断开了连接,则不会调用两次finalization(),对象变为不可触及的
3.1.12.5 finalize 的对象复活
- 上图是没有重写finalize方法的,在main方法中赋值为null,然后去垃圾回收,结果显而易见是垃圾
- 上图是重写finalize方法的将obj重新赋值,在main方法中赋值为null,然后去垃圾回收,因为重写了finalize方法,所有会执行一次手动赋值,结果就不是null,第二次又赋值为Null,但是finalize方法只能调用一次,所以是第一次没有成功回收(第一次就是对象的复活),第二次不会调用finalize方法,所以就是垃圾回收了
3.1.12.6 运用工具Mat查看Gc Roots
- 代码先执行,让程序卡在这里
- 通过JDK自带的工具生成dmp文件
- 保存临时文件到桌面
- 让程序继续执行,卡在这里
- 保存这两个文件,一个没有复制为Null进行gc,一个没有赋值为null然后gc
- 打开mat软件
- 将文件加载进来,然后打开gc
- 这个是没有进行null就gc了,可以看到list对象和data对象时可达的,这里是选中main方法中查看的
- 这个是进行null就gc了,可以看到list对象和data对象已经没有,数量是19,没为null之前是21,可以看出来,这两个对象已经是垃圾了,这里是选中main方法中查看的
3.1.12.7 使用JProfiler查看
- 这里可以查看数据的浮动状态,如果有对象一直浮动,无法回收,一直在增,可以直接看到
- 这里可以查看对象的引用
- 点击ok查看数据是在哪里来的
3.1.12.8 使用JProfiler查看异常
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-执行完毕之后会生成文件,打开
- 这就能看到超大对象
3.1.12.9 垃圾清除
- 当成功区分出内存中存活对象和死亡对象之后,Gc接下来的任务是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够可用的内存空间去对新的对象去进行分配
- 目前常见的三种标记清除算法,
标记清除算法(Mark-Sweep)
,复制算法(copying)
,标记压缩算法(Mark-Compact)
3.1.12.10 标记清除算法 Mark-Compact
- 执行过程:
- 当堆中的有效内存被耗尽的时候,就会停止整个程序也就是(Stop the Word),然后进行两项工作,第一项是标记,第二项是清除
标记
:collector 从引用根节点开始遍历,标记所有被引用的对象,一般在清除Header中记录可达对象清除
:collerctor对堆中进行从头到尾进行循环遍历,如果发现某个对象在Header中没有标记为可达对象,那么就会进行回收
- 优点:
- 简单
- 缺点:
- 效率不高
- 在进行GC的时候需要停止用户线程,导致用户体验感很差
- 这种方式清理出来的空闲内存是不可连续的,会产生内存碎片,需要维护一个空闲列表
何为清除:?
- 这里清除并不是真的清除,而是需要把清除的地址保存在空闲的地址列表中,下次有新对象需要加载的时候 ,判断垃圾的位置是否放的下,放得下就放,放不下就oom
3.1.12.11 复制算法(copying)
- 将活着的内存对象分为两块,每次只使用其中的一块,在垃圾回收的时候将正在使用的内存中的存活对象复制到未被使用的内存块中,交换两个内存的角色,最终完成垃圾回收
- 优点: 实现简单,运行高效
- 缺点:非常明显的一个是需要两倍的运行空间,因为数据是需要来回复制的
对于G1这种拆分称为大量的regionGc,复制而不是移动,意味着Gc需要维护reginGc之间的引用关系,不管是内存占用或者时间开销也不小大白话:加入栈中引用的数据,在来回替换,地址肯定也巫妖变换,就需要进行维护
- 特殊:如果系统中的垃圾对象非常少,复制算法需要复制的对象并不会太大
这里指的是,这种算法适合使用与新生代中进行使用,因为很多对象时朝生夕死的,用一次就需要销毁,会有大量的垃圾对象,但是老年代中不行,老年代中的对象存货的时间是很长的,不适合用
3.1.12.11 标记压缩算法(Mark-Compact)
- 复制算法的高效性是建立在存活对象少,垃圾多的前提下,这种情况是通常发生在新生代,但是在老年代,大部分都是存活对象,如果依然坚持使用复制算法,成本比较高
- 标记-清除算法可以应用于老年代,但是执行效率低,会产生内存碎片
- 也是先标记,然后进行一个碎片化的整理,然后再清除,对比于标记清除算法,标记压缩算法进行了碎片的整理
- 优点:消除了标记清除算法碎片化的问题,消除了复制算法中内存减半的代价
- 缺点: 效率比起来复制算法要低,从移动对象来说,如果对象被其他对象所引用需要调整引用的地址,还会产生STW
- 对象的内存分配这个,如果内存规整使用的是指针碰撞,就是复制算法
- 如果内存不规整,就是标记清楚算法,有了碎片化的问题,需要分配空闲列表
3.1.12.12 三种算法的对比
- 从效率上来看算法是当之无愧的老大,但是浪费了太多的内存
- 为了兼顾以上三种指标,标记压缩算法相对于平滑一些,虽然效率不尽人意,但是不需要复制算法的高额内存,也解决了标记清除算法的碎片化的问题
3.1.12.13 分代收集算法:
- 前面所有的算法没有一款算法可以完全的替代其他的算法,他们都是有自己独特的优点和缺点,分代收集算法就此诞生
- 分代收集算法是基于这样的一个事实:不同对象的生命周期是不一样的,因此:
不同生命周期的对象可以采用不同的收集方式,以便特生效率
,一般是把JAVA堆分为新生代和老年代的,这样可以格局各个代的特点来使用不同的垃圾回收算法,以提高垃圾回收的效率 - 在JAVA运行过程中,会产生大量的对象,其中有些对象时和业务挂钩的,比如
Http的session对象,线程,soket连接
这类对象和业务直接挂钩,因此生命周期长,还有对象是运行的时候临时产生的变量,比如String对象
,这些对象可能用一次就回收了,由于其不变的特性,可能会产生很多大量的对象,有的对象甚至只用一次就回收了 目前为止所有的GC都是采用的分代收集的算法来执行垃圾回收的
- 在Hotstop中,基于分区的概念,GC所使用的内存回收算法必须是结合新生代老年代各自持有的特点
- 新生代:
- 特点:区域相对于老年代存活时间少,对象寿命周期短暂,存活效率低,回收频繁
- 这种用上复制算法是最快的,复制算法的效率和当前存活对象的大小有关系,因此很适用于年轻代的回收,而复制算法内存利用率不高的问题,通过jvm的survivor设计得到了缓解
- 老年代:
- 老年代特点:区域大,生命周期强,存活时间高,回收不频繁
- 这种不适用于复制算法,一般用标记清除算法和标记压缩算法
Mark(标记)阶段和开销和存活与数量成正比
sweep阶段的开销与管理区域的大小数量成正比
compact阶段的开销和存活对象的数量成正比
- 以Hostpot虚拟机的CMS为例: CMS是基于Mark-sweep实现的,对于对象的回收效率高,而对于碎片的问题CMS采用的是Mark-compact算法Serial old回收器作为补偿,当内存不够的时候,(碎片导致的Concurrent Mode Failure时)将采用Serial old执行Full Gc来达到对老年代的内存整理的问题
- 分代思想被现有的虚拟机广泛使用几乎所有的垃圾收集器都区分新生代和老年大
3.1.12.14 增量收集算法
- 上述的算法在垃圾回收中都会处于Stop the Word的状态,在Stop the Word的状态下应用程序都会挂起,暂停一切的工作,等待垃圾回收的完成,如果垃圾回收时间过长,应用程序被挂起很久,
将会影响用户体验和系统稳定性
,为了解决这个问题,有了增量手机算法 - **基本思想:**如果一次性将所有的垃圾进行处理,需要造成长时间的停顿,那么可以让垃圾收集线程和应用线程交替执行,每次
垃圾收集只收集一小片的内存空间,接着切换到用户线程,依次反复,直到垃圾回收完毕
, - 总的来说增量算法的基础乃是传统的标记-清除-和赋值算法,增量收集算法通过
对线程之间冲突的妥善管理,允许垃圾收集线程以分阶段的方式完成标记,清除和复制操作
- 缺点: 使用这种方式由于在垃圾回收的时候间接性还执行了其他的代码,所以可以减少停顿的时间,但是因为线程切换和上下文转换的消耗会使得垃圾回收成本增高,
造成吞吐量增加
3.1.12.15 分区算法
- 一般来说在相同条件下堆空间越大,一次GC所需要的时间就越长,为了更好的控制GC产生的停顿时间,将一块大的区域划分为多个小块的区域,每次合理的回收若干个小块,而不是整个堆空间,从而减少一次GC的时间
- 分代算法是将堆分为两个部分,分区算法是将堆分为大大小小若干的区间
- 每一个小区间都是独立的,独立回收这种算法的好处是一次可以回收多个小区间
3.1.13 垃圾回收相关
3.1.13.1 System.gc()
- 在默认情况下,System.gc()或者Runitime.getRuntime.gc()的调用会显示的触发
Full Gc
同时对新生代和老年代进行垃圾回收,尝试释放内存 - 然而System.gc()的调用附带一个免责声明,无法保证对垃圾收集的调用
就是调用system.gc()有的时候会触发垃圾回收,有的时候不会
- JVM实现者可以通过System.gc()调用来决定JVM的G行为,而一般情况下,垃圾回收是自动执行的,
无需手动触发,否则太麻烦了
,在一些特殊情况下,我们在编写性能测试我们可以在测试之前调用System.gc() - System.runFinalization()调用次方法会强制执行Sysyem.gc行为,前提是需要有System.gc
3.1.13.2 手动GC的垃圾对于不可达的垃圾回收
-XX:+PrintGCDetails
- 数据<10mb,数据已经回收了
- 数据没有回收,因为在{}中,默认对象时还存在的
- 数据已经回收,因为int a这个局部变量的出现,顶替了slot1的位置,所以回收掉了
3.1.13.3 内存溢出的问题
- 内存溢出相对于内存泄露更容易理解,但是同样的内存溢出也是引发程序崩溃的罪魁祸首之一
- 由于GC一直在发展,除非应用程序占有的内存增长速度快,造成的垃圾已经跟不上内存消耗的速度,否则不太容易出现OOM的问题
- 大多情况下,GC会根据各年龄段的垃圾进行回收,是在不行了就放大招,来一次独占式的Full GC,这时候会回收大量的内存供程序使用
- JAVA对于outofMemoryError的解释是:
没有空闲内存,并且垃圾收集器无法在提供更多的内存
- 虚拟机内存堆不够的情况
- 第一种是堆内存设置的不够,可以同归-Xms,-Xmx来设置
- 第二种是代码创建了大量的对象,存在被引用,导致无法被回收,
3.1.13.4 内存泄露的问题
- 也被称为存储渗漏,
严格上来说,程序中用不到了,但是又无法被回收,叫做内存泄露
- 如下图,已经有很多对象用不到了,但是还有一根是关联着,就会造成内存泄露问题
- 举例:
- 单例模式:单例的生命周期和对象时一样长的,所以单例程序中,如果对持有外部对象引用的话,这个外部对象时不能被回收的
- 一些连接未关闭:一些提供close的资源没有关闭,数据库连接,网络连接,和io,必须手动close,否则是不能被GC回收的
3.1.13.5 Stop the word
- 简称stw 指的是在GC事件发生的时候,会产生影响程序的停顿,
停顿的时候整个线程被停顿,没有任何响应
有点像卡死的样子称之为stw - 可达性分析算法会导致JAVA线程停止,
-
- 分析工作必须在一个能确保一致性的快照中执行
-
- 一致性指整个执行系统看起来像是卡在某个时间段
-
如果出现分析的时候对象还在不断的变化,则分析的结果的准确性就无法被保证
- STW和哪种GC没有关系,所有的GC都会有这个事件
- 哪怕是GC也不能完全避免这种情况的发生,只能说垃圾收集器在不断地升级,越来越优秀,回收效率高,尽可能缩短了暂停的时间
- 开发中不要使用System.gc()会导致Stop-the-word的发生
package bj.cy.sj.Thread;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author LXY
* @desc
* @time 2023--01--31--22:38
*/
public class StopTheWordDeom {
public static class wordThread1 extends Thread{
List<byte[]>list=new ArrayList<byte[]>();
@Override
public void run() {
while (true){
byte[] bytes = new byte[1024];
list.add(bytes);
// System.out.println(list.size());
if (list.size()>100000000){
list.clear();
// System.out.println(list.size());
System.gc();
}
}
}
}
public static class wordThread2 extends Thread{
@Override
public void run() {
while (true){
System.out.println(new Date().toString());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
wordThread1 wordThread1=new wordThread1();
wordThread1.start();
wordThread2 wordThread2=new wordThread2();
wordThread2.start();
}
}
- 图一是发生了stw的状态,第二个线程的时间也进行了停顿,所以时间是有间隔的,第二个图是没有进行垃圾回收,所以时间是连续的,证明了发生STW的情况是会发生所有
3.1.13.6 程序的并行和并发
- 并行(Concurrent)
- 在操作系统中,是指
一个时间段中
,有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行的 - 并发不是真正意义上的同时执行,只是一个CPU把一个时间段划分为几个时间段(时间区间),然后在这几个时间区间之内来回切换,由于CPU处理速度非常的块,只要时间间隔处理得当既可让用户感觉是多个线程在同时执行
- 并行 (Parallel)
- 当系统有一个以上cpu的时候,当一个cpu执行一个进行的时候,另一个cpu可以执行另一个进程,两个进程互相不抢占cpu的资源,可以同时进行,称之为并行
- 其实决定并行的数量不是cpu的数量,而是cpu的核心数,比如一个cpu,多个核也可以并行
- 二者对比:
- 并发的多个任务是互相抢占资源的
- 并行的多个任务是不互相抢占资源的
- 只有在多个cpu或者一个cpu多核的情况下才会发生并行,否则看似同时发生的事情,其实都是并发执行的
3.1.13.7 安全点和安全区域
- 安全点(Safe Point)
- 程序执行的时候并非在所有地方都能停下来进行GC,只有在特定的位置上才可以停下来GC,这些位置称之为“安全点”
- Safe Point的选择很重要,
如果太少可能会导致GC的时间加长,如果太频繁会导致运行时性能的问题
大部分指令的执行时间都是非常短暂的,通常会根据是否让程序长时间执行的特征
,为标准,比如选择一些执行时间较长的的指令作为安全点,如方法的调用,循环跳转和异常跳转等
- 安全区域(Safe Region)
- safepoint机制保证了程序执行时,在不长时间内就会遇到可进入GC的Safe point 但是程序不执行的时候呢?例如线程处于sleep状态或者Blocket状态,这时候线程无法响应JVM的终端请求,走到安全点去终端挂起,JVM也不太可能等待线程被唤醒,对于这种情况,就需要安全区域(Safe Region)来解决
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的
我们也可以把Safe Regin 看做是扩展的Safepoint
3.1.13.8 JAVA中几种引用的概述
- 我们能希望描述这样一类对象,当内存空间足够的时候,则能保留在内存中,如果内存空间在我们垃圾回收之后还是很紧张,则可以抛弃这些对象
- 强引用(StrongReference)最传统“引用”的定义,是指程序代码之中普遍存在的引用赋值,既类似object obg=new object()的这种引用关系,无论任何引用关系下,只要引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
- 软引用(SoftReference)在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行二次回收,如果这次回收之后还是没有足够的内存,才会抛出内存溢出
- 弱引用(WeakReference)在弱引用关联的对象只能生存到下一次垃圾手机之前,当垃圾收集器工作的时候,无论空间是否足够,都会回收点被弱引用关联的对象
- 虚引用(PhantomReference)一个对象是否有虚引用的存在,完全不会对其生存间隔造成影响,也无法通过一个虚引用来获得对象的实例,为一个对象设置虚引用的唯一的目的就是能在这个对象被收集器收集的时候收到一个系统的通知
3.1.13.9 强引用(Strong Reference)——不回收
- 在JAVA程序中,最常见的就是强引用(普通的99%都是强引用),也就是最常见的普通对象,
也是默认的引用类型
- 在JAVA语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个对象就是强引用
强引用的对象时可触及的,垃圾收集器永远不会回收被引用的对象
- 对于一个普通对象,如果没有其他的引用关系,只要超过了引用的作用域,或者显示的赋值为null,就是可以当做垃圾收集了,当然具体的回收还得看具体的策略
- 因为s1还在引用,所以这种是强引用,不会回收
3.1.13.10 软引用(Soft Reference)——内存不够在回收
- 软引用是用来描述一些还有用,但是又非必要的对象,
只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象回收单位之内进行二次回收,如果这回收还是没有足够的内存,才会抛出内存溢出
- 软引用通常时间内存敏感的缓存,比如告诉缓存就用用到软引用,如果还有空闲内存,就可以暂时保留缓存,当内存不足的时候清理掉,这样保证了使用缓存的时候,不会耗尽内存
package bj.cy.sj.Thread;
import java.lang.ref.SoftReference;
/**
* @author LXY
* @desc
* @time 2023--02--01--22:40
*/
public class Strong {
public static class User{
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public static void main(String[] args) {
User user=new User(1,"张三");
SoftReference<User>softReference=new SoftReference<User>(user);
user=null;
System.out.println("softReference.get() = " + softReference.get());
System.gc();
System.out.println("GC-->softReference.get() = " + softReference.get());
try {
byte[] bytes = new byte[1024 * 1024 * 7];
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("finally-->softReference.get() = " + softReference.get());
}
}
}
- 图解:
- 将强引用变为软引用,软引用时在内存不足的时候进行数据的回收,或者是下一个对象无法容纳的时候,回收软引用的数据
3.1.13.11 弱引用(Weak Reference)——发现既回收
- 弱引用也是用来描述哪些非必要的对象,
被弱引用关联的对象只能生存到下一次垃圾回收发生为止
,在系统GC的时候,只要发现了弱引用,不管堆空间是否充足,都会回收
- 软引用和弱引用的区别是,软引用和弱引用都同时维护了一个强引用,强引用为null的时候,软引用时内存不足的时候回收,弱引用时发现的时候就会回收不管内存是否充足
3.1.13.12 虚引用
3.1.14 垃圾回收器的概述和分类
- 垃圾回收期没有在规范中进行规定,可以由不同的厂商,不同的版本的JVM进行实现
- 由于JDK的版本处于告诉迭代的时期,因此JAVA版本已经衍生了众多的GC版本
- 从不同的垃圾分析收集器,可以将GC分为不同的类型
- 按照线程来分类可以分为串行回收期和并行回收期
- 串行回收期是在同一个时间段只有一个CPU用于执行垃圾回收期的操作,此时工作线程被暂停,直到垃圾收集器工作结束
在诸多单cpu处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收期的性能表现可以超过并行回收期和并发回收器
在并发能力比较强的CPU上,并行回收期产生的停顿时间要短于串行回收器
- 和串行回收期相反,并行收集可以运行多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然和串行回收一样,采用独占式,使用了STW
- 按照工作模式划分分为并发垃圾回收期和并行垃圾回收器
- 并发式垃圾回收器与应用线程交替工作,以尽可能减少应用程序停顿的时间
- 独占式垃圾回收期(stw)一旦运行,就停止所以的线程,直到垃圾回收期执行结束之后
- 按碎片处理方式划分可以分为压缩式垃圾回收期和非压缩式垃圾回收器
- 压缩式垃圾收集器会在回收对象之后,对存活对象进行压缩整理,消除回收后的碎片
- 非压缩式的垃圾收集器不进行这步操作
- 按照工作区间划分,可以划分为年轻代垃圾回收期和老年代垃圾回收器
3.1.14.1 评估GC的性能指标
吞吐量:运行用户代码的时间占总运行时间的比例
- 垃圾收集开销:吞吐量的补救,垃圾收集所用的时间与运行时间的比例
暂停时间:执行垃圾收集器时,程序的工作线程被暂停的时间
- 收集频率:相对于应用程序的执行,收集操作发生的频率
内存占用时间JAVA堆内存的大小
- 快速:一个对象从诞生诶回收所经历的时间
- 这三者构成一个不可能的三角,三者总体的表现会随着技术进步而运来越好,一款优秀的收集器通常最多满足其中的两项
- 这三项中暂停时间是很重要的,随着硬件的发展,内存占用多可以容忍,硬件性能的提升也有助于降低收集器运行时候对数据的影响,既提高了吞吐量,而内存的扩大,对延迟反而带来负面的效果
抓住两点:暂停时间和吞吐量
3.1.14.2 吞吐量和暂停时间的对比:
- 吞吐量
- 吞吐量就是CPU就是运行用户代码的时间和CPU总消耗时间的比值,既吞吐量=运行代码的时间/(运行用户代码的时间+垃圾收集的时间)
- 比如虚拟机运行了100分钟,其中垃圾收集是1分钟,那么吞吐量就是99%
- 这种情况下应用程序可以容忍较高的暂停时间,因此高吞吐量的应用程序有更长的时间基准,快速响应是不比考虑的
- 吞吐量优先,意味着在时间单位,STW的时间最短
- 高吞吐量较好因为这会让应用程序的最终客户感觉只有应用线程在做“生产性”工作,直觉上,吞吐量越高,程序运行越快
- 低延迟较好是从最终客户的角度来看的,不管是其他,还是GC来看的导致一个引用被挂起是不好的,这取决于应用程序的类型,
有时候甚至短暂的200ms暂停都要打断终极用户的体验
因此具有低的较大暂停时间是非常重要的特别是对于一个交互式的应用程序
- 不幸的是高吞吐量和低延迟一直是处于一种竞争关系
- 如果选择吞吐量优先,那么
肯定是会降低回收的频率
,但是这样会导致更长的STW来进行垃圾回收 - 相反,如果选择的是低延迟,为了降低STW的时间,
只能频繁的进行垃圾回收
,这样会引起年轻代的内存缩减导致吞吐量下降
- 在设计,使用GC的时候,我们需要明确目标,一个GC算法只能针对其中之一(既高吞吐或者低延迟),或者找到他们二者之间实现一个折中
现在的标准,高吞吐量的情况下,降低延迟时间(折中G1)
3.1.14.3 7款经典垃圾回收器
- 串行垃圾回收器: Serial,Serial old
- 并发垃圾回收器:ParNre,Paraller Scavenge,Paraller old
- 并发垃圾回收器:CMS,G1
3.1.1.14.4 垃圾回收期的组合关系
- 新生代收集器:SerialGC,Parallel Scavenge GC,ParNewGC
- 老年代收集器:Serial Old GC,Paraller Old GC.CMS GC
- 两个连接器中间有线,证明他们可以连接使用
- 其中Serial Old 作为CMS出现的失败的后备预选方案
- (红色虚线)由于维护和兼容性的测试版本,在JDK8的时候将Serial+CMS和ParNew Serial Old放弃了,在JDK9中取消了支持
- (绿色虚线)是启用了这个组合
- (青色虚线)在JDK14中已经移除
- JAVA8默认的垃圾收集器是Parallel Scavenge gc+Parallel Old gc
垃圾收集器这么多,我们针对具体的应用选择最合适的垃圾收集器
3.1.14.4 查看默认的垃圾回收器
-XX:+PrintCommandLineFlags
- JDK8默认的是ParallelGC
- 也可以通过命令行的方式去查看,当前运行的程序
- jinfo -flag 垃圾收集器相关参数 进程ID
3.1.14.5 Serial回收器 串行回收
- Serial收集器是最基本,历史最悠久的收集器JDK1.3之前回收新生代是唯一的选择
- `Serial收集器采用复制算法,串行回收和STW机制进行内存回收
- 除了年轻代之外,Serial Old是提供的老年代收集器,SerialOld收集器同样采用了串行回收和STW机制,只不过内存回收算法使用的是标记-整理算法`
- Serial Old在Server模式下有两个用途 1. 与新生代的Paraller Scavenge配合使用 2.作为老年代CMS收集器的后备垃圾收集方案
- 这个收集器是一个单线程的收集器,但它单线程的意义并不是说他只会
使用一个CPU或者一条收集线程去完成垃圾收集工作
,更重要的是他它进行垃圾收集的时候,需要停掉其他线程,直到回收结束(STW) - 优势:
高效简单
(与其他收集器单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程的开销,专心做垃圾收集自然获得最高的单线程收集效率 - 在Hostpot虚拟机中,使用-XX:+UseSerialGC参数解压指定年轻代和老年代都使用串行收集器
- Serial回收期限定单核CPU环境下使用,因为他运行需要用户线程去等待回收完毕为止
3.1.14.6 ParNew回收器 并行回收
- 如果说Serial GC是年轻代中单线程垃圾收集器,那么ParNew收集器是Serial收集器的多线程版本
- Par是Parallel的缩写,New:只能处理新生代
- PaeNew 收集器除了采用并行回收的方式执行内存回收之外,两款垃圾回收期是没有任何的区别的,ParNew收集器在年轻代也是采用的
复制算法和STW机制
- 对于新生代,回收次数频繁,使用并行的方式高效
- 对于老年代回收次数少,使用串行的方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源)
- 由于ParNew是并行回收的,那么是否可以断定ParNre收集器的回收效率在任何场景下都会比Serial收集器更高效?
- ParNew收集器运行在多CPU的环境下,由于可以充分的利用多CPU,多核心数等优势,可以更快速的完成垃圾收集,提高程序的吞吐量
- 但是在
单个CPU的环境下,ParNew收集器不比Serial收集器更高效
虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁的作任务切换,因此可以有效避免交互过程中产生的额外开销 - 在程序汇总,开发人员可以通过选项:-XX:+UseParNreGC 手动指定使用ParNre收集器进行回收任务,它表示年轻代使用的收集器,不影响老年嗲
- -XXParallelGCThreads 限制线程的数量,默认开启和CPU数据相同的线程
3.1.14.7 Parallel回收器 吞吐量优先
- Hostpot的年轻代除了拥有ParNew书及其是基于回收的以外,Parallel Scavenge收集器同样也是采用了
复制算法,并行回收和STW
的机制 - 那么Paraller收集器的出现是否多此一举?
- 和ParNew收集器不同,Parallel收集器的目标则是达到一个
可控的吞吐量
,也是被称为吞吐量优先的处理器 - 自使用调节策略也是Parallel Scavenge与ParNew的一个重要的区别
- 高吞吐量可以高效的利用CPU时间,尽快完成程序的交互任务,主要是用于
适合在后台运算而不需要太多的交互任务
,因此,常见在服务器环境中使用,例如:那些执行批量处理,订单处理,工资支付,科学计算的应用层序
- Parallel收集器在JDK1.6的时候提供了用于老年代的Parallel Old收集器
- Parallel Old收集器采用了
标记-压缩算法但同样也是基于并行回收的STW的机制
- 在程序吞吐量优先的场景下,Parallel收集器和Pallel Old收集器的组合,在Server模式下的内存回收性能很不错
- 在JAVA8中是默认的垃圾收集器
3.1.14.7 Parallel回收器的参数设置
- -XX:+UseParallelGC 手动指定年轻代使用Parallel并行垃圾收集器执行
- -XX:+UseParallelOldGC 手动指定老年代只用垃圾收集器(只要执行一个上述也会激活,是互相激活的)
- -XX:+ParallelGCThreads 设置年轻代并行垃圾收集器的线程数,一般的最好和CPU相等,以避免多个线程数影响垃圾收集性能
- -XX:MaxGCPauseMillis 设置垃圾收集器的最大停顿时间(既STW的时间单位是ms)
-
为了尽可能的把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整JAVA堆大小或者其他的一些参数,该参数要慎重设置
- -XX:GCTimeRatio 垃圾收集时间占总时间的比例来
- -XX:+UseAdaptiveSizePolicy 设置parallel Scavenge收集器的自适应调节而略(自动开启的)在这种模式下年轻代的大小,Eden和Survivor的比例,晋升老年代的年轻等参数会进行调整,达到一个平衡点
3.1.14.8 CMS回收器 低延迟
- 在JDK1.5时期HostPot 推出了CMS收集器
这款收集器是真正意义的并发收集器,它是第一次实现了让垃圾回收和用户线程进行同时的工作
- CMS垃圾说机器的关注点是尽可能减少STW的时间,采用的是标记清楚算法,也会发生STW
- 第一个阶段: 初始标记
- 在这个阶段中程序中会出现STW的暂停,这个阶段的只要任务是
标记处GC Root能直接关联的对象
,由于直接关联的对象少,所以执行快 - 第二个阶段:并发标记
- 从GC Root中标记的对象,按照根节点遍历整个树图像的过程,这个过程耗时长,不会产生STW,是并发执行的
- 第三个阶段:重新标记:
- 由于在并发阶段是用户线程和垃圾回收线程交叉执行的,因此为了修正并发标记期间,因用户程序继续运作导致的标记变动就行修正,这个阶段的停顿时间比初始的标记时间短一点,并且是STW的
- 第四个阶段:并发清除
- 此阶段
清理删除标记阶段已经死亡的对象,释放内存空间
和用户线程同时执行
3.1.14.9 CMS回收器的优缺点
- 优点: 低延迟,并发手机
- 弊端:
- 会产生内存碎皮,并发清除之后,用户线程的空间不足,无法产生分配超大内存对象,不得不提前触发Full GC
- CMS收集器对CPU的资源非常的敏感,在并发阶段,它虽然不会差生STW,但是因为占用了一部分线程会导致执行变慢,总体的吞吐量降低
- CMS收集器无法处理浮动的垃圾,在并发标记阶段由于程序的工作线程和垃圾线程是同时运行或者交叉运行的,
那么在并发标记线程中如果产生新的对象,CMS不会对这些对象进行标记,导致垃圾不能被即使回收
3.1.14.10 CMS垃圾回收器的参数设置
- -XX:+UseConcMarkSweepGC 手动指定CMS收集器执行内存回收任务(即开启该参数之后,会自动将-XX:UseParNewGC打开,即ParNew(Yang区)+CMS(Old区)+Serial Old区的组合)
- -XX:CMSLnitiatingOccupanyFracttion设置堆内存使用率的阈值,一旦达到该阈值,便开始回收
- (JDK5以前默认值是68,既当老年代空间使用率达到68%时,会执行CMS回收,JDK6以上默认是92%)
- -XX:+UseCMSCompactAtFullCollection 用于指定在执行完毕Full GC之后对内存空间进行压缩整理,依次避免内存碎片的问题,不过由于内存压缩过程无法并发执行,所带来的问题就是停顿时间变长了
- -XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full之后,对内存空间进行压缩整理
- -XX:+ParallelCMSThreads 设置CMS的线程数量(默认是(CPU总数+3)/4)
3.1.14.11 CMS收集器使用小结
- Serial Parall CMS这三个GC有什么不同呢
- 如果想最小化的使用内存和开销:选择Serial gc
- 如果想使用最大化的提高吞吐量:选择Parallel gc
- 如果想最小化的终端停止时间,请选择CMS
3.1.14.12 认识G1垃圾回收器
- 为什么名字是G1呢
-
- 因为G1是一个并行回收期,它将堆内存分割为很多不想关的区域(物理上是不连续的)。使用不同的Region来表示Eden,s0,s1,老年代等
- G1 GC有计划避免在整个JAVA堆中进行全区域的垃圾回收,G1跟踪个个Regin里面的垃圾堆积价值大小,在后在维护一个优先列表
每次根据允许的收集时间,优先回收价值最大的Region
- 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以起名G1(垃圾优先)
3.1.14.13 G1垃圾收集器的好处和不足
并发与并行
- 并行: G1在垃圾回收期间,可以有多个GC同时工作,有效利用多核计算能力,此时用户线程STW
- 并发: G1拥有和应用程序交互执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会再整个回收阶段完成阻塞的情况
分代收集
- 从分代上来看
G1是属于分代性垃圾收集器
,它会区分年轻代和老年代,年轻代依然有Eden去和Survivor区,从堆的结构上来看,它不要求整个Eden区,年轻代和老年代是连续的,也不再坚持固定大小和固定数量了 - 将堆的空间分为若干个区域,这些区域中包含了逻辑上的年轻代和老年代,它是同时兼顾新生代和老年代的
空间整合
- CMS 标记清除算法,内存碎片,若干次GC之后进行一次碎片整理
- G1将内存划分为一个个的region,内存的回收是region作为基本单位的,Regin之间是复制算法,单整体实际是标记-压缩算法,两种算法都可以避免内存碎片,这些特征有利于程序长时间运行,分配大对象时不会因为无法找到内存空间提前触发GC
可预测的停顿时间
- 这是G1相对于CMS的另一大优势。G1除了追求低停顿之外,还能建立可预测的停顿时间模型,折让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过nms
- 由于分区的原因,G1可以只选取部分区域进行乃村回收,这样缩小了回收的范围,因此对于全局停顿情况的发生可以得到较好的控制
- G1跟踪个个Region里面的垃圾堆积价值大小,在后台维护了一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,保证了G1收集器在有限的时间可以获得可能高的收集效率
缺点:
- 相较于CMS,G1来不具备全方位的压倒性优势,比如在用户程序运行的过程中,G1无论是为了垃圾手机产生的内存占比,还是程序运行时的额外执行负载,都要比CMS高
- 从经验上来说在小内存应用应用上CMS的表现大概率会优于G1,而G1在大内存应用商店则发挥其优势,平衡点在6-8GB之间
3.1.14.14 分区(Region)
-使用G1收集器的时候,它将整个JAVA堆划分为2048个大小相同的独立Region,每个Region块大小根据堆空间的大小定的,整体被控制在1MB到32MB之间且为2的N次幂,既1MB,2MB,4MB,8MB,32MB,可以通过-XX:G1HeapRegionSize设定,所有的Region大小相同,且在JVM生命周期不会发生改变
虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region的集合,通过Region的动态分配方式实现了逻辑上的连续
- 设置H的原因,对于堆中的大对象,默认直接会分配到老年代,如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响,为了解决这个问题G1专门划分了一个Humongous区,用来存储大对象,如果一个H区存储不下大对象,那么G1会寻找连续的H区来存储
3.1.14.15 G1垃圾回收期的回收过程
- G1 GC垃圾回收期主要包含三个环节:
- 年轻代GC (Young GC)
- 老年代并发标记过程 (Concurrent Marking)
- 混合回收(Mixed GC)
- 应用程序分配内存
当年轻代的Eden区用尽开始年轻代回收过程
,G1的年轻代手机阶段是一个并行的独占式收集器
,在年轻代收集的时候,GC GC会暂停所有线程,启动多线程进行回收,然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能两个区间都涉及
- 当堆内存使用达到(默认45%的时候)开始老年代并发标记的过程
- 标记完成马上开始混合回收过程,对于一个混合回收期,GC GC从老年区间移动存活对象到空闲区域,这些空闲区域也就成为了老年代的一部分,和年轻代不同,老年代的G1回收器和其他GC不同,
G1的老年代回收期不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以
3.1.14.16 G1垃圾回收的Remenbred Set
- 一个对象被不同区域孤立的问题
- 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region所引用。判断对象是否存活时是否需要扫描整个JAVA堆才能保证准确
- 其他的分代收集器也存在这个问题(G1更突出)
- 回收新生代,不得不扫描老年代,这样的话执行效率会很低
解决办法: - 无论是G1还是其他的分代收集器,JVM都是使用的Remembered Set来避免全局扫描
- 每个Region都有一个Remembered Set
- 每次Reference 类型数据写操作时,都会产生一个Write Barrier暂时中断操作
- 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器,通过检查老年代对象是否引用了新生代的对象)
- 如果不同,通过cardTable把相关引用信息记录到引用指向对象所在的Region对应的Remembered Set中
- 当进行垃圾收集的时候,在GC根节点的枚举范围加入Remembered Set 就可以保证不进行全局扫描,也不会有遗漏
3.1.14.17 G1垃圾回收器过程详细说明
3.1.14.18 7种经典垃圾回收器总结
- 如何去选择垃圾回收器?
-
- 优先调整堆的大小让JVM自适应
-
- 如果内存小于100m使用串行回收器
-
- 如果单核,单机,并且没有停顿时间要求,选择串行
-
- 如果是多cpu,需要提高吞吐量,允许停顿时间超过1s,选择并行,或者让JVM自己选择
-
- 如果是多CPU,追求低停顿时间,需要快速响应(比如延迟不能超过1s)使用并发
- 官方推荐G1
3.1.14.19 GC的日志
3.1.14.19 GC的日志分析
3.1.14.20 GC的日志分析2
- 可用空间时9216 eden区+s0区正好,因为s0,s1总会有一个是空着的
- 老年代 6144k/1024等于6
- Eden区的53%8m 相当于就是4m
- jdk7和8的存放区别,jdk7是空间不够的时候让年轻代的进入old区,jdk8的时候是空间不够了我进入到老年代
2.0 Class文件的结构
2.1.1 字节码文件的跨平台性
-
- JAVA语言:跨平台的语言
-
当JAVA源代码成功编译为字节码之后,如果想在不同的平面上运行,则无需再次编译
-
跨平台似乎块成为了一门语言必选的课程
-
- JAVA虚拟机: 跨语言的平台
-
JAVA虚拟机不和包括JAVA在内的任何语言绑定,它与Class文件这种特定的二进制文件有所关联,无论使用何种语言进行软件开发,只要能将源文件编译为正确的class文件,那么这种语言就可以在JAVA虚拟机上执行,可以收统一而强大的Class文件结构,就是JAVA虚拟机的基石,桥梁
2.1.2 通过字节码指令看细节(1)
- 为什么i3==i4 是false呢
- 调用了valueOf 方法
有一个缓存数组,取值范围是-128 ,+127 之间,这个之间的数字是会存储到缓冲数组中
超过了这个值,当你设定的数字没在这个区间是新new一个Integer
- 所以就是第三个超过了+127 重新new了一个对象,所以值没在缓存数组之间,就是false,地址值不相等
2.1.3 通过字节码指令看细节(2)
- 首先是new一个Stringbuilder,压入栈,调用构造器,newstring,将hello压入字符常量池,调用构造器,在newString,压入栈,将word压入,
2.1.4 通过字节码指令看细节(3)
首先去new Son();的时候,先调用的是父类的构造器,走的父类的方法,所以是10
然后掉子类的构造器,是30
最后调用的是父类的i
2.1.5 Class文件本质和内部数据类型
Class类的本质
- 任何一个Class文件都对应着唯一的一个类或接口的定义信息,但反过来说,Class文件实际上它并不是不一定以磁盘文件的形式出现
Class文件的格式
- Class文件的结构不像XML等描述语言,由于它没有任何的分割符号,所以在其中的数据项,无论是字节顺序还是数量,都被严格限定的,哪个字节代表的什么含义,长度是多少,先后顺序如何都不许改变
2.1.6 Class文件内部结构概述
2.1.7 字节码保存到Excel中
2.1.7.1 Class文件标识:魔术(magic)
- 每个class文件开头4个字节的无符号整数称之为魔术
- 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的合法class文件,既:魔术是class文件的标识符
- 使用魔术而不是扩展名来辨别主要是基于安全方面考虑,因文件扩展名可以所以的改动
2.1.7.2 Class文件标识:版本号
- 紧接着魔术的第四个字节存储的是Class文件的版本号,同样也是4个字符,第5个和第6个字节所代表的的含义是编译器的副版本,第七个第八个字节码代表的是编译器的主版本
- 他们共同构成了class文件的版本号,比如某个class文件的主版本号是m,副版本是n,那么版本号就是m.n版
- 16 进制的转换也就是52,版本为52.0版本
- JAVA的JDK版本是从45开始的,JDK1.1之后美发部一个版本,主版本就+1
不同版本的JAVA编译器编译的class文件对应的版本是不一样的,高版本的JVM可以执行低版本的class文件,低版本的jvm不能执行高版本的字节码文件(向下兼容)
- 在实际应用中,由于开发环境和生产环境的不同,就会告知该问题的发生,因此在开发的时候,需要注意开发的版本和运行的版本是否一致
2.1.7.3 Class文件常量池
- 常量池是class文件中内容最为丰富的区域之一,常量池对于class文件中的字段和方法的解析有着至关重要的作用
- 随着JAVA虚拟机不断的发展,常量池的内容日渐丰富,可以说常量池是整个class文件的基石
- 在版本号之后,跟着的是常量池的数量,以及若干个常量池的选择
- 常量池中常理的数量不是固定的,所以在常量池的入口需要放置一项u类型的无符号引用,代表常量池容量计数器值(constant_pool_count),与JAVA中语言习惯不一样的是,这个容量计数器是从1而不是从0开始的
常量池表中,用于存放编译时期生成的各种字面量和富豪引用,这部分内容在类加载后进入方法区的运行常量池中存放
2.1.7.4 Class文件常量池计数器
constant_pool_count(常量池计数器)
- 由于常量池的数量不固定,时长时短,所以需要放置两个自己来表示常量池容量的数值
- 常量池容量计数值(u2类型)从1开始,表示常量池中有多少个常量
- 实际上只有21个常量,索引的范围是1-12
2.1.7.5 常量池的字面量和符号引用
- 常量池主要存放了两大类常量,字面量和富豪引用
- 它包含了class文件结构及其结构中所引用的所有字符串常量,类或者接口名,字段名和其他变量,常量池中每一项都具有相同的特征,第一个字节作为类型标记,用于确定该选项的格式,这哦字节称为标签字节
- 字面量和符号引用
- 常量池中主要存放两大类常量,字面量和富豪引用
- 全限定名:
- com/j/bf/Demo这个就是类的全限定名,仅仅是将,替换为/,在最后增加一个;表示限定结束
- 简单名称
- 简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子的add()方法的num字段的简单名称是add和num
- 描述符:
描述符的作用是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值,根据描述符的规则,基本数据类型(byte,char,double,float,int,long,short,boolean)以及代表无返回值的void 类型都用一个大写符来表示,二对象类型则用字符L加对象的全限名来表示
- 补充说明
- 虚拟机在加载class文件才会进行动态连接,也就是说class文件中不会保存各个方法和字段的最终内存布局显示信息,incident,这些字段和方法的符号引用不经过转换是无法被直接用的,
当虚拟机运行的时候,需要从常量池中获取对应的符号引用,在用类加载过程中解析阶段,直替换为直接引用
- 符号引用和直接引用的区别
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的自变量,
符号引用和虚拟机实现的直接内存没有关系
引用的目标并不一定已经加载到了内存中 - 直接引用:直接引用可以是直接
指向目标的指针,相对偏移或是一个能简介定位到目标的句柄,直接引用时与虚拟机实现的内存布局相关的
2.1.7.6 Class文件常量池的解析
-
- 首先解析0a,对应的10进制是10
- 首先解析0a,对应的10进制是10
- 最后是得出占5个字节
-
- 09 占5个
- 09 占5个
-
- 07占3个
- 07占3个
- 01 是非常特殊的- 因为是字符串,取第三个字节的长度,在从后数取出的字节长度(10进制的)
- 最后一共是有22-1个
- 只有001特殊取3个字节然后从第三个字节从后数(第三个字节(10机制)的大小)
2.1.7.7 访问标识
- 在常量池后,紧接着的是访问标识,该标记使用两个字节表示,用于识别类或者接口层次的访问信息,包括这个class是类还是接口,是否定义public类型,是否定义为abstract类型,如果是类的话,是否被声明final等
- 类的访问权限通常是ACC-开头的常量
- 每一种类型的标识都是通过设置访问索引的32位的特定位来实现的,比如如果是public 或者是final的类,则该标记为ACC_PUBLIC|ACC_FINAL
补充说明:
- 带有ACC_INTERFACE标识的class文件是接口而不是类,反之则表示是类而不是接口
-
如果一个带有class文件设置了ACC_INTERFACE标志,同时也得设置ACC_ABSTRACT标志,同时它不能设置ACC_FINAL,ACC_SUPER或者ACC_ENUM标志
2.1.8 JAVA -g的操作说明
- 直接使用javac命令就不会生成对应的局部变量表,使用javac -g是会生成的
- javac -g 解析是会生成局部变量表的,JAVA的开发工具运行代码时候自动解析的时候就是带局部变量表的
2.1.9 javap的指令
- 代码,翻译为源文件
2.1.10 字节码
- JAVA字节码对于虚拟机就好像汇编语言对于计算机,属于基本执行指令
- JAVA虚拟机的指令由
一个字节长度
的,代表着某种特定操作含义的数字(称为操作码Opcode),以及跟随其后,的零至多个代表此操作的所需参数(称为操作数Operands)而构成,由于JAVA虚拟机采用面向操作栈,而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码
2.1.10.1 字节码与数据类型
- 在JAVA虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据基本信息,例如:iload指令用于从局部变量表中加载int类型的数据到操作栈中,而fload指令则是加载float类型的数据
- i 代表int类型的数据
- l–>long
- s–>short
- b–>byte
- c–>char
- f–>float
- d–>double
- 也有一些指令的助记符中没有
明确的指明操作类型的字母
,如arraylength指令,它没有代表数据类型的特殊字符串,但操作数永远只能是一个数组类型的对象 - 还要另一些指令,如无条件跳转指令goto 则是与数据类型无关的
2.1.10.2 加载与存储指令
- 作用: 加载和存储指令用于将数据从栈帧的局部变量表和操作数栈中来回传递
- 常用指令
【局部变量压栈指令】将一个常量加载到操作数栈:xload,xload< n>其中x为i,l,f,d,a,n为0-3
【常用入栈指令】将一个常量加载到操作数栈中,bipush,sipush,lds,ldc_w,ldc_2w,aconst_null,icounst_m1,icount_< i>,fcounst_< f>,dcounst< d>
【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstare,xstare_< n>(其中x为i,l,f,d,a,n 为0到3);xastore(其中x为i,f,d,a,b,c,s)
扩种局部变量表的访问索引指令:wide
- 上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_< n>),这些指令助记符,实际上代表了一组指令(例如iload_< n >代表了iload_0,iload_1,iload_2,iload_3这几个指令,)这几组指令都是带有某个带有一个操作数的通用指令,
对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中
- 比如iload_0就是将局部变量表中索引为0的位置上的数据压入操作数栈中
2.1.10.3 操作数栈和局部变量表
- 局部变量表中long和double是占两个空间,程序计数器就会出现1后面是3的情况
- 代码举例 左边是局部变量表: 因为l代表的是long所以占用两个槽位,{}i和s的作用域,超出就失效了所以是两个变量复用的一个槽位
2.1.10.4 局部变量表的压栈指令
局部变量表压栈指令将给定的局部变量表的数据压入操作数栈中
这类指令大体可以分为
xload_< n >(x为i,f,d,a,n为0-3)
2.1.10.5 常用入栈指令
常用入栈指令的功能是将常量压入到操作数栈中,根据数据类型和入栈的内容不同,又可以分为conts系列,push系列,和ldc指令
根据const系列
对于特定的常量入栈,入栈的常量隐含在指令本身里,指令有iconts_< i >(i从1-5),lconst_< l> (l从0-1),fconst_< f> (f从0-2),dconts_< d>(d从0-1)aconst_null
- 比如: iconst_m1是将-1压入到操作数栈
- iconst_x(x为0-5)将x压入栈
- lconst_0,lconst_1 分别将长整型0和1压入栈
- fconst_0,fconst_1,fconst_2 分别将浮点数0,1,2压入栈
- dconst_0和dconst_1分别将double型0和1 压入栈
- aconst_null 是将null压入操作数栈中
- 从指令的命名不难展出规律,指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点数,习惯上将a表示对象的引用,如果指令隐含操作的参数,会以下划线的形式指出
指令push系列
主要包含bipush和sipush,它们的区别在于接受的数据类型不同,bipush接受8位整形作为参数,sipush接受16位整数,它们都将压入栈中指令ldc系列
如果以上指令都不能满足需求,那么可以使用万能的ldc指令,它可以接受一个8位的参数,该参数指向常量池中的int,float,string的索引,将指定的内容压入栈- 类似的还有ldc_w,它接受两个位参数,能支持的索引范围大于ldc
- 如果要压入的元素是long或者double类型的,则使用ldc2_w指令,使用的方式都是类似的
2.1.10.6 出栈装入局部变量表指令
- 首先iload_1 是将局部变量表的数据压入栈中
- iconst_1 是将1这个value压入到栈中
- 出栈装入局部变量表指令,用于将操作数栈中栈顶元素弹出后,装入到局部变量表中,用于给局部变量表赋值这类指令主要以store的形式存在,比如:xstore,xstore_n n为0-3
- iconst_2 是将值为2的数据压入栈中
- iload_1 是取出局部变量表索引为1的数据压入到栈中,也就是k
- iadd 将上述两个变量出栈相加
- istore 4 是存入到局部变量表索引为4的位置
- ldc2_w #13 <12> 是将常量池中12的value压入栈中
- lstore 5 存入到局部变量表索引5的位置
- ldc #15 是将常量池中lxy的value压入栈中
- astore 7 存入到局部变量表索引7的位置
- 然后一次类推,知道return结束
2.1.10.7 算数指令
- 大体上算数指令可以分为两种,对整形数据进行运算的指令与对
浮点类型数据
进行运算的命令
- 所有的算数指令
- 从常量池中加载10
- 放入局部变量表索引1的位置
- 取出索引1的位置
- 取反
- 存入索引为2的位置
- 取出索引为2的位置
- 取反
- 存入到索引为1的位置
- 加载100到栈中
- 存入局部变量表2的位置
- 取出2索引的值
- 加载10
- 相加
- 存入索引为2的位置
- 这种方式相对于更加简洁
- 对索引2的数据做一个自增10的操作
2.1.10.8 i++和++i
- 单从字节码的角度来看是完全一样的没有区别
- 首先是加载10到栈中
- 存入局部变量表索引为1的位置
- 然后加载索引为1的位置放入栈中
- 对索引为1的位置进行自增1的操作
- 对刚才加载到栈中的数据进行保存到局部变量表索引为2的位置
- 所以得到的结果i++是11,但是a=i++; a还是10 ,因为a保存的是栈中的数据.i++是取出数据在进行自增操作
- 首先是加载10到栈中
- 存入局部变量表索引为1的位置
- 对索引为1的位置进行自增1的操作
- 然后加载索引为1的位置放入栈中
- 对刚才加载到栈中的数据进行保存到局部变量表索引为2的位置
- 所以得到的是++i是11,a=++i,a是11,i++和++i的区别就在于是,i++是先取出在自增,++i是先自增在取出
首先是加载10到栈中 - 存入局部变量表索引为1的位置
- 然后加载索引为1的位置放入栈中
- 对索引为1的位置进行自增1的操作
- 对刚才加载到栈中的数据进行保存到局部变量表索引为2的位置
- 取出的值是保存到索引为1的位置的数据,所以是10,不是11
2.1.10.9 对象的创建与访问指令
JAVA是面向对象的程序设计语言虚拟机平台从字节码层面就对面向对象做了深层次的支持,有一系列的指令专门用于对象操作,可进一步细分为创建指令,字段访问指令,数组操作指令,类型检查指令
- 一:创建指令
- 虽然类实例的数组都是对象但JAVA虚拟机对类实例和数组实例创建了不同的指令
- 创建类实例的指令
New
- 创建数组的指令:newarray(创建数组),anewarray(对象数组),multianewarray(二维数组)
- 首先是new一个object放入栈中
- 在栈中赋值刚才new的一个object,这样栈中是有两个同object的
- 执行init方法会消耗栈中的一个object值
- 然后将栈中唯一的一个ibject放入局部变量表索引为1的位置
ps:这就是dup还需要赋值一个的作用init会消耗一个,局部变量表会消耗一个
- 接下来就是newfile 赋值,执行init,将123.txt压入栈中,调用init,将栈中file对象保存在索引为2的位置
2.1.1010 字段访问指令
- 对象创建之后就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素
- 访问类字段(static字段,)getstatic,putstatic
- 访问类实例字段(非Static字段)getfield,putfield
调用静态的out方法,然后压入546到找中,调用print方法将546方进入,结束
-
- 首先是new Order放入栈中
-
- 栈中复制
-
- 调用构造器方法
-
- 将剩下的栈中对象地址存入到索引为1的位置
-
- 取出索引为1的位置
-
- 占中对18这个value入栈
-
- 进行出栈通过栈中取出索引1的地址,访问到堆中对象的实体中的id属性,将值赋给他
-
- 调用静态的out属性,入栈
-
- 加载索引为1的变量
-
- 通过加载order入栈的时候去访问堆中对象age属性的值,让其进行入栈操作
-
- 出栈,调动print方法,打印
-
- 加载DIRE到栈中
-
- 直接出栈加载到类结构中,因为是static修饰的,不需要在加载到栈中,直接到类中去寻找到name属性去赋值
- 打印
2.1.10.11 数组操作指令
- 数组操作的指令主要有xaload 和xastore作用为
- 把一个数组元素加载到操作栈中baload,caload,iaload,laload,faload,daload,aaload
- 将一个操作数栈的值存储到数组元素的指令xastore 其中x可以为b,c,s,i,l,f,d,a
- 说明:
- 指令xaload表示将数组的元素压栈,指令xaload在执行的时候,要求操作数栈顶的元素是元素的索引i,栈顶第二个元素是索引的引用,该指令会将这两个弹出栈顶,然后将数组【i】压入栈中
- 指令xastore则专门针对数组进行操作,以为iastore为例,它用于给一个int数组给指定的索引值赋值,在iastore执行之前,操作栈中需要准备三个元素,值,索引,引用,操作数栈会弹出这三个元素,并将值进行赋值
- 首先压栈10这个value
- 然后new一个数组
- 出栈,保存在局部变量表1的位置
- 加载局部变量表1的位置
- 加载6
- 加载55
- 执行赋值操作,将引用,索引,值弹出栈,寻找到指定的索引修改
- 压栈Systrm.out
- 加载局部变量表索引1
- 加载0这个value
- 弹出栈,寻找到引用对应堆空间的值,压入栈
- 输出
2.1.10.12 方法的调用
- invokevirtual,invokeinteface,invokespecial,invokestatic,invokedynamic
- invokevirtual 用于调用方法的实例方法根据对象的实际类型进行分派(虚方法分配)支持多态
这也是JAVA语言中最常见的方法分派
- invokeinteface 用于调用
接口方法
,它会在运行的时候搜索由特定对象所实现的这个接口方法,并找出合适的方法调用 - invokespecial用于调用一些需要特殊处理的方法,包括
实例初始化方法(构造器)私有方法和父类方法
这些方法都是静态绑定的,不会在调用的时候进行动态分发 - invokestatic 用于调用Static方法
- invokedynamic
2.1.10.13 方法的返回值
方法调用结束之前需要进行返回,方法返回指令是根据返回值类型区分的
2.1.10.14 操作数栈的指令
dup 是栈上复制
pop是推出栈,代码调用了tostring方法,但是并没有赋值或者调用,就是一个垃圾,就会被JVM弹出栈,如果是long类型占4个字节的垃圾就时pop2
2.1.10.15 比较指令
- 比较指令的作用是比较栈顶两个元素的大小并将比较结果入栈
- 比较指令由dcmpg,dcmpl,fcmpg,fcmpl,lcmp
- 与前面的指令相似,首字母d表示double,f表示float类型,l表示long
- 对于double和float的存在由于有NaN的存在各有两个版本的比较,以float为例,有fcmpg,和fcmpl两个指令,他们的区别在比较的时候如果遇到了NaN处理结果不同
- 由于long类型是没有NaN的,所以无需准备两套指令
- 举例: 社栈顶的元素为V2,栈顶第二位的元素为V1,如果V2=V1则压入0,如果V1>V2则压入1,V2>V1压入-1
2.1.10.16 条件跳转指令
条件跳转指令通常和比较指令一起使用,在条件跳转指令执行之前,一般可以用比较指令进行栈顶元素的准备,然后进行条件跳转
ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,
- 例子1
- 首先压栈0
- 写入局部变量表1的位置
- 取出1
- ifne(当栈顶不等于0的时候跳转)现在0是等于0的所以不跳转,如果是不等于0的时候跳转到程序计数器12的位置
- 压入10
- 保存到索引1的位置完成赋值
- 执行goto(必须执行的)执行程序计数器15也就是结束程序
- 例子2
- 在运行常量池中取出lxy压入进去
- 保存在索引1的位置
- 取出索引1位置的数据
- 不为null的时候跳转带程序计数器9的地方(满足条件直接到程序计数器9的地方)
- 压入0
- 返回0就是false
- 例子3:
- 首先压入9.0
- 保存在索引1的位置
- 压入10.0
- 保存在索引2的位置
- 调用System.out
- 取出索引1的数据
- 取出索引2的数据
- fcmpl(现在栈顶的数是10.0,V2=10,V1=9.0 V2是大于V1的 所以返回的数据是-1)
- ifle(小于0的时候跳转,返回的数据是-1 符合条件,挑战到程序计数器19的位置)
- 压入0
- 执行println,接受到的数据是0 0是false 所以f1是不大于f2的
- 压入50
- 保存索引1的位置
- 取出索引1的位置
- 压入20(栈顶是20)
- dcmpg(结果是1)
- ifge(大于0的时候跳转,成立,执行23)
- 调用System.out
- 压入》20
- 打印
2.1.10.17 比较条件跳转指令
- 比较条件跳转指令类似于比较指令和调教跳转指令的结合体
- 这类指令有if_icmpeq,if_icmpne,if_icmple,if_icmpgt,id_icmple,if_icmpge,if_ifacmpeq和if_acmpne
- 压栈10
- 保存1
- 压栈20
- 保存2
- SSystem.out
- 加载1和加载2
- if_icmple(当前这小于等于后者跳转) 跳转到18,当前者是10,栈顶是20
2.1.10.18 无条件跳转指令
- 目前主要是go to ,两个字节,超出了是goto_w是4个字节
2.1.10.19 抛出异常指令
- athrow指令
- 在JAVA程序中显示抛出异常的指令(throw语句)都是由athrow来实现的
- 除了使用throw语句抛出异常意外,
JVM规范还规定了许多运行时异常,会在其他java虚拟机指令检测到异常状况自动抛出
例如在之前介绍的整数运算时,/0 - 在正常情况下,操作数栈压入的弹窗都是一条一条执行的,唯一的例外情况就是,
在抛出异常时,JAVA虚拟机会清除操作数栈上的所有内容,而后将异常实例压入到调用者操作数栈中
- 异常以及异常处理:异常对象的生成过程(throw(手动/自动)—>指令throw)
- 异常的处理,抛出模型try-catch-finaly -->使用异常表
显示的抛出异常code中会有athrow
code下面是异常表,里面储存处方法抛出的异常
- 不是显示的抛出不会有athrow 但是会有idiv
2.1.10.20 异常处理和异常表
- 在JAVA虚拟机中,处理异常(catch语句)不是由字节码命令来实现的,是采用异常表来说实现的
- 正常的代码执行到goto就直接return了,
- 下面的代码是如果发生了对应的异常,在异常表中寻找到对应的程序计数器,执行相对应的数据
2.1.10.21 同步控制指令
- 方法的同步,是方法的标识是否为同步的,同步的话就阻塞
- 同步代码块的执行,就是有一个计数器,是0的时候线程才可以进行抢占,一个线程抢占完毕资源需要进行计数器+1的操作,操作完毕之后执行一个计数器-1的操作,如果发生了异常还没有进行到释放锁的阶段,把么就会触发异常,将1变成0,在让其他的线程去抢占
2.1.11 类的生命周期概述
- JAVA数据类型分为基本数据类型和引用数据类型,
基本数据类型由虚拟机预先定义引用数据类型则需要进行类的加载
.
- 首先肯定是加载,如果调用的这个类没有去加载就会执行对应的classload去加载
2.1.11.1加载完成的操作以及二进制文件的获取
所谓加载,就是将JAVA类的字节码文件加载到内存中,并在JAVa类中构建成类的模型---类模板对象
,所谓类模板就是就是java类在jvm中的快照,JVM从字节码中解析出来常量池,类字段,类方法,等信息存储到模板中,这样JVM就可以在运行期间通过类模板去获取JAVA类中的任意信息,- 反射的机制基于这一个基础,如果JVM没有将Java类的信息存储起来,则JVM在运行期间也无法反射
- 加载完成的操作
加载阶段,简言之,查找并加载类的二进制数据,生成Class实例
- 在加载类的时候,Java虚拟机必须完成以下三种事情,
- 通过类的全名,获取类的二进制数据流
- 解析类的二进制数据流为方法区的数据结构
- 创建class实例,表示该类型作为方法区各个类的访问入口
2.1.11.2 类模型和class实例的位置
- 类模型的位置:加载的类在JVM中创建相应的类结构,类结构会存储在方法区元空间之间
- class实例的位置:class文件加载到元空间之后,会在对中创建一个class对象,用于封装类位于方法区中的数据结构,该class对象是在加载类的过程中创建的,每一个类都有一个对应的class实例
2.1.11.3 链接阶段之验证阶段
- 当类加载到系统中,加开始链接操作,验证是连接操作的第一步
它的目的是保证加载的字节码是合法的,合理并符合规范的
- 验证的步骤是比较复杂的,实际要验证的项目很多
2.1.11.4 链接阶段之准备阶段
准备阶段,简而言之是给类的静态变量分配内存,并给其赋默认值
静态变量非final修饰的,是在准备阶段进行初始化
静态变量用final修饰的,是在准备阶段进行显示赋值
- 这里不会为实例变量分配内存,类变量会分配到方法区中,而实例对象回随着对象一起分配到JAVA堆中
2.1.11.4 链接阶段之解析阶段
将类,接口,字段,方法的符号引用变为直接引用
2.1.11.5 初始化阶段
- 初始化阶段就是显示的将静态变量去赋值
- 类的初始化是类装载的最后一个阶段,如果前面的步骤没有问题,那么表示类可以顺利的装载到子系统中,此时类才会执行JAVA字节码
初始化状态最重要的是执行类的初始化方法<clinit>
- 该方法仅由JAVA编译器生成并由JVM去调用,程序开发者无法自定义一个同名的方法,无法在JAVA程序中直接调用该方法,它是由静态成员的复制语句和静态代码块的static语句合并产生的
- 说明:在加载一个类之前,虚拟机总是会加载这个类的父类,因此父类的《clinit》方法总是会先执行,父类的优先级是高于子集的
初始化阶段会在clinit中会为静态变量复制,执行静态代码块中的方法
2.1.11.6 类的主动使用和被动使用
主动使用:
- class只有在必须要首次使用时才会被装载,JAVA虚拟机不会无条件的装载Class类型,JAVA虚拟机有一个规定,一个类或者接口在首次使用的时候,必须要进行初始化,这里使用指的是主动加载
-
- 当创建一个类的实例的时候,比如使用new关键字,或者通过反射,克隆,反序列化
-
- 调用该类的静态方法的时候,使用到了字节码的invokesratic指令
-
- 当使用类,接口的静态字段时候,比如使用getstatic指令或者putstatic指令
-
- 当使用java.long.reflect 包中的方法反射类的方法
-
- 当初始化子类的时候,如果发现其父类还没有进行初始化,则需要先触发父类的初始化
-
- 如果一个接口定义了default方法,那么直接实现或者间接实现接口的类的初始化,该接口要在之前初始化
-
- 当虚拟机启动的时候,用户需要指定一个要执行的主类,虚拟机就会先初始化这个类
-
- 首次调用MethodHandle 实例的时候,初始化该MethodHandle指向的方法所在的类
被动使用
- 除了以上的情况属于主动使用,其他的属于被动使用,
被动使用不会引起类的初始化
并不是在代码中出现的类,就一定会被加载或者初始化,如果不符合主动使用的条件,类就不会初始化
- 当访问一个静态变量的时候,只有真正声明这个字段的类才会初始化
- 当通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类引用不会触发此类的初始化
- 引用常量不会触发此类或者接口的初始化,因为常量在连接阶段就已经被显示的赋值了
- 调用classLoad类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化
3.0 性能监控与性能调优
3.0.1 JPS查看正在运行的JAVA进程
-q:仅仅显示LVMID 即本地虚拟机的唯一id,不显示主类的名称等等
-l:输出应用程序主类的全类名或如果进程执行的是jar包,则输出jar包的完整路径
-m:输出虚拟机进程启动传递给主函数main()的参数
-v 列出虚拟机进程启动的jvm参数,比如-Xms20m 等
以上的参数可以综合使用
3.0.2 jstat的使用
-
jstat 用于监视虚拟机各种运行状态信息的命令行工具,它可以显示本地或者虚拟机进程中的类装载,内存,垃圾收集等数据
-
option相关
-
-class:显示ClassLoader的相关信息,类的装载,卸载数量,总空间,类装载所消耗的时间等
-
其他参数
-
interval参数:用于指定输出统计数据的周期,单位为毫秒,查询间隔
-
count :用于指定查询的总数
-
-t参数:可以再输出信息前加上一个Timestamp例,显示程序的运行时间,单位秒
-
-h参数:可以再周期性数据输出的时候,输出多少行后输出一个表头信息
3.0.3 排查oom和内存泄露
-gc:显示与GC相关的堆信息,包括Eden区,两个Servivor区,老年代,永久代等容量,已用空间,GC时间等信息
-gccapacity:显示内容与-gc基本相同,但输出主要关注JAVA堆各个区域使用到了最大,最小空间
-gcutil:显示与-gc一致,但输出主要关注已使用空间占总空间的百分比
-gccause:与-gcutils一样,但是会额外的输出导致最后一次GC发生的原因
-gcnew:显示新生代
-gcnewcapacity:显示内容与-gcnew基本相同,输出主要关注使用到的,最大,最小空间
-gcold:显示老年代GC情况
-gcoldcapacity:显示内容与-gcold基本相同
-gcpermacapacity:显示永久代使用到的最大,最小空间
参数说明
- s0c :幸存者0区大小
- s1c幸存者1区大小
- s0u幸存者0区使用大小
- s1u幸存者1区使用大小
- Ec:Eden区
- Eu:Eden使用大小
- oc老年代大小
- ou老年代使用大小
- Mc方法区大小
- Mu方法区使用大小
- CCSC压缩之前的大小
- CCSU压缩之后的大小
- YGCYGC的使用次数
- YGCT使用时间
- FGC次数
- FGCT时间
- GCT 整总和
- 预防oom异常
我们可以比较JAVA进程的启动事件以及总GC的时间,测量出两次的间隔时间比例,如果比例超过2%说明堆的压力大,如果超过90%说明堆内存随时oom
任意抽取两条数据,可以获得到时间和GC时间 - 判断内存泄露
- 在长时间的JAVA程序中,我们可以通过jstat命令去查看性能数据,并取出ou的数据,做抽样
- 然后每隔一段较长的时间重复上述操作,来获得多组o最小值,如果这些值呈上升趋势说明这个程序老年代的内存在不断上涨,意味着无法回收的对象不断增加,因此可能存在内存泄露
3.0.4 jinfo
-
jinfo -sysprops 19584 可以查看所有系统的设置
-
jinfo -flags 19584 查看曾经赋过值的参数
-
上面的是系统就是非必要的参数,下面的是命令行设置的参数
-
jinfo -flag UseG1GC 19584 查看某个具体参数的值(因为是JAVA8所以不是G1垃圾回收期)
jinfo不仅可以查看,还可以修改,并非所有都可修改,只有被标记为flag时可以被实施修改的
- 打开参数打印
3.0.5 jmap
- jmap的作用一方面是获取dump文件(堆转存储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括JAVA堆各区域的使用情况,堆中对象的统计信息,类加载信息等
-dump
:生成JAVA堆转储快照,dump文件
-heap
:输出整个堆空间的详细信息,包括GC的使用,堆配置信息,以及内存使用信息等
-histo
:输出堆内存对象的统计信息,包括类,实例数量和合作容量
-permstat:以classLoad为统计口径输出永久代的内存状态信息(仅Linuc和solanis平台有效)
-finallizerinfo:显示F-Queue中等待finallizer方法的对象(仅Linuc和solanis平台有效)
-F:当虚拟机进程对-dump选项没有任何响应的时,可只用此选项强制执行dump文件(仅Linuc和solanis平台有效)
-h|-help:jmap工具使用的帮助命令
-j< flag>:传递给jmap启动的jvm
3.0.5.1 导出dump映射文件的方式
- 手动的方式
- jmap -dump:format=b,file=<filename.hprof> < pid>
- jmap -dump:live,fromat=b,file=<filename.hprof> < pid>(区别是这个是导出存活的对象)
- 自动导出的方式
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=<filename.hprof>
- 自动导出的文件,到底是带live的好还是不带,一般来说带的好,因为带live是导出存活的对象,假设项目很大,导出的文件几百mb的话,运维导出文件,在传给你耗时很大,同样的文件,用live的话就不是很大,传输起来很快
- 自动导出,需要运行的时候设置参数,发生oom的时候会进行自动导出文件
3.0.5.2 显示堆内存的相关信息
jmap -heap pid
jmap -histo pid
显示的是对象在堆中的占用情况等等
3.0.6 JDK自带的分析服务
- jhat 和jmap命令是搭配使用的,用于分析jmap生成的heap dump文件
- 启动的时候相当于启动了一个http服务,端口号是7000
- 这个命令在jdk9 jdk10中已经删除了
3.0.7 GUI图形化界面JVM工具介绍
- 图形化综合诊断工具
- JD自带的工具
- jconsole:jdk自带的可视化监视工具,查看java应用进程的运行概述,监控堆信息,永久区(或元空间)使用情况,类加载等情况(jdk/bin/jonsole.exe)
- Visual VM:是一个工具童工了可视化界面,用于查看JAVA虚拟机上运行的详细信息
- 第三方工具:
- MAT:是基于Eclipse的内存分析工具,是一个快速,功能丰富的Java heap分析工具,它可以帮助我们查找内存泄露和减少内存消耗
- JProfiler:商业软件,付费,功能强大
- Artthas:Alibaba开源的Java诊断工具,深受开发者喜爱
- Btrace:JAVA运时的追踪工具,可以再不停机的情况下,跟踪指定的方法调用,构造函数和系统内存等信息
3.0.8 jconsole 的使用
-查看基本信息,堆内存,线程,类和cpu的走势图
- 点击下面的堆可以查看具体的
3.0.8 jvisualvm
- 生成dump文件,可以直接点击
- 打开dump文件
- 也可以进行比较
- 可以查看到两个dump文件相比新增了多少个和减少了多少个
- 死锁的话可以直接见识出来
抽样器可以查看cpu的使用情况和内存的使用情况
3.0.9 MAT
是一款分析主要分析堆内存的文件
- 第一个选项是他会自动的检测疑点,用于查看哪些是可疑的可能泄露的疑点
- 点击完成会生成压缩包文件
- 还可以实时的进行检测
- 这个球球是显示的最大的对象
- 点击之后,右边会显示关于对象的详细信息
- 点击可以显示堆文件的概述信息
- 堆空间当中每一个类实例的个数和占用内存的大小列举出来
- 各个对象都会呈现出来,以及这个对象被哪些其他对象所关联(分析内存泄露常用的点)
- 通过图形的方式去列举出最大的对象
- 可以看到哪些类是被重复加载了
- 泄露的疑点等
- 这里会分析在你的代码中有这么一个这么大占到了97.22%,等等信息
3.0.9.1 MAT的Histogram
- 深堆刻画的是,如果能被回收,能回收的数据大小总和
- 可以进行分组查看
- 这里还刻意写正则,和你输入有关的都可以找到
- 排除弱引用,虚引用等
- 就可以看到为什么不能被回收,原来是被List引用了
- 还可以两个dump文件进行比较,分析在一段时间内,哪个类增长的比较快
3.0.9.2 分析内存泄露
- 假设一个长对象A,一个短对象B,但是A一直引用着B,导致数据不能被回收,这种情况也算是内存泄露
- 分析中已经提示了a40的线程中main方法里,其中的里面object[]占据了总内存的xxx
- 可以更直观的看到这个线程中List对象之中装了很多byte数组
- 还可以查看我引用的是谁,第一个是查看我引用的,第二个是查看我被谁引用的
- 还可以查看我到底是被谁引用了,为什么一直无法回收
- 如果这个对象除了List还有另外一个引用,如果是静态变量,静态变量随着类的加载而加载随着类的销毁而销毁,这样一直引用这个对象,会导致一直无法回收,可以改为是弱引用,内存不足的时候进行回收
3.0.10 内存泄漏的理解和分析
- 内存泄露:
- 可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否在引用,那么对于这种情况下,由于代码的实现不同,就会出现内存泄露的问题(让JVM误以为还在引用,但实际上已经不用了,因为还有一根可达连接所以迟迟无法断开连接,造成内存泄露)
- 严格上来说,只有对象不会再被程序使用到了,但是GC又不能回收他们,才叫内存泄露
- 但是实际情况下,很多一些不太好的编码习惯导致对象的生命周期变得很长
- 通俗来说,申请了内存不释放,比如一共有1024m,分配了512的内存一直不回收,那么可用了内存只有512了,仿佛泄露了一样
泄露的分类
- 经常发生:发生内存泄露的代码会被多次执行,每执行一次,泄露一块内存
- 偶然发生:在某些特定情况下才会发生
- 一次性:发生内存泄露的方法只会执行一次
- 隐式泄露:一直占着内存不释放,直到执行结束,严格的来说这个不算泄露,因为最终释放了,但是如果执行时间特别长,也可能会导致内存的耗尽
3.0.10.1 JAVA中内存泄漏的8种情况
-
静态集合类
-
单例模式
-
内部类持有外部类
-
各种连接,数据库连接,网络连接
-
变量不合理的作用域
-
改变哈希值
-
缓存泄露
-
监听和回调
- 这个出栈的操作就会引起内存的泄露的问题,因为每次出栈是指针上调,但是过期数据并没有数据,过期数据还是在里面,因此修改为,出战的时候给当前数组的值复制为null,这个变量就可以被回收了,然后数组在进行–操作,就不会发生数据的泄露了
3.0.11 JProfiler
- 第一个是分析一个session或者保存一个session
- 第二个是运行当前正在运行的JVM程序进行一个监控(本地和远程)
- 第三个是可以运行服务器端,可以本地也可以远程的
- 第四个:打开一个堆快照,支持离线打开
-当我们选择了本地连接之后
- 会显示两种 数据采集的模式
- 第一个是重构模式
- 第二个是抽样模式
- Instrumentation:这是全新功能模式,在class加载之前,JProfile把相关功能代码写入到需要分析的class的bytecode总,对正在运行的jvm有一定的影响
优点:功能强大,在此设置中,调用堆栈是准确的
缺点:若要分析的class较多,则对应用的性能影响比较大,cpu开销可能会很高(取决于fillter的控制),因此使用此模式一般配合filter一起使用,只对特定的类或者包进行分析 - Sampling:类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法栈中的信息统计出来
优点:对cpu开销比较低,对应用影响小
缺点:一些数据/特性不能提供(例如:方法的调用次数,执行时间)
如果是正在运行的推荐使用第二种
3.0.11.1 遥感监测视图中相关检测数据
- 这个显示的是对象啊或者数据回收的一个表
- 这个是垃圾回收的一个情况
- 这个是显示的是程序运行的时候加载的类的个数
- 蓝色的是cpu相关的,非蓝色是是非cpu相关的
- 这个是显示线程相关的
- 这个是cpu的一个加载,程序执行过程中cpu的使用率的状况
3.0.11.2 内存视图的分析
- 第一个是所有的对象
- 第二个是记录的对象
- 第三个是分配调用的数
- 第四个是分配的热点
- 第五个是类的追踪器
- 点击这里会在你点击之后对数据做一个监控,可以看到数据增长了多少,或者减少了多少
- 也可以进行实时的刷新
- 上面可以分析对象中内存的使用情况
- 频繁创建的JAVA对象,循环,死循环过多
- 存在大对象,读写文件的时候byte[]应该边读边写,–>如果长时间不写出的话,会导致byte[]过大
- 存在内存泄露的问题
开启记录对象开启的话会造成系统的性能降低,主要是判断内存泄露的时候才会开启
- 这样也可以看到内存是否泄露,每次垃圾回收之后,剩余的内存越来越多,非常有可能是内存泄露了,积攒的对象越来越多
3.0.11.3 Heap Walker
- 第一个是当前的类有哪些
- 第二个是分配的情况
- 第三个是大对象
- 第四个是相关的引用
- 第五个是时间
- 第六个是检查第
- 七个是图表
- 对
- 可以选择是谁指向他了,或者是我被谁引用了
- 可以看到是被list引用了
- 可以直接查看是谁指向的
- 还可以看图表
- 堆刚打开这块,可以进行快照,下面是保存生成的堆转存储文件就是dump的
3.0.11.4 CPU视图功能说明
-
第一个是访问数
-
第二个是热点
-
第三个是图
-
第四个是方法的统计
-
第五个是复杂的分析
-
第六个是访问的追踪
-
对CPU的追踪是对程序的性能产生影响的
-
使用这个做演示
-
先打开方法,然后看第一个
- run是95.7%的调用几率
385ms是被调用的时间
-5inv 是被调用的次数
-在往后是方法所在的类的一个描述
3.0.11.5 Thread视图的功能
-第一个是线程的记录情况
第三个是线程的dump文件
- 可以看到线程中存活的还有死的线程一个展示,不同的颜色表明了他的当前的一个状态
3.0.11.6 代码演示
- 这个堆越来越大
- 对数据做对比,发现是byte一直在增大
- 手动GC了之后,还是无法回收
- 进行快照分析
- 可以看到最后是被bin中的静态list引用了,所以一直无法被回收
- 最后是报错了
3.0.12 Arthas
- 上面的工具也优缺点:就是图像化展示,往往远程连接比较麻烦
- 通过linux命令下载
sudo wget https://arthas.gitee.io/arthas-boot.jar
- 项目跑起来之后,直接启动,选择需要监听的序号(对应的类)
- 这样就算启动成功了
3.0.13 JVM的参数选项-标准参数选项
- 这种就是JAVA标准的参数选项
- JAVA-x类型
3.0.14 添加JVM参数选项
3.0.15 常见的JVM参数
3.0.15.1 打印设置的XX选项和值
3.0.15.2 堆,栈,方法区垃圾收集器等内存的设置
3.0.15.2 GC日志相关参数
3.0.15.3 其他参数
3.0.15.4 yangGC分析
3.0.15.5 FullGC分析