1 JVM 逃逸分析
1.1 背景
随着
JIT
编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会
导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
在
Java
虚拟机中,对象是在
Java
堆中分配内存的,这是一个普遍的常识。但是,有
一种特殊情况,那就是如果经过逃逸分析(
Escape Analysis
)后发现,一个对象并没有逃
逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃
圾回收了。这也是最常见的堆外存储技术。
逃逸分析技术到现在还不是很成熟,虽然经过逃逸分析可以做标量替换、栈上分配、锁
消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过
程。
1.2 何为逃逸分析
逃逸分析一种数据分析算法,基于此算法可以有效减少
Java
对象在堆内存中的分配。
Hotspot
虚拟机的编译器能够分析出一个新对象的引用范围,然后决定是否要将这个对象
分配到堆上。例如:
>当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
>
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
1.3 逃逸分析案例演示
1.3.1
逃逸对象
如下代码中的
StringBuffer
发生了逃逸,不会在栈上分配。

1.3.2
未逃逸对象
当一个对象在方法内创建,又没有被外界引用,此对象为非逃逸对象。例如
1.3.3
逃逸分析参数设置
在
JDK 1.7
版本之后,
HotSpot
中默认就已经开启了逃逸分析,如果使用的是较早的
版本,开发人员则可以通过:
> 选项“
-XX:+DoEscapeAnalysis"
显式开启逃逸分析。
>
通过选项“
-XX:+PrintEscapeAnalysis"
查看逃逸分析的筛选结果。
建议:开发中能在方法内部应用对象的,就尽量控制在内部。
1.4 代码优化实践
1.4.1
概述
使用逃逸分析,编译器可以对代码做如下优化:
▪ 栈上分配:将堆分配转化为栈分配。如果一个对象在方法内创建,要使指向该对象的引
用不会发生逃逸,对象可能是栈上分配的候选。
▪ 同步省略:又称之为同步锁消除,如果一个对象被发现只有一个线程被访问到,那么对
于这个对象的操作可以不考虑同步。
▪ 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问
到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。
1.4.2
栈上分配
对如上代码运行测试时,分别开启和关闭逃逸分析,检查控制台日志的输出以及花费时
间上的不同。
1.4.3
同步锁消除
我们知道线程同步是靠牺牲性能来保证数据的正确性,这个过程的代价会非常高。程序
的并发行和性能都会降低。
JVM
的
JIT
编译器可以借助逃逸分析来判断同步块所使用的锁
对象是否只能够被一个线程应用?假如是,那么
JIT
编译器在编译这个同步块的时候就会
取消对这部分代码上加的锁。这个取消同步的过程就叫同步省略,也叫锁消除。
1.4.4
标量替换分析
所谓的标量(
scalar
)一般指的是一个无法再分解成更小数据的数据。例如,
Java
中
的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(
Aggregate
),
Java
中的对象就是聚合量,因为他可以分解成其他聚合量和标量。在
JIT
阶段,如果经过逃逸分
析,发现一个对象不会被外界访问的话,那么经过
JIT
优化,就会把这个对象分解成若干个
变量来代替。这个过程就是标量替换。例如:

对于上面代码,假如开启了标量替换,那么
alloc
方法的内容就会变为如下形式:

alloc
方法内部的
Point
对象是一个聚合量,这个聚合量经过逃逸分析后,发现他并
没有逃逸,就被替换成两个标量了。那么标量替换有什么好处呢?就是可以大大减少堆内存
的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。标量替换为栈上分配
提供了很好的基础。
2 内存溢出分析
2.1 何为内存溢出
内存中剩余的内存不足以分配给新的内存请求就会内存溢出。内存溢出可能直接导致系统溃。
2.2 内存溢出的原因
导致内存溢出的原因,可能有:
▪ 创建的对象太大导致堆内存溢出
▪ 创建的对象太多导致堆内存溢出
▪ 方法出现了无限递归调用导致栈内存溢出
▪ 方法区内存空间不足导致内存溢出。
▪ 出现大量的内存泄漏
3 JVM 内存泄漏
3.1 内存泄漏分析
3.1.1
何为内存泄漏
动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序
结束。这个现象称之为内存泄漏。因此良好的代码规范,可以有效地避免这些错误。
3.1.2
内存泄漏带来的问题
长时间运行,程序会变卡,性能严重下降。
(
早期的只能手机这个问题很严重
)
OutOfMemoryError
错误,系统直接挂掉。
猎才
3.1.3
导致内存泄漏的原因
▪ 大量使用静态变量(
静态变量与程序生命周期一样
)
▪ IO/连接资源用完没关闭
(
记得执行
close
操作
)
▪ 内部类的使用方式存在问题(
实力内部类或默认引用外部类对象
)
▪ 缓存(Cache)
应用不当
(
尽量不要使用强引用
)
▪
ThreadLocal
应用不当
(
用完记得执行
remove
操作
)
3.1.4
内存泄漏分析常用手段
▪ 应用内存分析工具 JProfiler, Java VisualVM
等。
▪ 在开发阶段时或者在测试环节,增加压力测试(
可考虑使用
JMeter
进行压力测试
)
。
▪ 认真对待开发工具给出的告警提示,该关闭的资源尽早关闭。
▪ 选择合适的时机进行代码 review
。
通俗地说,我们可以将内存泄漏视为一种疾病,如果不治愈,随着时间的推移,它可能导致
致命的应用程序崩溃。内存泄漏很难解决,发现它们需要对
Java
语言的复杂掌握和掌握。
在处理内存泄漏时,没有一种万能的解决方案,因为泄漏可能通过各种不同的事件发生。
但是,如果我们采用最佳实践并定期执行严格的代码排查和分析,那么我们可以将应用程序
中内存泄漏的风险降到最低。
4 Java 四大引用分析
4.1 背景分析
Java
中为了更好控制对象的生命周期,提高对象对内存的敏感度,设计了四种类型的引用。
按其在内存中的生命力强弱,可分为强引用、软引用、弱引用、虚引用。其中,
"
强引用
"
引
用的对象生命力最强,其它引用引用的对象生命力依次递减。
JVM
的
GC
系统被触发时,会
因对象引用的不同,执行不同的对象回收逻辑。
4.2 准备工作
创建一个类,作为演示应用,例如:

