本文主要整理自炼术成金JVM教学资料和《深入理解Java虚拟机》,部分资料整理自网络,已不明来源
一. JVM规范
1.1 位运算
1.1.1 整型int
- 原码:第一位符号位,0为正,1为负
- 反码:符号位不动,原码取反
- 补码
- 正数补码:和源码相同
- 负数补码:符号位不动,反码加1
example
-6 原码: 10000110
反码: 11111001
补码: 11111010
- 为何使用补码
- 可以无歧义地表示0
不使用补码,将0看为
正数:0000 0000
负数:1000 0000
则不一致
使用补码:
负数:1000 0000 反码:1111 111 补码:0000 0000 = 正数
正数和负数使用补码做运算相当于用加法做运算
计算时都是使用补码进行计算
1.1.2 单精度Float
- 表示方式
当指数位
- 全为0,尾数附加位为0
- 不全为0,则尾数附加位为1
如此,尾数位就凑足了24位
计算方式 S\*M\*2^(e-127)
eg: -5的单精度表示 1 10000001 01000000000000000000000
其符号位 S为1,表示负数 -1
指数位E:10000001 ,e =129
尾数附加位:指数位不全为0,则为1
尾数M: 1+2^-2
;(-2,尾数位由右往左数第二位)
结果:-1 * ( 1+2^-2) * 2^( 129 - 127) = -5
二.JVM运行机制
2.1 JVM启动流程
2.2 JVM基本结构
方法区物理上存在于堆里,而且是在堆的持久代里面;但在逻辑上,方法区和堆是独立的
方法区method area
只是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。而永久代是Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西
java 8和java 7的某版本后,perm gen 被去除了,取而代之的是metaspace。
不同点在于:perm gen 含class metadata、class static variables和interned string
metaspace只含class metadata了,class static variables和interned string被移到java heap上去了(所以java heap使用肯定要大一点)
JVM主要管理两种类型的内存:堆和非堆.
简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的
所以方法区,JVM内部处理或优化所需的内存(如JIT编译后的代码缓存),每个类结构(如运行时常数池,字段和方法数据)以及方法和构造方法的代码都在非堆内存中.
2.2.1 PC寄存器
- 每一个线程拥有一个PC寄存器
- 在线程创建时创建
- 指向下一条指令
- 执行本地方法时,PC值为undefined ?
2.2.2 方法区
- 保存装载的类信息:字段、方法信息、方法字节码
- 通常和永久区(perm)关联在一起
2.2.3 Java堆
- 对象保存在堆中
- 所有线程共享java堆
- GC工作空间
2.2.4 Java栈
- 线程私有
- 栈由一系列帧组成(故也叫帧栈)
- 帧保存每个方法的局部变量表,操作数栈,常量池指针,程序计数器
- 每一次方法调用创建一个帧,并压栈
帧中有局部变量表
操作数栈
Java没有寄存器,所有参数传递使用操作数栈
栈上分配空间- 小对象(几十bytes),在没有逃逸的情况下,可以直接分配在栈上
- 直接分配在栈上,可以自动回收,减轻GC压力
- 大对象或逃逸对象无法在栈上分配
逃逸对象:栈内对象被外部对象引用,其作用范围脱离了当前方法栈
public class AppMain {
//运行时, jvm 把appmain的信息都放入方法区
public static void main(String[] args) {
//main 方法本身放入方法区。
Sample test1 = new Sample( " 测试1 " );
//test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面
Sample test2 = new Sample( " 测试2 " );
test1.printName();
test2.printName();
}
}
public class Sample {
//运行时, jvm 把appmain的信息都放入方法区
private name;
//new Sample实例后, name 引用放入栈区里, name 对象放入堆里
public Sample(String name) {
this .name = name;
}
//print方法本身放入 方法区里
public void printName() {
System.out.println(name);
}
}
三.内存模型
每一个线程有一个工作内存和主存独立
工作内存存放主存中变量的值和拷贝
对于普通变量,一个线程中更新的值,不能马上反应在其他变量中
如果需要在其他线程中立即可见,需要使用 volatile 关键字
3.1 内存模型特性
- 可见性:一个线程修改了变量,其他线程可以立即知道
- 保证可见性的方法:
- volatile
- synchronized(unlock之前,写变量值回主存)
- final(一旦初始化完成,其他线程就可见)
- 有序性
在本线程内,操作是有序的
在线程外观察,操作是无序的(指令重排 或 主内存与线程内存同步延期) - 指令重排
为了提高程序运行效率,调整指令执行次序
与写相邻的指令不可重排:读后写,写后读,写后写
编译器不考虑多线程间的语义 - 指令重排 – 破坏线程间的有序性
class OrderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
int i = a +1;
……
}
}
}
线程A首先执行writer()
方法
线程B线程接着执行reader()
方法
线程B在int i=a+1
是不一定能看到a已经被赋值为1
在writer中,两句话顺序可能打乱
- 指令重排 – 保证有序性的方法
对方法加上同步关键字synchronized - 指令重排的基本原则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C 那么A必然先于C
- 线程的start方法先于它的每一个动作和/方法
- 线程的所有操作先于线程的终结
Thread.join()
,最后才终结 - 线程的中断
interrupt()
先于被中断线程的代码,中断立即停止 - 对象的构造函数执行结束先于
finalize()
方法
3.2 常用JVM参数配置
- Tract跟踪参数
-XX:+TraceClassLoading
:监控类的加载-XX:+PrintClassHistogram
: 按下Ctrl+Break后,打印类的信息 - 堆的分配参数
XX:+HeapDumpOnOutOfMemoryError
:OOM时导出堆到文件-XX:OnOutOfMemoryError
: 在OOM时,执行一个脚本
官方推荐新生代占堆的3/8
幸存代占新生代的1/10 - 栈的分配参数
Xss- 通常只有几百K
- 决定了函数调用的深度
- 每个线程都有独立的栈空间
- 局部变量、参数 分配在栈上
四.GC的算法与种类
4.1 GC算法
- 引用计数法:java中未使用
- 标记清除:老年代
- 标记压缩:老年代
- 复制算法:新生代
- 分代思想
- 依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代
- 根据不同代的特点,选取合适的收集算法
- 少量对象存活,适合复制算法
- 大量对象存活,适合标记清理或者标记压缩
4.2 可触及性
-
可触及的
- 从根节点可以触及到这个对象
- 根:(与方法区栈相关)
- 栈中引用的对象
- 方法区中静态成员或者常量引用的对象(全局对象)
- JNI方法栈中引用对象
-
可复活的
- 一旦所有引用被释放,就是可复活状态,即不可达
- 但在finalize()中可能复活该对象
-
不可触及的
- 在finalize()后,可能会进入不可触及状态
- 不可触及的对象不可能复活
- 可以回收
public class CanReliveObj {
public static CanReliveObj obj;
public static void main(String[] args) throws InterruptedException{
obj=new CanReliveObj();
obj=null; //可复活
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj 是 null");
}else{
System.out.println("obj 可用");
}
System.out.println("第二次gc");
obj=null; //不可复活
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj 是 null");
}else{
System.out.println("obj 可用");
}
}
@Override
//重写析构方法
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanReliveObj finalize called");
obj=this;
}
@Override
public String toString(){
return "I am CanReliveObj";
}
}
- 避免使用finalize方法
- 对象中只能调用一次,操作不慎可能导致错误
- 优先级低,何时被调用不确定,何时发生GC不确定
- 可以使用try-catch-finally来替代
对于用可达性分析法搜索不到的对象,GC并不一定会回收该对象。要完全回收一个对象,至少需要经过两次标记的过程。
第一次标记:对于一个没有其他引用的对象,筛选该对象是否有必要执行finalize()方法,如果没有执行必要,则意味可直接回收。(筛选依据:是否复写或执行过finalize()方法;因为finalize方法只能被执行一次)。
第二次标记:如果被筛选判定位有必要执行,则会放入FQueue队列,并自动创建一个低优先级的finalize线程来执行释放操作。如果在一个对象释放前被其他对象引用,则该对象会被移除FQueue队列
4.3 Stop-The-World
Java中一种全局暂停的现象
全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
多半由于GC引起,也可以是Dump线程、死锁检查、堆Dump
4.4 串行搜集器
- 最古老,最稳定
- 效率高
- 可能会产生较长的停顿
- 适用于数据量较小,对响应时间无要求的小型应用
- -XX:+UseSerialGC
- 新生代、老年代使用串行回收
- 新生代复制算法
- 老年代标记-压缩
4.5 并行收集器
适用于对吞吐量有较高要求, 多CPU、对应用响应时间无要求的中、大型应用。
举例:后台处理、科学计算 吞吐量:=运行用户代码时间/(运行用户代码时间+GC时间)
- 并发Concurrent:交替做不同事的能力,用户程序可以不暂停,不一定并行,但可以交替执行
- 并行Parallel:同时做不同事的能力,垃圾回收线程并行工作,但应用程序等待暂停
4.5.1 ParNew
-
-XX:+UseParNewGC
- 新生代并行
- 老年代串行
- Serial收集器新生代的并行版本
- 复制算法
- 多线程,需要多核支持
-
-XX:ParallelGCThreads
限制线程数量
0.834: [GC 0.834: [ParNew: 13184K->1600K(14784K), 0.0092203 secs] 13184K->1921K(63936K), 0.0093401 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
4.5.2 Parallel收集器(可自定义的、灵活)
- 类似ParNew
- 新生代复制算法
- 老年代 标记-压缩
- 更加关注吞吐量
-
-XX:+UseAdaptiveSizePolicy
自适应调节策略是Parallel与ParNew的重要区别 -
-XX:+UseParallelGC
- 新生代使用Parallel收集器+ 老年代串行
-
-XX:+UseParallelOldGC
- 新生代使用Parallel收集器+ 老年代并行
老年代不一样而已
1.500: [Full GC [PSYounhttps://user-gold-cdn.xitu.io/2017/12/3/1601bd5a57d6924fen: 2682K->0K(19136K)] [ParOldGen: 28035K->30437K(43712K)] 30717K->30437K(62848K) [PSPermGen: 10943K->10928K(32768K)], 0.2902791 secs] [Times: user=1.44 sys=0.03, real=0.30 secs]
- 特殊参数
-
-XX:MaxGCPauseMills
- 最大停顿时间,单位毫秒
- GC尽力保证回收时间不超过设定值
-
-XX:GCTimeRatio
- 0-100的取值范围
- 垃圾收集时间占总时间的比
- 默认99,即最大允许1%时间做GC
- 这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优
-
4.6 CMS并发收集器
适用于对响应时间有高要求,多CPU、对应用响应时间有较高要求的中、大型应用。
举例:Web服务器/应用服务器、电信交换、集成开发环境
- 特性
Concurrent Mark Sweep 并发标记清除(与用户线程一起执行 )
标记-清除算法(不是标记压缩)
并发阶段会降低吞吐量(?)
只是针对老年代收集器(新生代使用ParNew/或串行)-XX:+UseConcMarkSweepGC
-
运行过程
- 初始标记
根可以直接关联到的对象
速度快
独占CPU,全局停顿 - 并发标记(和用户线程一起)
标记的主要过程,标记全部对象 - 重新标记
重新修正标记
独占CPU,全局停顿 - 并发清除(和用户线程一起)
基于标记结果,直接清除对象
- 初始标记
优:
尽可能降低停顿,在并发标记过程中并不需要全局停顿-
劣:
- 会影响系统整体吞吐量和性能
- 比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
- 清理不彻底
- 在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理,因为和用户线程一起运行,不能在空间快满时再清理
-
-XX:CMSInitiatingOccupancyFraction
设置触发GC的阈值 - 如果不幸内存预留空间不够,就会引起concurrent mode failure,此时应该使用串行收集器作为后备,由于空间不足,此时一般停顿时间较长
- 会影响系统整体吞吐量和性能
- 碎片清理问题
CMS使用的是标记-清除算法,在清除后堆内存有效对象地址不连续,有内存碎片存在,故可设置内存压缩,整理内存碎片
即CMS为了性能考虑在老年代使用标记-清除算法,但仍可以设置使用标记-压缩算法-
-XX:+ UseCMSCompactAtFullCollection
Full GC后,进行一次整理- 整理过程是独占的,会引起停顿时间变长
-
-XX:+CMSFullGCsBeforeCompaction
- 设置进行几次Full GC后,进行一次碎片整理
-
-XX:ParallelCMSThreads
- 设定CMS的线程数量,一般大约设成cpu核数,默认定义为(CPU数量+3)/4,即至少25%
-
4.7 GC参数整理
4.7.1 内存分配
参数名称 | 含义 | 备注 |
---|---|---|
-Xms | 初始堆大小 | 默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制 |
-Xmx | 最大堆大小 | 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 |
-Xmn | 年轻代大小 | eden+ 2 survivor space,增大年轻代后,将会减小年老代大小,Sun官方推荐配置为整个堆的3/8 |
-XX:PermSize | 设置持久代(perm gen)初始值 | 持久代是方法区的一种实现 |
-XX:MaxPermSize | 设置持久代最大值 | |
-Xss | 每个线程的栈大小 | JDK5.0以后每个线程堆栈大小为1M,栈越大,线程越少,栈深度越深 |
-XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) | -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5,Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 |
-XX:SurvivorRatio | Eden区与Survivor区的大小比值 | 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 |
-XX:MaxTenuringThreshold | 垃圾最大年龄 | 该参数只有在串行GC时才有效 |
-XX:PretenureSizeThreshold | 对象超过多大是直接在旧生代分配 | 单位字节 新生代采用Parallel Scavenge GC时无效, 另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. |
4.7.2 并行收集器相关参数
参数名称 | 含义 | 备注 |
---|---|---|
-XX:+UseParallelGC | 新生代使用Parallel收集器+ 老年代串行 | |
-XX:+UseParNewGC | 在新生代使用并行收集器 | |
-XX:ParallelGCThreads | 并行收集器的线程数 | 此值最好配置与处理器数目相等 也适用于CMS |
-XX:+UseParallelOldGC | 新生代使用Parallel收集器+ 老年代并行 | |
-XX:MaxGCPauseMillis | 每次年轻代垃圾回收的最长时间(最大暂停时间) | 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值 |
-XX:+UseAdaptiveSizePolicy | 自动选择年轻代区大小和相应的Survivor区比例 | 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开. |
4.7.3 CMS并发相关参数
参数名称 | 含义 | 备注 |
---|---|---|
-XX:+UseConcMarkSweepGC | 使用CMS内存收集 | 新生代使用并行收集器ParNew,老年代使用CMS+串行收集器 |
-XX:CMSFullGCsBeforeCompaction | 多少次后进行内存压缩 | 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生”碎片”,使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理 |
-XX+UseCMSCompactAtFullCollection | 在FULL GC的时候, 对年老代的压缩 | CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片 |
-XX:CMSInitiatingPermOccupancyFraction | 当永久区占用率达到这一百分比时,启动CMS回收 |
4.7.4 辅助信息
参数名称 | 含义 |
---|---|
-XX:+PrintGC | |
-XX:+PrintGCDetails | |
-XX:+PrintGCTimeStamps | |
-XX:+PrintGCApplicationStoppedTime | 打印垃圾回收期间程序暂停的时间.可与上面混合使用 |
-XX:+PrintHeapAtGC | 打印GC前后的详细堆栈信息 |
-Xlohttps://user-gold-cdn.xitu.io/2017/12/3/1601bd5a57d6924fc:filename | 把相关日志信息记录到文件以便分析 |
-XX:+HeapDumpOnOutOfMemoryError | |
-XX:HeapDumpPath | |
-XX:+PrintCommandLineFlags | 打印出已经被设置过的详细的 XX 参数的名称和值 |
4.8 调优总结
项目 | 响应时间优先 | 吞吐量优先 |
---|---|---|
年轻代 | -Xmn尽量大,直到接近系统的最低响应时间限制-XX:MaxGCPauseMillis,减少年轻代GC,减少到达老年代对象 | -Xmn尽量大 |
年轻代垃圾回收器 | 并发收集器 | 并行收集器 |
年老代 | 如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间 | |
要参照年轻代和年老代垃圾回收时间与次数 | -XX:NewRatio 年老代设置小一些,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象 | |
年老代垃圾回收器 | 年老代使用并发收集器 | 因为对响应时间没有要求,垃圾收集可以并行进行,也可以串行 |
典型配置
- 吞吐量优先的并行收集器
并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等
年轻代都使用并行收集器,老年代没要求
年轻代使用并行收集,而年老代仍旧使用串行收集
-Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
年老代并行
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
- 响应时间优先的并发收集器
并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
:设置年老代为并发收集 -XX:+UseParNewGC
:设置年轻代为并行收集
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction
:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection
:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
4.9 GC日志
5.617: [GC 5.617: [ParNew: 43296K->7006K(47808K), 0.0136826 secs] 44992K->8702K(252608K), 0.0137904 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
解释
5.617(时间戳): [GC(Young GC) 5.617(时间戳): [ParNew(使用ParNew作为年轻代的垃圾回收器): 43296K(年轻代垃圾回收前的大小)->7006K(年轻代垃圾回收以后的大小)(47808K)(年轻代的总大小), 0.0136826 secs(回收时间)] 44992K(堆区垃圾回收前的大小)->8702K(堆区垃圾回收后的大小)(252608K)(堆区总大小), 0.0137904 secs(回收时间)] [Times: user=0.03(Young GC用户耗时) sys=0.00(Young GC系统耗时), real=0.02 secs(Young GC实际耗时)]
[GC [DefNew: 3468K->150K(9216K), 0.0028638 secs][Tenured:
1562K->1712K(10240K), 0.0084220 secs] 3468K->1712K(19456K),
[Perm : 377K->377K(12288K)],
0.0113816 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Tenured:持久代/老年代
串行收集器:
DefNew:使用-XX:+UseSerialGC
(新生代,老年代都使用串行回收收集器)。
并行收集器:
ParNew:是使用-XX:+UseParNewGC
(新生代使用并行收集器,老年代使用串行回收收集器)或者-XX:+UseConcMarkSweepGC
(新生代使用并行收集器,老年代使用CMS)。
PSYounhttps://user-gold-cdn.xitu.io/2017/12/3/1601bd5a57d6924fen:是使用-XX:+UseParallelOldGC
(新生代,老年代都使用并行回收收集器)或者-XX:+UseParallelGC
(新生代使用并行回收收集器,老年代使用串行收集器)
garbage-first heap:是使用-XX:+UseG1GC
(G1收集器)
4.10 GC触发条件
触发条件就是某GC算法对应区域满了,或是预测快满了(比如该区使用比例达到一定比例-对并行/并发,或不够晋升)
4.10.1 GC分类
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
- Partial GC:并不收集整个GC堆的模式
Young GC:只收集young gen的GC
Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式 - Full GC
收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。收集是整体收集的,无所谓先收集old还是young。marking是整体一起做的,然后compaction(压缩)是old gen先来然后再young gen来
4.10.2 HotSpot VM的serial GC
Major GC通常是跟full GC是等价的,收集整个GC堆。最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:
- young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
- full GC
- 当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);
- 如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;
- System.gc()、heap dump带GC,默认也是触发full GC。
4.10.3 HotSpot VM非并发GC(Parallel GC)
触发条件复杂一些,不过大致的原理与串行GC一样。
例外: Parallel Scavenge(-XX:+UseParallelGC
新生代使用Parallel收集器)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。控制这个行为的VM参数是-XX:+ScavengeBeforeFullGC
4.10.4 HotSpot VM并发GC
并发GC的触发条件就不太一样。以CMS GC为例,主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集
-XX:CMSInitiatingOccupancyFraction=80 // old达到80%收集
或者GC过程中,由于预留的内存无法满足程序需要, 出现concurrent mode failure,临时使用serial old进行Full GC
4.10.5 HotSpot VM G1收集
G1 GC的initial marking(初始标记)的触发条件是Heap使用比率超过某值,收集时是按照回收价值的优先级,不按照young old区
G1 GC:Young GC + mixed GC(新生代,再加上部分老生代)+ Full GC for G1 GC算法(应对G1 GC算法某些时候的不赶趟,开销很大);
五. 类装载器
5.1 Class装载验证流程
5.1.1 加载
转为方法区数据结构
在Java堆中生成对应的java.lang.Class对象
- 类装载器ClassLoader
- ClassLoader是一个抽象类
- ClassLoader的实例将读入Java字节码将类装载到JVM中
- ClassLoader可以定制,满足不同的字节码流获取方式(比如网络)
tomcat和OSGi有做更改
example:类从上往下加载
在工程目录中添加A.java,自动编译生成A.class
又指定根加载目录path,-Xbootclasspath/a:path,重新放一个同名A.class
此时会加载指定根加载目录下的class文件
注意:以上是jdk默认的类加载模式,但tomcat和OSGi有自己的加载方式
Tomcat:Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent
OSGi的ClassLoader形成网状结构,根据需要*加载Class
5.1.2 链接
- 验证
目的:保证Class流的格式是正确的- 文件格式的验证
- 是否以0xCAFEBABE开头
- 版本号是否合理:class文件由什么版本jdk编译生成,与执行class的jdk是否兼容
- 元数据验证(基本信息验证)
- 是否有父类:class中指定了父类,检查父类是否存在
- 继承了final类?
- 非抽象类实现了所有的抽象方法
- 字节码验证 (复杂)
- 运行检查
- 栈数据类型和操作码数据参数吻合
- 跳转指令指定到合理的位置
- 符号引用验证
- 常量池中描述类是否存在:引用的类必须存在
- 访问的方法或字段是否存在且有足够的权限:private…
- 文件格式的验证
- 准备
-
分配内存,并为类设置初始值 (方法区中)
- public static int v=1;
- 在准备阶段中,v会被设置为0
- 在初始化的中才会被设置为1
- 对于static final类型,在准备阶段就会被赋上正确的值—在初始化之前就赋值
- public static final int v=1;
-
分配内存,并为类设置初始值 (方法区中)
- 解析
符号引用替换为直接引用:即类名应用,直接替换为内存地址指针
5.1.3 初始化
- 执行类构造器
- static变量 赋值语句 : 注意,static final 在准备阶段已经赋值了
- static{}语句
- 子类的调用前保证父类的被调用
- 是线程安全的,即单线程执行
六. 性能分析
6.1 Java自带性能分析的工具
直接在控制台输入命令,参数具体使用可使用-help 命令
6.1.1 jps
一般是第一步,方便后续其他命令调用
列出java进程,类似于ps命令
参数-q可以指定jps只输出进程ID ,不输出类的短名称
参数-m可以用于输出传递给Java进程(主函数)的参数
参数-l可以用于输出主函数的完整路径
参数-v可以显示传递给JVM的参数
6.1.2 jinfo
查看进程参数
可以用来查看正在运行的Java应用程序的扩展参数,甚至支持在运行时,修改部分参数
-flag 进程ID:打印指定JVM的参数值
-flag [+|-] 进程ID:设置指定JVM参数的布尔值
-flag = 进程ID:设置指定JVM参数的值
6.1.3 jmap
生成Java应用程序的堆快照和对象的统计信息
num #instances #bytes class name ----------------------------------------------
1: 370469 32727816 [C
2: 223476 26486384 <constMethodKlass>
3: 260199 20815920 java.lang.reflect.Method
…..
8067: 1 8 sun.reflect.GeneratedMethodAccessor35
Total 4431459 255496024
6.1.4 jstack
打印线程dump -l
打印锁信息 -m
打印java和native的帧信息 -F
强制dump,当jstack没有响应时使用
Jdk1.6版本只有 –l选项
6.1.5 JConsole
图形化监控工具
可以查看Java应用程序的运行概况,监控堆信息、永久区使用情况、类加载情况等
6.1.6 Visual VM
Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具
6.1.7 MAT
6.2 Java堆分析
- 内存溢出OOM原因
Jvm内存区间:堆、永久区、线程栈、直接内存堆+线程栈 +直接内存<= 操作系统可分配空间
- 堆溢出
占用大量堆空间,直接溢出
- 堆溢出
public static void main(String args[]){
ArrayList<byte[]> list=new ArrayList<byte[]>();
for(int i=0;i<1024;i++){
list.add(new byte[1024*1024]);
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at geym.jvm.ch8.oom.SimpleHeapOOM.main(SimpleHeapOOM.java:14)
解决方法:增大堆空间,及时释放内存,分批处理
- 永久区溢出
//生成大量的类
public static void main(String[] args) {
for(int i=0;i<100000;i++){
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
}
}
Caused by: java.lang.OutOfMemoryError: 【PermGen space】
[Full GC[Tenured: 2523K->2523K(10944K), 0.0125610 secs] 2523K->2523K(15936K),
[Perm : 【4095K->4095K(4096K)】], 0.0125868 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Heap
def new generation total 4992K, used 89K [0x28280000, 0x287e0000, 0x2d7d0000)
eden space 4480K, 2% used [0x28280000, 0x282966d0, 0x286e0000)
from space 512K, 0% used [0x286e0000, 0x286e0000, 0x28760000)
to space 512K, 0% used [0x28760000, 0x28760000, 0x287e0000)
tenured generation total 10944K, used 2523K [0x2d7d0000, 0x2e280000, 0x38280000)
the space 10944K, 23% used [0x2d7d0000, 0x2da46cf0, 0x2da46e00, 0x2e280000)
compacting perm gen total 4096K, used 4095K [0x38280000, 0x38680000, 0x38680000)
the space 4096K, 【99%】 used [0x38280000, 0x3867fff0, 0x38680000, 0x38680000)
ro space 10240K, 44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)
rw space 12288K, 52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000)
解决方法:避免动态生成class,增大Perm区,允许Class回收
- Java栈溢出
-Xmx1g -Xss1m
public static class SleepThread implements Runnable{
public void run(){
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]){
for(int i=0;i<1000;i++){
new Thread(new SleepThread(),"Thread"+i).start();
System.out.println("Thread"+i+" created");
}
}
Exception in thread "main" java.lang.OutOfMemoryError:
unable to create new native thread
这里的栈溢出指,在创建线程的时候,需要为线程分配栈空间,这个栈空间是向操作系统请求的,如果操作系统无法给出足够的空间,就会抛出OOM
eg:堆空间1G,每个线程栈空间1m
注意:堆+线程栈+直接内存 <= 操作系统可分配空间
- 直接内存溢出
ByteBuffer.allocateDirect()
:申请堆外的直接内存
直接内存也可以被GC回收-Xmx1g -XX:+PrintGCDetails
//会抛出oom,但堆内存空间充足
for(int i=0;i<1024;i++){
ByteBuffer.allocateDirect(1024*1024);
System.out.println(i);
System.gc();
}
七. 锁
7.1 线程安全
public static List<Integer> numberList =new ArrayList<Integer>();
public static class AddToList implements Runnable{
int startnum=0;
public AddToList(int startnumber){
startnum=startnumber;
}
@Override
public void run() {
int count=0;
while(count<1000000){
numberList.add(startnum);
startnum+=2;
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new AddToList(0));
Thread t2=new Thread(new AddToList(1));
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
Thread.sleep(1);
}
System.out.println(numberList.size());
}
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 73
at java.util.ArrayList.add(Unknown Source)
at simpleTest.TestSome$AddToList.run(TestSome.java:27)
at java.lang.Thread.run(Unknown Source)
1000005
ArrayList 不是线程安全的集合对象,在两个线程添加元素的过程中,当数组填满,正在自动扩展时,另一个线程却还是在添加元素,在ArrayList底层就是不可变长的数组,则抛出下表越界异常
7.2 对象头Mark
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
7.3 偏向锁
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。偏向锁只能在单线程下起作用。
偏向锁在锁对象的对象头中有个ThreadId字段,这个字段如果是空的,第一次获取锁的时候,就将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态位置1.,这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。
1. 大部分情况是没有竞争的,所以可以通过偏向来提高性能
2. 所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
3. 将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
4. 只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
5. 当其他线程请求相同的锁时,偏向模式结束,在全局安全点(在这个时间点上没有字节码正在执行)撤销偏向锁,采用其他锁
6. -XX:+UseBiasedLocking
- 默认启用
- 在竞争激烈的场合,偏向锁会增加系统负担
开启偏向锁 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
系统启动后,并不会立即开启偏向锁,而是会延迟,可以设置延迟时间为0
7.4 轻量级锁
普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法
轻量级锁是为了在线程交替执行同步块时提高性能
-
如果对象没有被锁定
将对象头的Mark指针保存到锁对象中
将对象头设置为指向锁的指针(在线程栈空间中)
即对象和锁都互相保存引用轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。
如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁 lock位于线程栈中
由上可知,判断一个线程是否持有轻量级锁,只要判断对象头的指针,是否在线程的栈空间范围内- 特性
- 如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁,操作系统,进程级)
- 在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗
- 在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降
mark word中的lock record指向堆栈最近的一个线程的lock record,其实就是按照先来后到模式进行了轻量级的加锁
7.5 自旋锁 spin lock
- 尽量减少系统级别的线程挂起
- 当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋)等待获得锁
- JDK1.6中
-XX:+UseSpinning
开启 - JDK1.7中,去掉此参数,改为内置实现
- 如果同步块很长,自旋失败,会降低系统性能—空占线程操作,最后还是要在OS层挂起,自旋锁空耗资源
- 如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能
当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程(未阻塞)可以稍微等一等(自旋),在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免线程阻塞
7.6 偏向锁vs轻量级锁vs自旋锁
- 不是Java语言层面的锁优化方法
- 内置于JVM中的获取锁的优化方法和获取锁的步骤
- 偏向锁可用会先尝试偏向锁
- 轻量级锁可用会先尝试轻量级锁
- 以上都失败,尝试自旋锁
- 再失败,尝试普通锁(重量级锁),使用OS互斥量在操作系统层挂起
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。有竞争时会比重量级锁更慢 | 追求响应时间。同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行速度较长。 |
偏向锁与轻量级锁理念上的区别:
轻量级锁:在无竞争的情况下使用CAS操作去消除同步使用的互斥量
偏向锁:在无竞争的情况下把整个同步都消除掉
连CAS操作都不做了?
7.7 Java语言层面优化锁
7.7.1 减少锁持有时间
同步范围减少
7.7.2 减小锁粒度
将大对象拆成小对象,增加并行度,降低锁竞争
偏向锁和轻量级锁成功率提高——粒度大,竞争激烈,偏向锁,轻量级锁失败概率就高
- ConcurrentHashMap
若干个Segment :Segment<K,V>[] segments
Segment中维护HashEntry<K,V>
put操作时
先定位到Segment,锁定一个Segment,执行put
在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入
7.7.3 锁分离
- 读写锁ReadWriteLock
锁类型 | 读锁 | 写锁 |
---|---|---|
读锁 | 可访问 | 不可访问 |
写锁 | 不可访问 | 不可访问 |
- LinkedBlockingQueue
只要操作互不影响,锁就可以分离
7.7.4 锁粗化
如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化
- Example1:
public void demoMethod(){
synchronized(lock){
//do sth.
}
//做其他不需要的同步的工作,但能很快执行完毕
synchronized(lock){
//do sth.
}
}
直接扩大范围
public void demoMethod(){
//整合成一次锁请求
synchronized(lock){
//do sth.
//做其他不需要的同步的工作,但能很快执行完毕
}
}
- Example2
for(int i=0;i<CIRCLE;i++){
synchronized(lock){
}
}
//锁粗化
synchronized(lock){
for(int i=0;i<CIRCLE;i++){
}
}
7.7.5 锁消除
在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作
锁不是由程序员引入的,JDK自带的一些库,可能内置锁
栈上对象,不会被全局访问的,没有必要加锁
- Example
public static void main(String args[]) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < CIRCLE; i++) {
craeteStringBuffer("JVM", "Diagnosis");
}
long bufferCost = System.currentTimeMillis() - start;
System.out.println("craeteStringBuffer: " + bufferCost + " ms");
}
public static String craeteStringBuffer(String s1, String s2) {
//StringBuffer线程安全对象,内置锁
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
- 栈上对象(方法局部变量),不会被全局访问的,没有必要加锁
7.7.6 无锁
无锁的一种实现方式 CAS(Compare And Swap)
非阻塞的同步
CAS(V,E,N):if V==E then V=N
CAS算法的过程: CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。
仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
最后,CAS返回当前V的真实值。
CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
java.util.concurrent.atomic包使用无锁实现,性能高于一般的有锁操作
7.8 线程状态及装换
当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
- Contention List:所有请求锁的线程将被首先放置到该竞争队列
- Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
- Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
- OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
- Owner:获得锁的线程称为Owner
- !Owner:释放锁的线程
那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthread_mutex_lock函数)。
线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能 - Synchronized加锁
每一个线程在准备获取共享资源时:- 检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”
- 如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
- 两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord,
- 第三步中成功执行CAS的获得资源,失败的则进入自旋
- 自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败
- 进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己
八.Class文件结构
U4:无符号整型,4个字节
类型 | 名称 | 数量 | 备注 |
---|---|---|---|
u4 | magic | 1 | 0xCAFEBABE:表示java class文件类型 |
u2 | minor_version | 1 | Jdk编译版本 |
u2 | major_version | 1 | Jdk编译版本 |
u2 | constant_pool_count | 1 | |
cp_info | constant_pool | constant_pool_count - 1 | 链式引用基本类型-被各处引用-要减1 |
u2 | access_flags | 1 | 访问修饰符&class type |
u2 | this_class | 1 | 指向常量池的class |
u2 | super_class | 1 | 指向常量池的class |
u2 | interfaces_count | 1 | |
u2 | interfaces | interfaces_count | 每个接口指向常量池CONSTANT_Class索引 |
u2 | fields_count | 1 | |
field_info | fields | fields_count | access_flags,name_index ,descriptor_index ,attributes_count,attribute_info attributes[attributes_count] |
u2 | methods_count | 1 | |
method_info | methods | methods_count | |
u2 | attribute_count | 1 | |
attribute_info | attributes | attributes_count |
九. JVM字节码执行
9.1 javap
线程帧栈中的数据:
- 程序计数器:每个线程都有一个,用于指向当前线程执行的指令地
- 局部变量表
- 操作数栈
9.2 JIT及其相关参数
- JIT Just-In-Time
字节码执行性能较差,所以可以对于热点代码(Hot Spot Code)编译成机器码再执行,在运行时的编译
当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码) - 辨别热点代码
方法调用计数器:方法调用次数
回边计数器:方法内循环次数,可以在栈上直接替换为机器码 - 编译设置
-XX:CompileThreshold=1000
:执行超过一千次即为热点代码-XX:+PrintCompilation
:打印编译为机器码的代码-Xint
:解释执行-Xcomp
:全部编译执行-Xmixed
:默认,混合