JVM启动流程
JVM基本结构
PC寄存器
- 每个线程拥有一个PC寄存器
- 在线程创建时创建
- 指向下一条指令的地址
- 执行本地方法时,PC的值为undefined
方法区
保存装载的类信息
- 类型的常量池(JDK6时,String等常量池置于方法,JDK7时,已经移动到了堆)
- 字段、方法信息
- 方法字节码
通常和永久区(Perm)关联在一起
Java堆
- 和程序开发密切相关
- 应用系统对象都保存在Java堆中
- 所有线程共享Java堆
- 对分代GC来说,堆也是分代的
GC的主要工作区间
Java栈
- 线程私有
- 栈由一系列帧组成(因此Java栈也叫做帧栈)
- 帧保存一个方法的局部变量、操作数栈、常量池指针
- 每一次方法调用创建一个帧,并压栈
Java栈——局部变量表 包含参数和局部变量
public static int runStatic(int i, long l, float f, Object o, byte b) {
return 0;
}
静态方法的局部变量表如下图所示
public int runInstance(char c, short s, boolean b) {
return 0;
}
实例方法的局部变量表如下图所示
Java栈——函数调用组成帧栈
public static int runStatic(int i, long l, float f, Object o, byte b) {
return runStatic(i, l, f, o, b);
}
Java栈——操作数栈
java没有寄存器,所有参数传递使用操作数栈
public static int add(int a, int b) { int c = 0; c = a + b; return c; }
对应的操作为:
0: iconst_0 // 0 压栈 1: istore_2 // 弹出int,存放于局部变量2 2: iload_0 // 把局部变量0压栈 3: iload_1 // 局部变量1压栈 4: iadd // 弹出2个变量,求和,结果压栈 5: istore_2 // 弹出结果,放于局部变量2 6: iload_2 // 局部变量2压栈 7: ireturn // 返回
Java栈——栈上分配
- 小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
- 直接分配在栈上,函数调用完成自动清理空间,减轻GC压力
- 大对象或者逃逸对象无法栈上分配
栈、堆、方法区交互
// 运行时,JVM把AppMain的信息都放入方法区
public class AppMain {
// main方法本身放入方法区
public static void main(String[] args){
// test1 是引用,所以放到栈区里,Sample是自定义对象应该放到堆里面
Sample test1 = new Sample("测试1");
Sample test2 = new Sample("测试2");
test1.printName();
test2.printName();
}
}
// 运行时,JVM把Sample的信息都放入方法区
public class Sample {
private String name;
// new Sample实例后,name引用放入栈区里,name对象放入堆里
public Sample(String name){
this.name = name;
}
// print方法本身放入方法区里
public void printName(){
System.out.println(name);
}
}
内存模型
- 每一个线程有一个工作内存和主内存
- 工作内存存放主内存中变量的值的拷贝
当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作
每一个操作都是原子的,即执行期间不会被中断
对于普通变量,一个线程中更新的值,不能马上反应在其他线程中
如果需要在其他线程中立即可见,需要使用volatile关键字
volatile
public class VolatileStopThread extends Thread {
private volatile boolean stop = false;
public void stopMe(){
stop = true;
}
@Override
public void run(){
int i = 0;
while(!stop){
i++;
}
System.out.println("Stop thread");
}
public static void main(String args[]) throws InterruptedException {
VolatileStopThread t = new VolatileStopThread();
t.start();
Thread.sleep(1000);
t.stopMe();
Thread.sleep(1000);
}
}
如果没有volatile关键字,server运行就无法停止
volatile不能代替锁,一般认为volatile比锁性能好(不绝对)
选择使用volatile的条件是:语义是否满足应用
可见性
一个线程修改了变量,其他线程可以立即知道的方法:
- volatile
- synchronized(unlock之前,写变量值回主内存)
- final(一旦初始化完成,其他线程就可见)
有序性
- 在本线程内,操作都是有序的
- 在线程外观察,操作都是无序的(指令重排或主内存同步延时)
指令重排
线程内串行语义
- 写后读 a = 1; b = a; 写一个变量之后,再读这个位置
- 写后写 a = 1; a = 2; 写一个变量之后,再写这个变量
- 读后写 a = b; b = 1; 读一个变量之后,再写这个变量
- 以上语句不可重排
- 编译器不考虑多线程间的语义
- 可重排: a = 1; b = 2;
指令重排——破坏线程间的有序性
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中,两句话顺序可能打乱
指令重排——保证有序性的方法
class OrderExample { int a = 0; boolean flag = false; public synchronized void writer(){ a = 1; flag = true; } public synchronized void reader(){ if(flag){ int i = a + 1; ... } } }
同步后,即使做了writer重排,因为互斥的缘故,reader线程看writer线程也是顺序执行的
指令重排的基本原则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C,那么A必然先于C
- 线程的start方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()方法
解释运行
- 解释执行以解释方式运行字节码
- 解释执行的意思是:读一句执行一句
编译运行(JIT)
- 将字节码编译成机器码
- 直接执行机器码
- 运行时编译
- 编译后性能有数量级的提升
常用JVM配置参数
Trace跟踪参数
-verbose:gc
输出虚拟机中GC的详细情况
使用后输出如下:
[Full GC 168K->97K(1984K), 0.0253873 secs]
168K和97K分别表示垃圾收集GC前后所有存活对象使用的内存容量,数据1984K为堆内存的总容量,收集所需要的时间是0.0253873秒
-XX:+PrintGC
同-verbose:gc
-XX:+PrintGCDetails
打印GC详细信息
-XX:+PrintGCTimeStamps
[GC[DefNew: 4416K->0K(4928K), 0.0001897 secs] 4790K->374K(15872K), 0.0002232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-Xloggc:log/gc.log
- 指定GC log的位置,以文件输出
- 帮助开发人员分析问题
-XX:+PrintHeapAtGC
- 每次一次GC后,都打印堆信息
-XX:+TraceClassLoading
- 监控类的加载
-XX:+PrintClassHistogram
- 按下Ctrl+Break后,打印类的信息
- 分别显示:序号、实例数量、总大小、类型
堆分配参数
-Xmx -Xms
- 指定最大堆和最小堆
-Xmx20m -Xms5m
System.out.print("Xmx = "); System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); System.out.print("free mem = "); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("total mem = "); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); // 结果为 Xmx = 19.375M free mem = 4.342750549316406M total mem = 4.875M
- Java会尽可能维持在最小堆
-Xmn
- 设置新生代大小(官方推荐新生代占堆的3/8)
-XX:NewRatio
- 新生代(eden + 2 * s)和老年代(不包含永久区)的比值
- 4表示新生代:老年代=1:4,即新生代占堆的1/5
-XX:SurvivorRatio
- 设置两个Survivor区和eden的比
- 8表示两个Survivor:eden=2:8,即一个Survivor占新生代的1/10(官方推荐)
如下例
public static void main(String[] args){ byte[] b = null; for(int i = 0; i < 10; i++){ b = new byte[1 * 1024 * 1024]; } }
如果设置
-Xmx20m -Xms20m -Xmn1m -XX:+PrintGCDetails
则没有触发GC,数据全部分配在老年代
如果设置
-Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails
则没有触发GC,数据全部分配在eden,老年代没有使用
如果设置
-Xmx20m -Xms20m -Xmn7m -XX:+PrintGCDetails
则进行了2次新生代GC,s0 s1太小需要老年代担保
如果设置
-Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails
则进行了3次新生代GC,s0 s1增大
如果设置
-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:+PrintGCDetails
则进行了2次新生代GC,新生代空间增大
如果设置
-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=3 -XX:+PrintGCDetails
则进行了1次新生代GC,新生代空间增大,s0 s1增大
-XX:+HeapDumpOnOutOfMemoryError
- OOM时导出堆到文件
-XX:+HeapDumpPath
- 导出OOM的路径
示例:
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpPath Vector v = new Vector(); for(int i = 0; i < 25; i++){ v.add(new byte[1 * 1024 * 1024]); }
-XX:OnOutOfMemoryError
- 在OOM时,执行一个脚本
“-XX:OnOutOfMemoryError=D:/tools/printstack.bat %p”,printstack.bat的内容为
D:/tools/jdk1.7_40/bin/jstack -F %1 > D:/a.txt
- 当程序OOM时,在D:/a.txt中会生成线程的dump
- 可以在OOM时,发送邮件,甚至是重启程序
永久区分配参数
-XX:PermSize -XX:MaxPermSize
- 设置永久区的初始空间和最大空间
- 他们表示,一个系统可以容纳多少个类型
使用CGLIB等库的时候,可能会产生大量的类,这些类,有可能撑爆永久区导致OOM
for(int i = 0; i < 100000; i++){
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean" + i, new HashMap()); // 不断地产生新的类
}
栈大小分配
-Xss
- 通常只有几百K
- 决定了函数调用的深度
- 每个线程都有独立的栈空间
- 局部变量、参数分配在栈上
如下例
public class TestStackDeep {
private static int count = 0;
public static void recursion(long a, long b, long c) {
long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10;
count++;
recursion(a, b, c);
}
public static void main(String args[]) {
try {
recursion(0L, 0L, 0L);
} catch (Throwable e) {
System.out.println("deep of calling = " + count);
e.printStackTrace();
}
}
}
设置-Xss128K,抛出java.lang.StackOverflowError时,deep of calling = 292
设置-Xss256K,抛出java.lang.StackOverflowError时,deep of calling = 1080
JIT及其相关参数
- 字节码执行性能较差,所以可以对于热点代码编译成机器码再执行,在运行时的编译,叫做JIT Just-In-Time
- JIT的基本思路是,将热点代码,就是执行比较频繁的代码,编译成机器码
相关参数
Xint
- 解释执行
Xcomp
- 全部编译执行
Xmixed
- 默认,混合
GC算法与种类
引用计数法
- 老牌垃圾回收算法
- 通过引用计算来回收垃圾
使用者
- COM
- ActionScript
- Python
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能被再被使用
引用计数法的问题
- 引用和去引用伴随着加法和减法,影响性能
- 很难处理循环引用
标记-清除
标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法是将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根结点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象,然后,在清除阶段,清除所有未被标记的对象
标记-压缩
标记-压缩算法适合用于存活对象较多的场合,如老年代,它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端,之后,清理边界外所有的空间
复制算法
- 与标记-清除算法相比,复制算法是一种相对高效的回收方法
- 不适用于存活对象较多的场合,如老年代
- 将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
分代思想
- 依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代
根据不同代的特点,选取合适的收集算法
- 少量对象存活,适合复制算法
- 大量对象存活,适合标记清理或者标记压缩
GC算法总结
引用计数
- 没有被Java采用
标记-清除
- 标记-压缩
复制算法
- 新生代
所有的算法,需要能够识别一个垃圾对象,因此需要给出一个可触及性的定义
可触及性
可触及的
从根节点可以触及到这个对象
根节点包括
- 栈中引用的对象
- 方法区中静态成员或者常量引用的对象(全局对象)
- JNI方法栈中引用对象
可复活的
- 一旦所有引用被释放,就是可复活状态
- 因为在finalize()中可能复活该对象
不可触及的
- 在finalize()后,可能会进入不可触及状态
- 不可触及的对象不可能复活
可以回收
public class CanReliveObj { public static CanReliveObj 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"; } 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 可用"); } } } // 输出结果为: CanReliveObj finalize called obj 可用 第二次gc obj 是 null
应该尽量避免使用finalize(),操作不慎可能导致错误,因为它的优先级低,何时被调用不确定,何时发生GC也不确定,可以使用try-catch-finally来替代它
Stop-The-World
Stop-The-World
- java中一种全局暂停的现象
- 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
多半由于GC引起
- Dump线程
- 死锁检查
- 堆Dump
GC时为什么会有全局停顿
- 类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净
危害
- 长时间服务停止,没有响应
- 遇到HA系统,可能引起主备切换,严重危害生产环境
GC参数
串行收集器
- 最古老,最稳定
- 效率高
- 可能会产生较长的停顿
-XX:+UseSerialGC
- 新生代、老年代使用串行回收
- 新生代复制算法
- 老年代标记-压缩
并行收集器
ParNew
-XX:+UseParNewGC
- 新生代并行
- 老年代串行
- Serial收集器新生代的并行版本
- 复制算法
- 多线程,需要多核支持
- -XX:ParallelGCThreads 限制线程数量
Parallel收集器
- 类似ParNew
- 新生代复制算法
- 老年代标记-压缩
- 更加关注吞吐量
-XX:+UseParallelGC
- 使用Parallel收集器+老年代串行
-XX:+UseParallelOldGC
- 使用Parallel收集器+老年代并行
-XX:MaxGCPauseMills
- 最大停顿时间,单位毫秒
- GC尽力保证回收时间不超过设定值
-XX:GCTimeRatio
- 0-100的取值范围
- 垃圾收集时间占总时间的比
- 默认99,即最大允许1%时间做GC
XX:MaxGCPauseMills和XX:GCTimeRatio,这两个参数是矛盾的,因为停顿时间和吞吐量不可能同时调优
CMS收集器
CMS收集器
- Concurrent Mark Sweep 并发(与用户线程一起执行)标记清除
- 标记-清除算法
- 并发阶段会降低吞吐量
- 老年代收集器(新生代使用ParNew)
- -XX:+UseConcMarkSweepGC
CMS运行过程比较复杂,着重实现了标记过程,可分为
初始标记
- 根可以直接关联到的对象
- 速度快
并发标记(和用户线程一起)
- 主要标记过程,标记全部对象
重新标记
- 由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
并发清除(和用户线程一起)
- 基于标记结果,直接清理对象
特点
- 尽可能降低停顿
会影响系统整体吞吐量和性能
- 比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
清理不彻底
- 因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
因为和用户线程一起运行,不能在空间快满时再清理
- -XX:CMSInitiatingOccupancyFraction设置触发GC的阀值
- 如果不幸内存预留空间不够,就会引起concurrent mode failure
-XX:+UseCMSCompactAtFullCollection Full GC后,进行一次整理
- 整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction
- 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads
- 设定CMS的线程数量
-XX:CMSInitiatingPermOccupancyFraction
- 当永久区占用率达到这一百分比时,启动CMS回收
XX:UseCMSInitiatingOccupancyOnly
- 表示只在到达阀值的时候,才进行CMS回收
类装载器
class装载验证流程
加载
- 取得类的二进制流
- 转为方法区数据结构
- 在Java堆中生成对应的java.lang.Class对象
链接
验证
目的:保证Class流的格式是正确的
文件格式的验证
- 是否以0xCAFEBABE开头
- 版本号是否合理
元数据验证
- 是否有父类
- 是否继承了final类
- 非抽象类是否实现了所有的抽象方法
字节码验证(很复杂)
- 运行检查
- 栈数据类型和操作码数据参数是否吻合
- 跳转指令是否指定到合理的位置
准备
分配内存,并为类设置初始值(方法区中)
- public static int v = 1;
- 在准备阶段中,v会被设置为0
- 在初始化的<clinit>中才会被设置为1
- 对于static final类型,在准备阶段就会被赋上正确的值
解析
- 符合引用(字符串)替换为直接引用(指针或者地址偏移量)
初始化
执行类构造器<clinit>
- static变量赋值语句
- static{}语句
子类的<clinit>调用前保证父类的<clinit>被调用
- <clinit>是线程安全的
什么是类装载器ClassLoader
- ClassLoader是一个抽象类
- ClassLoader的实例将读入Java字节码将类装载到JVM中
- ClassLoader可以定制,满足不同的字节码流获取方式
- ClassLoader负责类装载过程中的加载阶段
ClassLoader的重要方法
public Class <?> loadClass(String name) throws ClassNotFoundException
- 载入并返回一个Class
protected final Class<?> defineClass(byte[] b, int off, int len)
- 定义一个类,不公开调用
protected Class<?> findClass(String name) throws ClassNotFoundException
- loadClass回调该方法,自定义ClassLoader的推荐做法
protected final Class<?> findLoadedClass(String name)
- 寻找已经加载的类
ClassLoader的分类
- BootStrap ClassLoader(启动ClassLoader)
- Extension ClassLoader(扩展ClassLoader)
- App ClassLoader(应用ClassLoader/系统ClassLoader)
- Custom ClassLoader(自定义ClassLoader)
ClassLoader的协同工作
锁
对象头Mark
- Mark Word,对象头的标记,32位
描述对象的hash、锁信息,垃圾回收标记,年龄
- 指向锁记录的指针
- 指向monitor的指针
- GC标记
- 偏向锁线程ID
偏向锁
- 大部分情况是没有竞争的,所以可以通过偏向来提高性能
- 所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
- 将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
- 只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
- 当其他线程请求相同的锁时,偏向模式结束
-XX:+UseBiasedLocking
- 默认启用
在竞争激烈的场合,偏向锁会增加系统负担
轻量级锁
- 普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法
如果对象没有被锁定
- 将对象头的Mark指针保存到锁对象中
- 将对象头设置为指向锁的指针(在线程栈空间中)
- 如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)
- 在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗
- 在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降
自旋锁
- 当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋)
- JDK1.6中-XX:+UseSpinning开启
- JDK1.7中,去掉此参数,改为内置实现
- 如果同步块很长,自旋失败,会降低系统性能
- 如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能
JVM中获取锁的步骤
- 偏向锁可用会尝试偏向锁
- 轻量级锁可用会先尝试轻量级锁
- 以上都失败,尝试自旋锁
- 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起
锁优化方法
减少锁持有时间
public synchronized void syncMethod(){ othercode1(); mutextMethod(); othercode2(); } => public void syncMethod2(){ othercode1() synchronized(this){ mutextMethod(); } othercode2(); }
减小锁粒度
- 将大对象,拆成小对象,大大增加并行度,降低锁竞争
- 偏向锁,轻量级锁成功率提高
ConcurrentHashMap
- 若干个Segment:Segment<K,V>[] segments
- Segment中维护HashEntry<K,V>
- put操作时,先定位到Segment,锁定一个Segment,执行input
- 在减小锁粒度后,ConcurrentHashMap允许若干个线程同时进入
锁分离
- 根据功能进行锁分离
- ReadWriteLock
- 读多写少的情况,可以提高性能
锁粗化
如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化
public void demoMethod(){ synchronized(lock){ // do sth } // 做其他不需要同步的工作,但能很快执行完毕 synchronized(lock){ // do sth } } => public void demoMethod(){ // 整合成一次锁请求 synchronized(lock){ // do sth // 做其他不需要同步的工作,但能很快执行完毕 // do sth } }
锁消除
- 在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作
无锁
- 锁是悲观的操作
- 无锁是乐观的操作
无锁是一种实现方式
- CAS(Compare And Swap)
- 非阻塞的同步
- CAS(V,E,N)
在应用层面判断多线程的干扰,如果有干扰,则通知线程重试