【读书笔记】深入理解Java虚拟机(周志明)(1)第一部分 走进Java(2)第二部分 自动内存管理机制
文章目录
文章说明
本文是《深入理解Java虚拟机(周志明)》这本书的重点摘要。
本笔记仅作为复习,不过多的对内容进行讲解。
本笔记按照书的目录进行,如遇到需要细看的,可以到书中找对应内容。
本笔记并不是按照书中原话进行摘要,而是根据自己的理解使用大白话进行记录,同时进行了少部分扩展。如有错误欢迎指出。
由于内容较多,一共分为三篇:
篇幅 | 链接 |
---|---|
深入理解Java虚拟机(周志明)(1)第一部分 走进Java(2)第二部分 自动内存管理机制 | https://blog.csdn.net/zhaohongfei_358/article/details/134927759 |
深入理解Java虚拟机(周志明)(3)第三部分 虚拟机执行子系统(4)第四部分 程序编译与代码优化 | https://blog.csdn.net/zhaohongfei_358/article/details/135067398 |
深入理解Java虚拟机(周志明)(5)第五部分 高效并发 | https://blog.csdn.net/zhaohongfei_358/article/details/135111650 |
第一部分 走近Java
第1章 走进Java
1.1 概述
无重点
1.2 Java技术体系
Java按运行平台可分为四种:
- Java Card:运行在小内存设备上的Java小程序(Applets),例如:智能卡
- Java ME(Micro Edition):移动中断,例如:手机、Pad。
- Java SE(Standard Edition):面向桌面应用程序
- Java EE(Enterprise Edition):面向企业应用
1.3 Java 发展史
无重点
1.4 Java虚拟机发展史
无重点
1.5 展望Java技术未来
无重点
1.6 实战:自己编译JDK
无重点
第二部分 自动内存管理机制
第2章 Java内存区域与内存溢出异常
2.1 概述
学习Java虚拟机的重要原因:当出现内存泄漏时,知道怎么排查。
2.2 运行时数据区域
数据区有两种:
- 由所有线程共享的数据区:字面意思,所有线程都使用同一个。
- 线程隔离的数据区:每个线程有自己的,例如:每个线程都有自己的程序技术器
如上图,JVM内存被分为了如下区域:
- 程序计数器:线程私有(即每个线程拥有自己的程序计数器),用于记录当前线程执行到哪一行了。
- 虚拟机栈:线程私有,用于记录方法执行过程中的局部变量、方法出口等信息。每当进入一个方法,就会为该方法创建一个“栈帧”,该方法的局部变量都在这个栈帧中存储。(第8章详解)
- 本地方法栈(Native Method Stack):和虚拟机栈类似,不过是给Native方法用的。(Native方法就是那些在外部实现的方法,例如某些方法是用C++实现的)
- 堆:线程共享(即整个虚拟机只有一块堆内存,所有线程都共用这一块),“几乎”所有的对象和数组都存在堆中。研究JVM重点就是研究堆内存。堆内存空间不足时会抛出
OutOfMeoryError
(OOM)。(第3章详解) - 方法区(Method Area):线程共享,用来存类信息、常量、静态变量、动态编译的代码(在运行时编译的代码,而非一开始编译好的)。方法区也被称为永久代(Permanent Generation)。
- 运行时常量池:方法区的一部分。用于存放常量(就是被final修饰的部分)。字符串也是放在常量池中的。
- 直接内存(Direct Memory):非虚拟机内存。直接操作本机的物理内存。例如:我写了一个Native方法调用了一个外部的C++程序,在这个C++程序中申请了10M内存,那这10M内存不会算在JVM中,而是直接和从本机物理内存申请的。
2.3 HotSpot虚拟机对象探秘
2.3.1 对象的创建(过程)
当JVM遇到一条new
指令时,会进行如下过程:
- 去“方法区”中找是否加载了该类。若没有加载,则会执行“类加载”过程。(第7章详解)
- 为新对象分配内存(分多少在类加载后即可确定)。(不同JVM分配方式详见书籍)
- 为分配的内存区域全部赋值0。(这也是为什么类字段不给默认值时会默认初始化为0)
- 设置对象信息,存储在对象头(Object Header)中。包括:该对象是哪个类、HashCode值等。
- 最后执行构造方法初始化对象。
2.3.2 对象的内存布局
一个对象在内存中有三块区域,分别是:
- 对象头(Header):存储对象的必要信息,包括:hashCode、GC分代年龄、锁状态标志、该对象是哪个类(用于找到对应类的信息)等
- 实例数据(Instance Data):存储对象字段数据(就是用户定义的各个变量)。
- 对齐填充(Padding):因为分配内存的时候最小单位是8字节,最后用不完的就填充一下。
2.3.3 对象的访问定位
我们使用的对象变量仅存储该对象的引用(reference),因此在实际访问对象时需要根据reference去堆内存中查找。
查找方式有两种(不同的JVM采用方式不同):
- 直接指针访问对象(常用):reference记录的就是对象在堆内存的地址,可以用这个地址直接在虚拟机中找到对象数据。若要找该对象对应类的信息,那么根据对象头中的类信息地址去方法区找。
- 通过句柄访问对象:reference指向的是句柄池,句柄池中记录了对象的地址。由于句柄是两次访问,因此缺点是速度较慢。优点是移动对象时(垃圾回收时会移动对象的位置)只需要更新句柄中的地址,比较方便。
2.4 实战:OutOfMemoryError异常(OOM)
除了程序计数器,其他区域都可能会出现OOM。
以下是不同内存区域OOM的原因:
- Java堆溢出:当不断的new对象,但这些对象又释放不掉,最终导致堆内存不够用时,就会产生OOM。堆溢出分两种原因:
- 内存泄露(Memory Leak):本该释放掉的对象由于代码bug,导致还在被引用,因此没有被释放。解决方案:分析dump文件的GC Roots引用链。
- 内存溢出(Memory Overflow):不存在内存泄露,所有的对象都是程序必须的,就是单纯的堆内存不够用了。解决方案:调大堆内存(
-Xmx
与-Xms
参数)
- 虚拟机栈和本地方法栈溢出:栈溢出通常有两种情况:
- 栈深度过大(常见):抛出
StackOverflowError
异常。一般出现在“异常的递归”中,正常方法间调用的深度不至于溢出。 - 栈内存不够(不常见):可以调整
-Xss
参数增大栈内存容量。
- 栈深度过大(常见):抛出
- 常量池溢出:由于字符串存在常量池中,如果程序中有大量被引用的字符串时,导致这些字符串不能被垃圾回收,最终就会出现常量池溢出。例如:你有个
List<String>
,然后你一直往里扔“不同的”String,就会出现常量池溢出。 - 方法区溢出:方法区是存Class信息的,因此当运行时不断的动态加载类时(例如使用动态编译在运行时新增类),就会产生方法区溢出。
- 本机直接内存溢出:当Native方法直接向本机申请内存,但内存又不够时,就会出现内存溢出。(实际上并没有真正执行申请动作,而是在检查本机内存是否够这个动作中抛出的异常,异常栈的最顶层一般为:
sun.misc.Unsafe.allocateMemory(Native Method)
)
第3章 垃圾收集器与内存分配策略
3.1 概述
学习垃圾收集(Garbage Collection, GC)的原因:可以帮助我们避免和解决OOM问题、由JVM引起的性能瓶颈等
3.2 对象已死吗(如何判断对象可以被回收)
3.2.1 引用计数法(不常用)
思路:记录每个对象当前被多少对象引用。若为0,说明没有被引用,就可以被回收了。
优点:简单、高效
缺点:无法解决循环引用问题。(例如:A、B两个垃圾互相引用,导致无法被回收)
3.2.2 可达性分析算法(常用)
思路:从“GC Roots”对象出发,若无法访问到某个对象,说明这个对象可以被回收了。
以下变量都会作为GC Roots:
- 虚拟机栈中的变量:目前还没运行结束的方法中的变量。
- 类的静态变量
- 方法区中的常量:例如常量池中的String
- 本地方法栈中引用的对象
- Java虚拟机的内部对象:例如:基本数据类型对应的Class对象等
- 被synchronized锁住的对象
- …
3.2.3 再谈引用(强/软/弱/虚引用、引用队列)
Java中的引用可以按强弱程度分为四种,JVM对不同程度的引用回收策略不同:
强引用(Strong Reference):我们平时用的都是强引用。例如:MyObject myObj = new MyObject();
- 回收:只要有引用,就不会被回收。
软引用(Soft Reference):使用SoftReference
显式声明。
- 回收:当JVM内存不够时,会对软引用对象进行回收。
- 应用场景:做缓存。
- 使用样例:
MyObject myObject = new MyObject("Amy"); // 从数据库中获取数据 SoftReference<MyObject> reference = new SoftReference<>(myObject); // 增添软引用 // do something ... myObject = reference.get(); // 尝试获取myObject对象 if (myObject == null) { // 没获取到,说明已经被JVM回收了 myObject = new MyObject("Amy"); // 重新从数据库中获取数据 } else { // 没有被JVM回收 }
弱引用(Weak Reference):使用WeakReference
显式声明。
- 回收:当JVM下次执行垃圾回收时,就会立刻回收。
- 应用场景:做缓存。
- 使用样例:和上面
SoftReference
一样,把SoftReference
改成WeakReference
即可。
虚引用(Phantom Reference):也称为“幽灵引用”、“幻影引用”等。
- 回收:当JVM执行垃圾回收时,就会立刻回收
- 应用场景:单纯的将其标记一下,配合引用队列(
ReferenceQueue
)进行回收前的一些操作 - 特殊点:虚引用的
reference.get()
方法一定会返回null
(源码就是直接return null
,这也是为什么叫虚引用的原因。 - 使用样例:建后续引用队列。
注:一个对象可以同时存在多种引用。例如:
MyObject myObject = new MyObject("Amy"); // 此时myObject存在强引用
SoftReference<MyObject> reference = new SoftReference<>(myObject); // myObject同时存在强引用和软引用
myObject = null; // 去掉myObject的强引用,其只剩下软引用
引用队列:在定义软/弱/虚引用时,可以传个引用队列(虚引用必须传),这样对象在被回收之前会进入引用队列,可以显式的对其进行一些操作。(引用队列只能获取到“引用对象”即XxxReference
,获取不到原对象,因为可能已经被JVM释放了)。
对着四种引用更详细的讲解可参考:Java中的强引用、软引用、弱引用、虚引用与引用队列 通俗举例实战详解
3.2.4 生存还是死亡(finalize方法)
对象被释放会经历两次标记:
- GC Roots不可达,第一次标记
- 没有重写
finalize
或JVM已经调用过finalize
。注意:这里只是调用过,执行成没成功甚至是否执行完成都不管
当两次标记结束后,就会正式释放对象。
若用户重写了finalize,则对象会在第一次标记后经历如下过程:
- 进入
F-Queue
队列 - JVM使用
Finalizer
线程去调用finalize
方法 - 之后不管
finalize
是否执行完,都释放对象。
若用户在
finalize
方法中重新给对象增加了引用(不推荐这么做),那么这个对象就不会被释放了。
由于JVM并不保证finalize是否执行完,因此不推荐使用finalize
方法。如果要释放资源,try-with和虚引用都比finalize
更好。
3.2.5 回收方法区
方法区通常有两种东西要回收:
- 废弃常量:例如 各种字符串。
- 无用的类:对于各种动态生成的类,若其对象和ClassLoader都被回收了,其就可能被回收。一般用到动态代理的框架中会有相关的卸载类机制来避免方法区OOM。
3.3 垃圾收集算法
不同的虚拟机会采用不同的垃圾收集算法。也可能会同时使用不同的收集算法。
(图片来源:JVM 内存结构)
JVM根据对象的存活时长,将堆区域分为了新生代(Young)和老年代(Old Generation),而新生代又被分为了Eden区和两个Suvivor区:
- 新生代:用于存放新生成的对象,内存小,清理快。新生代的GC称为Minor GC。新生代包含1一个Eden区和两个Survivor区
- Eden区:新生成的对象存在Eden区,当Eden区慢的时候会触发GC(也会定期清理)。其采用复制算法,将存活的对象复制到一个空的Survivor区域。
- Survivor区:两个Surviro区域,其中一个为空,另一个用于存放年龄大于1的对象。当发生Minor GC时,会将有对象的Survivor区域进行清理,并将存活对象全部挪到另一个Survivor去,然后清空当前Survivor取,并将所有对象年龄+1(表示这些对象又躲过了一次Minor GC,因此年龄又涨了一岁)。两个Suvivor区域就是这么交替清理。当对象存货超过15岁(参数可调),就会被移到老年代。
- 老年代:用于存放长期存活的对象,内存大,因此清理速度慢。(若有一个大对象Eden区放不下,那么也会直接进入老年代)。
3.3.1 标记-清理算法
思路:分两步:① 先对垃圾对象进行标记; ② 对标记的对象进行清理
缺点:会产生大量内存碎片
3.3.2 复制算法
思路:将内存分成两个区域。一次只使用一个区域,当该区域满时,直接将该区域存活的对象复制到另一个区域,然后使用另一个区域,因此这个区域就可以直接清空了。
优点:没有内存碎片
缺点:内存可用区域减少。
3.3.3 标记-整理算法
思路:与标记清除类似。分三步:① 标记;② 将存活对象往一端挪,避免内存碎片。③ 清理另一端内存。
缺点:慢
优点:无内存碎片
3.3.4 分代收集算法
思路:将内存分为新生代和老年代。
- 新生代:内存小,里面都是新对象,采用标记整理算法。
- 老年代:内存大,里面都是存货时间长的对象,采用复制算法。
3.4 HotSpot的(收集)算法实现
3.4.1 枚举根节点
为了保证GC过程中引用不能发生变化,因此在枚举根节点时,所有的Java线程都会暂停,称为Stop The World
。
3.4.2 安全点(Safepoint)
在开始枚举根节点前,必须要保证所有的线程都处在一个安全点(safepoint)上,以便可以快速准确完成GC。若线程不在安全点上,那就需要等它执行到安全点。
3.5 (不同实现的)垃圾收集器
没有最好的垃圾收集器,只有最适合自己的。
3.5.1 Serial收集器(不常用)
古老的收集器,现在不用了。
特点:单线程
3.5.2 ParNew收集器(不常用)
在Serial收集器的基础上增加了多线程。
3.5.3 Parallel Scavenge收集器
在ParNew收集器的基础上,增加了对吞吐量的关注。即:一定时间内,让JVM更多的运行Java代码。
主要方式就是:自适应条件新生代老年代内存大小、GC频次等。(也可以手动配)
因此,Parallel Scavenge收集器适用于计算型任务。
3.5.4 Serial Old收集器
无重点
3.5.5 Parallel Old收集器
无重点
3.5.6 CMS收集器(JDK1.8的收集器)
CMS(Concurrent Mark Sweep)目标:致力于回收停顿时间最短。常用于服务端,
CMS采用“标记-清理”算法,共分为4步:
- 初始标记:Stop the World,仅标记直接和GC Roots关联的对象(即父节点是GC Roots的对象)
- 并发标记:与用户程序并发进行,沿着初始标记的节点标记下面的子节点。
- 重新标记:需要Stop the World。由于②过程是并发进行,部分节点引用会改变,因此需要再次扫描整个堆,修正改变引用的标记。时间较慢。
- 并发清除:与用户程序并发进行。清除无用的对象
CMS收集器的缺点:
- 由于是并发收集,占用CPU资源。
- 预留内存,提前GC:由于清理过程是并发进行的,那么清理时就要给用户线程预留内存,避免用户线程申请的对象没地方放导致OOM。假设给用户预留8%内存,那么当老年代内存占比到92%时就会开始GC。
- 并发收集失败(Concurrent Model Failure)问题:若GC过程中,预留内存不够用户线程用,就会导致Concurrent Model Failure。此时会停止并发收集,改用原始Full GC,即Stop the World,然后标记清理。(因此,需要根据业务情况调节预留内存大小)
- 内存碎片:很明显,CMS会产生内存碎片。若导致用户线程大连续对象分不到内存,就会提前触发Full GC,并对内存碎片进行整理。
3.5.7 GI 收集器
GI收集器在CMS收集器的基础上进一步进化。
GI收集器面向服务端应用。JDK1.9作为默认收集器。
GI收集器在上述收集器的基础上,实现了“可指定最大停顿时间”。官方称为全能收集器。
GI收集器基本思路:
① GI将堆分为了许多同等大小的Region,每个小格子就是一个Region,每个Region取值范围为1~32MB。
② 这样Eden区、Survivor区、Old区都是逻辑连续,实际物理不连续。
③ 各个区的region数量不固定,运行过程中可以灵活调节。
④ Humongous区用于存放大对象。若一个对象的大小超过了Region大小的50%,就认为是大对象。
GI的三种垃圾回收模式:
- Young GC:当Eden区满时,可以很容易估算Eden区GC的耗时,若耗时太小,就会分几个Region给Eden区。
- Mixed GC:新生代+部分老年代(根据用户设置的“最大停顿时间”来决定回收哪些回收多少Old的Region区)+大对象区域
- Full GC:全堆扫描,对所有区域回收。
3.5.8 理解GC日志
GC日志样例:
33.125:[GC[DefNew: 3324K- >152K(3712K),0.0025925 secs] 3324K- > 152K( 11904K), 0. 0031680 secs]
100.667:[Full GC[Tenured: 0 K- > 210K( 10240K), 0. 0149142secs] 4603K- > 210K( 19456K),[Perm: 2999K->2999K( 21248K)], 0. 0150007 secs][Times: user= 0.01 sys= 0.00, real=0.02 secs]
含义如下:
33.125
/100.667
:GC发生的时间。该数字为JVM启动后经历的秒数。[GC
/[Full GC
:GC的类型。Full GC会Stop-The-World[DefNew
/Tenured
/Perm
:GC发生的区域(不同的虚拟机名字会有差异)3324K->152K(3712K)
:GC前该内存区域已使用容量
->GC后该内存区域已使用容量
(该内存区域总容量
)0.0025925 secs
:GC所使用的时间[Times: user=0.01 sys=0.00, real=0.02secs]
:user=用户态耗时、sys=内核态耗时、real总耗时(包括从准备开始GC到真正开始GC消耗的时间,这部分也会STW,见5.2.7节)。
3.5.9 垃圾收集器参数总结
SurvivorRatio
:调整Eden区和Survivor区的比值。默认为8:1:1。例如:我们经常要产生临时大对象时,可以将PretenureSizeThreshold
:直接晋级到老年代对象的大小。CMSInitiatingOccupancyFraction
:设置CMS收集器GC时给用户留多少%的空间
3.6 内存分配与回收策略
对象优先在Eden区分配
GC分两种:
- 新生代GC(Minor GC):新生代的GC,新生代内存小,GC速度快。
- 老年代GC(Major GC/Full GC):老年代的GC,老年代内存大,GC慢
大对象“可能”会直接进入老年代,三种情况:
- 新生代内存不够放大对象
- 大对象连续空间,新生代没这么多连续空间
- 大对象大小超过了虚拟机的配置(虚拟机可以配当对象超过多大直接进入老年代),直接进入老年代
长期存活的对象将进入老年代:对象每躲过一次Minor GC,年龄就会+1,当年龄到15岁时就会进入老年代。
第4章 虚拟机性能监控与故障处理工具
4.1 概述
无重点
4.2 JDK的命令行工具
JDK在bin
目录下提供了各种用于诊断程序的命令行工具:
4.2.1 jps:虚拟机进程状况工具
jps
:查看当前机器都运行了哪些Java程序。
样例:
> jps
181584 Launcher
180296 KotlinCompileDaemon
182216 RemoteMavenServer
176556 MySpringBootApplication
185036 Jps
前面的数字是该程序的虚拟机唯一ID(Local Virtual Machine Identifier, LVMID),后面排查问题需要用到。
后面的是Java程序的main
方法类名。
4.2.2 jstat:虚拟机统计信息监视工具
jstat
是一个监视各种状态的通用命令。
使用方式为:jstat -[工具] [vmid]
vmid就是上面提到的LVMID,使用jps命令查看。
使用样例:
监视GC情况:
> jstat -gc 176556
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
47616.0 42496.0 0.0 38949.0 968704.0 505022.9 165376.0 112599.7 92032.0 86841.6 11904.0 10893.4 15 0.255 3 0.185 0.440
监视类“装/卸载”情况:
> jstat -class 176556
Loaded Bytes Unloaded Bytes Time
16799 31461.8 7 7.1 18.95
jstat主要工具:
4.2.3 jinfo:Java配置信息工具
jinfo
:用于实时查看和调整虚拟机各项参数
使用方式:
- 查看JVM配置信息:
jinfo [vmid]
- 增加JVM配置:
jinfo -flag +[配置项]
- 取消JVM配置:
jinfo -flag -[配置项]
- 修改JVM配置:
jinfo -flag [配置项]=[修改后配置]
。注意:大部分配置是不可以动态修改的。若不能(或者是拼写错误),都会报flag 'XXX' cannot be changed
错误。
使用样例:
查看虚拟机各项参数:
> jinfo 176556
Attaching to process ID 176556, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.281-b09
Java System Properties:
jboss.modules.system.pkgs = com.intellij.rt
.... # 这里省略若干参数
VM Flags:
Non-default VM flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote .... # 省略若干参数
运行时增加打印GC详细信息:
> jinfo -flag +PrintGCDetails 176556
# 执行完这条命令后,再观察程序,就会发现发生GC时就会在控制台打日志了
运行时取消打印GC详细信息:
> jinfo -flag -PrintGCDetails 176556
# 执行完后,发现程序又不打印日志了
运行时修改Dump文件路径:
> jinfo -flag HeapDumpPath=D:/ 176556
4.2.4 jmap:Java内存映像工具
jmap
:用于查看/导出Java内存使用情况
使用方式:jmap [option] <vmid>
jmap工具的主要选项:
使用样例:
导出Dump文件到指定位置:
> jmap -dump:file=D:\test.dump 176556
Dumping heap to D:\test.dump ...
Heap dump file created
4.2.5 jhat:虚拟机堆转储快照分析工具
jhat
(Java Heap Analysis Tool):用于分析dump文件。
由于功能简陋,目前已经不怎么使用了。一般用VisualVM或其他更专业的工具
4.2.6 jstack:Java堆栈跟踪工具
jstack
:生成虚拟机当前时刻的线程快照。
使用方式:jstack <vmid>
使用样例:
> jstack 176556
# ... 生成了许多堆栈信息
4.2.7 HSDIS:JIT生成代码反汇编
XXX.class
字节码只是描述了程序在虚拟机中应该怎么执行,但使用不同的虚拟机运行过程还不太一样。如果我们想知道真正怎么执行,可以用JIT命令生成汇编文件,然后看虚拟机是怎么执行的。
4.3 JDK的可视化工具
4.3.1 JConsole:Java监视与管理控制台
JConsole是将上述的各种命令行工具的结果可视化出来了。
JConsole
使用方式:打开bin
目录下的JConsole.exe
,选择你要查看的程序即可。
JConsole会展示如下内容:
概述:
内存使用情况:
线程使用情况:
类加载情况:
虚拟机状况与参数:
MBean属性与执行MBean操作:
MBean:Java中可以将对象注册成MBean,这样外部程序就可以通过JMX查看、修改该对象的属性,也可以执行该对象的方法。例如:上面图片中我们可以利用JConsole执行SpringApplication对象的shutdown方法
4.3.2 VisualVM:多合一故障处理工具
VisualVM是官方强大的运行监视和故障处理程序(上面能干的,它基本上都能干)。其支持插件,因此有无限可能。
启动方式:执行jdk的bin
目录下的jvisualvm.exe
文件。进入后,再左侧选择你的Java程序。
VisualVM的插件安装:选择工具
->插件
,然后在可用插件出选择要安装的插件即可。
常用功能举例:
Visual GC:查看GC情况,清晰的看到堆的每个区域的使用情况
监视:查看CPU、堆、类、线程的基本情况
线程:查看线程的运行情况
其他常用:
- Profiler:分析方法的CPU情况和内存情况
- BTrace:动态增加调试代码。例如:生产报错,但是没加日志查不了。可以使用BTrace在不停程序的情况下,增加打印日志代码。
第5章 调优案例分析与实战
5.1 概述
无重点
5.2 案例分析
5.2.1 高性能硬件上的程序部署策略
异常场景:网站15万PV/天 (PV=Page View,可以理解为请求量)。每隔十几分钟,网站就卡十几秒。
机器情况:4个CPU,16GB内存,Java堆固定12GB
经过排查后:
- 直接原因:① Full GC频繁,② 且Full GC消耗时间长(一次要十几秒)
- 根本原因:① 由于网站总是产生大对象,因为Eden区放不下,进而大对象直接进入老年代,导致老年代很快就满了,进而频繁引发Full GC。② 由于堆内存过大,因此一次Full GC消耗的时间较长。
解决方法:
- Full GC频繁问题:优化代码,拆分大对象,或利用缓存等,避免大对象直接进入老年代。
- Full GC时间长问题:采用逻辑集群。即 将程序部署多份,每份的堆内存都控制在一个较小的值(例如2G),避免一次Full GC时间过长。
5.2.2 集群间同步导致的内存溢出
异常场景:程序是集群部署,隔一段时间就会内存溢出(OOM)。
排查后发现:
- 原因:集群之间采用了JBossCache进行数据同步,而这个框架设计有缺陷,同步失败重试时会有大量对象驻留内存,最终导致内存不够用。
因此,我们在开发或排查问题时,也要考虑会不会存在大量对象释放不掉导致OOM的问题。
5.2.3 堆外内存导致的溢出错误
异常场景:系统经常产生OOM,但堆内存的各个区域都很稳定,并且有发现内存不足现象。
排查后发现:
- 直接原因:是堆外内存不足导致的OOM,即Native方法产生的溢出(Native方法直接使用系统内存)。
- 根本原因:程序中用到了大量的NIO(NIO有大量的Native方法),该方法要不断的申请系统内存。而系统2G,给JVM分了1.6G,NIO只剩下0.4G可以用。而系统内存不够用时,JVM只能先抛出异常,然后再调用
System.gc()
,尝试让JVM去回收一下堆外内存。
解决方法:合理的调节JVM内存大小,给系统预留足够的内存。
5.2.4 外部命令导致系统缓慢
异常场景:系统CPU占用过高,但排查发现并不是Java程序占用高。
排查后发现:
- 直接原因:占用CPU高的是
fork
系统调用。该调用是创建进程用的。 - 根本原因:代码中采用了
Runtime.getRuntime.exec()
方法执行shell脚本,且非常高频。因此导致高频的创建进程。
解决方案:降低这段代码频率,或采用其他的替代方案。总之,不要高频创建进程。
Runtime.getRuntime.exec()
的执行逻辑:
- 首先克隆一个和当前虚拟机拥有一样环境变量的进程
- 使用该进程执行外部命令
- 最后退出这个进程
5.2.5 服务器JVM进程崩溃
异常场景:Java程序总是异常崩溃(不是OOM,是直接崩掉了)
排查后发现:
- 直接原因:程序不断创建线程,且线程一直处于等待状态,最终崩溃。
- 根本原因:远程调用响应过慢,导致线程等待,随着等待的线程越来越多,机器受不了了。
解决方案:远程调用增加超时时间,避免线程阻塞。或采用其他异步方案,例如MQ。
5.2.6 不恰当数据结构导致内存占用过大
异常场景:由于业务需要每10分钟加载一个80M的文件进行分析,导致分析期间Minor GC过于频繁,且时间较长。
原因:分析文件期间,Eden区很快就被占满,但由于这些对象要用一段时间,导致还清理不掉,因此会出现频繁Minor GC。同时,分析文件时使用的是HashMap<Long, Long>
,因为key, value都是Long,空间利用率过低(HashMap每个节点还有有其他数据去维护数据结构)。
解决方案:① 调整JVM参数,让这些数据直接进入老年代(不推荐)。② 优化分析代码,使用空间利用率高的数据结构。
5.2.7 由Windows虚拟内存导致的长时间停顿
异常情况:一个简单的GUI程序,平时GC都很快,一最小化,GC就会耗时很久。
根本原因:在windows程序被最小化时,该程序就改用虚拟内存了。
解决方案:启动时增加-Dsun.awt.keepWorkingSetOnMinimize=true
参数。
5.3 实战:Eclipse运行速度调优
无重点