4.3 引用类型应用
4.3.1强引用:
通过对象自身类型或父类类型直接引用当前对象称之为强引用,例如:

其中,使用
"
强引用
"
引用的对象,即便
JVM
内存空间不足,触发了
GC
操作,
JVM
宁愿抛出
OutOfMemoryError
运行时错误,也不会回收有强引用引用的对象。如果对象已不可以通过 何"
强引用
"
所访问,就可以直接被垃圾回收了,但具体回收时机还是要看垃圾收集策略。
4.3.2 软引用:
软引用通过
SoftReference
类型对象实现。
"
软引用
"
引用的对象,在
JVM
认为内存不足
时,会去试图回收其引用的对象。即
JVM
会确保在抛出
OutOfMemoryError
之前,清理
软引用指向的对象。软引用可以和一个引用队列
(ReferenceQueue)
联合使用,如果“软引
用”所引用的对象被垃圾回收器回收,
Java
虚拟机就会把这个软引用加入到与之关联的引
用队列中。后续,我们可以调用
ReferenceQueue
的
poll()
方法来检查是否有它所关心的
对象被回收。
软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留对象,当内存不
足时就清理掉对象,这样就可以更好保证在使用缓存时,尽量不会耗尽内存。
4.3.3
弱引用:
弱引用通过
WeakReference
类实现。当
GC
时,一旦发现了具有“弱引用”引用的对象,
不管当前内存空间足够与否,都可能会回收它的内存。由于垃圾回收器是一个优先级很低的
线程 ,因此不一 定会很快回收弱引用的对象 。 弱引用也可以和 一 个引用队列 (ReferenceQueue
)联合使用,如果弱引用所引用的对象被垃圾回收,
Java
虚拟机就会把
这个弱引用加入到与之关联的引用队列中。

弱引用同样可应用于内存敏感的缓存。
4.3.4 虚引用:
虚引用也叫幻象引用,通过
PhantomReference
类对象来实现。无法通过虚引用访问对象
的任何属性或函数。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时
候都可能被垃圾回收器回收。虚引用必须和引用队列 (
ReferenceQueue
)联合使用。当垃
圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这
个虚引用加入到与之关联的引用队列中。

因为
PhantomReference
引用的是需要被垃圾回收的对象,所以在类的定义中,
get
一直
都是返回
null
。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾
回收。虚引用可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收
集器回收之前会收到一条系统通知。
4.4 缓存应用增强分析
4.4.1 为什么要使用缓存?
降低数据库的访问压力,提高数据的查询效率,改善用户体验。
4.4.2 你了解哪些缓存?
1)
数据库内置的缓存?
(
例如
mysql
的查询缓存
)
2)
数据层缓存
(
一般由持久层框架提供,例如
MyBatis)
3)
业务层缓存
(
基于
map
等实现的本缓存,分布式缓存
-
例如
redis)
4) CPU
缓存
(
高速缓冲区
)
5)
浏览器内置缓存?
4.4.3 设计缓存时你应该考虑哪些问题?
1)
存储结构
(
使用什么结构存储数据效率会更高
?-
散列表
)
2)
淘汰算法
(
缓存容量有限
-LRU/FIFO/LFU)
3)
任务调度
(
定期刷新缓存,缓存失效时间
)
4)
并发安全
(
缓存并发访问时的线程安全
)
5)
日志记录
(
缓存是否命中,命中率是多少
)
6)
序列化
(
存对象时序列化、取对象时反序列化
)