更新时间:2021-11-02 20:11:15
参考资料:
《深入理解 Java 虚拟机 - JVM 高级特性与***实践》
第1部分主题为自动内存管理,以此延伸出 Java 内存区域与内存溢出、垃圾收集器与内存分配策略、参数配置与性能调优等相关内容;
第2部分主题为虚拟机执行子系统,以此延伸出 class 类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎等相关内容;
第3部分主题为程序编译与代码优化,以此延伸出程序前后端编译优化、前端易用性优化、后端性能优化等相关内容;
第4部分主题为高效并发,以此延伸出 Java 内存模型、线程与协程、线程安全与锁优化等相关内容;
本系列学习笔记可看做《深入理解 Java 虚拟机 - JVM 高级特性与***实践》书籍的缩减版与总结版,想要了解细节请见纸质版书籍;
线程共享数据区:
方法区(Non-Heap 非堆):存储已被 Java 虚拟机加载的类信息
、常量
、静态变量
,即时编译器编译后的代码
等数据。当方法区无法满足内存分配需求时,抛出 OutOfMemoryError 异常;
对象实例和数组
。是垃圾收集器管理的主要区域。可能划分出多个线程私有的分配缓冲区。目的是为了更好的回收内存,或者更快的分配内存。可以处于物理上不连续的内存空间中。没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常;线程独立数据区:
虚拟机字节码指令的地址
,否则 Undefined。Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。是虚拟机中唯一没有规定 OutOfMemoryError 情况的区域;Java 虚拟机栈(Java Virtual Machine Stacks):生命周期和线程一致。存储 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧
(Stack Frame)用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息;
局部变量表:存放方法参数
和方法内定义的局部变量
。存放编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,能找到对象在 Java 堆中的数据存放的起始地址索引,与对象所属数据类型在方法区中存储的类型信息)、returnAddress类型(指向了一条字节码指令的地址)。线程安全。通过索引定位
方式使用局部变量表,容量以变量槽(slot)为最小单位;
方法出口:有两种出口:
直接内存(Direct Memory):非虚拟机运行时数据区的部分。不是 Java 虚拟机规范中定义的内存区域;
直接内存与堆内存的区别:
耗费高性能
,堆内存申请空间耗费比较低;IO 读写的性能优
于堆内存,在多次读写操作的情况相差非常明显;2. 分配内存:类加载检查通过之后,为新对象分配内存(在堆里,内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(有两种:‘指针碰撞’——serial、ParNew 算法;‘空闲列表’——CMS 算法);
对象头(Header):包含两部分:
通过栈上的 reference 数据(在 Java 堆中)来操作堆上的具体对象:
垃圾回收机制的缺点:是否执行,什么时候执行却是不可知的;
引用计数法:
可达性分析法:(主流)
GC Roots 的对象:
类静态属于引用的对象
;常量引用的对象
;对象的四种引用:(JDK 1.2 之后,引用概念进行了扩充)
相关代码:
System.gc()
Object.finalize()
分代:
废弃的常量
(没有该常量的引用)和无用的类
(所有实例已回收、该类的 ClassLoader 已回收、无法通过反射访问);Java 堆:新生代 + 老年代。默认新生代与老年代的比例的值为 1:2;
“标记-清理”算法
或者“标记-整理”算法
。大对象、长期存活对象分配在老年代;新生代(1/3):Eden + From Survivor + To Survivor。默认的 Edem : From Survivor : To Survivor = 8 : 1 : 1。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,剩余的存放回收后存活的对象(与复制算法
有关);
几种分代 GC:
Minor GC:新生代 GC。执行频繁,回收速度快;
Major GC:老年代 GC。通常会连着 Minor GC 一起执行。速度慢;
Full GC:清理整个堆空间,包括新生代和老年代。Full GC 相对于Minor GC来说,停止用户线程的 STW(stop the world)时间过长,应尽量避免;
空间分配担保:
老年代可用连续空间大小 < 新生代对象总大小
时,查看相关参数判断是否允许担保失败。允许则判断是否:年代可用连续空间大小 > 历次晋升老年代对象平均大小
,成立则进行 Major GC(有风险);不成立说明老年代可用连续空间很少,进行 Full GC。或者不允许担保失败也会进行 Full GC;老年代可用连续空间大小 > 新生代对象总大小
或 老年代可用连续空间大小 > 次晋升老年代对象平均大小
时,进行 Major GC。反之进行 Full GC;引用计数算法(Reference counting):
标记–清除算法(Mark-Sweep):
标记–整理算法:
复制算法:
分代算法:(次要)
垃圾回收算法是内存回收的理论,垃圾回收器是内存回收的实践;
垃圾收集器:
JDK8 的垃圾收集器:
Serial:Client 模式下默认。一个单线程收集器,只会使用一个 CPU 或者线程去完成垃圾收集工作,而且在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。单线程收集高效;
ParNew:可看做 Serial 的多线程版本,Server 模式下首选, 可搭配 CMS 的新生代收集器;
Parallel Scavenge:目标是达到可控制的吞吐量(即:减少垃圾收集时间)。吞吐量 Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间);
Serial Old:Serial 老年代版本,Client 模式下的虚拟机使用;
Parallnel old:Parallel Scavenge 老年代版本,吞吐量优先;
CMS:一种以获取最短回收停顿时间
为目标的收集器,适用于互联网站或者 B/S 系统的服务端上。并发收集、低停顿。与用户线程可以同时工作;
G1:最前沿成果之一。面向服务端应用的垃圾收集器。可看做 CM的终极改进版。JDK1.9 默认垃圾收集器。能充分利用多CPU、多核环境下的硬件优势。可以并行来缩短(Stop The World)停顿时间。能独立管理整个 GC 堆。采用不同方式处理不同时期的对象。
常用:
-XX:MaxPermSize=n 设置持久代大小;
不常用:
调优目的:GC 的时间足够的小、GC 的次数足够的少、发生 Full GC 的周期足够的长;
问题原因:Full GC 的停止用户线程的 STW 时间过长,应尽量避免;
Full GC 触发条件:主要是两个:老年代内存过小、老年代连续内存过小;
控制 Full GC 频率的关键:保障老年代空间的稳定,大多数对象的生存时间不应当太长,尤其是不能有成批量的、长生存时间的大对象产生;
实际方案:目前单体应用在较大内存的硬件上主要的部署方式有两种:
方案一:通过一个单独的 Java 虚拟机实例来管理大量的 Java 堆内存。具体来说:
方案二:使用多个 Java 虚拟机,建立逻辑集群来利用硬件资源。具体来说:
调优过程:
经验之谈:
1. 计划使用单个 Java 虚拟机实例来管理大内存,可能遇到的问题:
2. 使用逻辑集群的方式来部署程序,可能遇到的问题:
调优过程:
-XX:+HeapDumpOnOutOfMemoryError
参数 -> 运行一段时间发现存在大量 t.NAKACK 对象;解决思路:注意占用较多内存的区域:调整直接内存、线程堆栈、Socket 缓冲区大小,注意 JNI 代码,选择合适的虚拟机与垃圾收集器;
-XX:MaxDirectMemorySize
调整直接内存大小;-Xss
调整线程堆大小;调优过程:
调优过程:
Runtime.getRuntime().exec()
方法创建大量进程;调优过程:
HashMap<Long,Long>
类型 key 和 value 共占 2*8=16 字节,封装成 Map.Entry 后多了 16 字节对象头、8 字节 next 字段和 4 字节 int 类型的 hash 字段,为了对其追加 4 字节空白对象头,还有 8 字节对这个 Map.Entry 的引用。最后实际耗费的内存为 (Long(24byte)×2)+Entry(32byte)+HashMap Ref(8byte) = 88byte
,空间效率为:16 字节 / 88 字节 = 18% 太低;实际方案:将新生代空间减少或使用亲合式集群将大内存划进老年代(类似 4.1)。除此之外还可以将 Survivor 空间去掉,让新生代中存活的对象在第一次 Minor GC 后立即进入老年代,等到 Major GC 的时候再去清理它们。最根本的方法是优化数据结构;
方案一:去掉 Survivor 空间。具体来说:
参数 -XX:SurvivorRatio=65536
、-XX:MaxTenuringThreshold=0
;
- 2\. 或者 `-XX:+Always-Tenure`;
调优过程:
实际方案:在应用程序最小化后阻止 JVM 对其进行修剪。具体来说:
-Dsun.awt.keepWorkingSetOnMinimize=true
;调优过程:
-XX:+PrintGCApplicationStoppedTime-XX:+PrintGCDate-Stamps-Xloggc:gclog.log
-> 确认了停顿确实是由垃圾收集导致;-XX:+PrintReferenceGC
参数,找到长时间停顿的具体日志信息 -> 发现从准备开始收集,到真正开始收集之间所消耗的时间却占了绝大部分;的数据类型作为索引值,不进入安全点(具有让程序长时间执行的特征)。在 HBase 连接中有很多个Mapper / Reducer / Executer 线程。清理这些线程靠一个连接超时清理的循环函数, HotSpot 判断这个循环函数为可数循环,等待循环全部跑完才能进入安全点,此时其他线程也必须一起等着,宏观来看就是长时间停顿;
调优过程:
-XX:+PrintSafepointStatistics
和 -XX:PrintSafepointStatisticsCount=1
查看安全点日志 -> 发现虚拟机在等待所有用户线程进入安全点时有线程很慢;+SafepointTimeout 和
-XX:SafepointTimeoutDelay=2000` 两个参数,使虚拟机在等到线程进入安全点的时间超过 2000 毫秒时就认定为超时 -> 输出导致问题的线程名称;