JVM相关知识总结整理
2018-02-05 16:34
567 查看
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-TimeJIT的基本思路是,将热点代码,就是执行比较频繁的代码,编译成机器码
相关参数
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-Worldjava中一种全局暂停的现象
全局停顿,所有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)
在应用层面判断多线程的干扰,如果有干扰,则通知线程重试
相关文章推荐
- JVM相关知识整理
- JVM的相关知识整理和学习
- JVM的相关知识整理和学习
- 虚拟机垃圾回收相关知识整理(JVM GC)
- JVM的相关知识整理和学习
- JVM相关知识整理
- JVM的相关知识整理和学习----转载(很好的JVM文章)
- JVM的相关知识整理和学习
- JVM的相关知识整理和学习
- JVM相关知识总结
- jvm内存相关的知识总结
- JVM的相关知识整理和学习
- JVM的相关知识整理和学习
- JVM的相关知识整理和学习
- JVM的相关知识整理和学习
- 【JVM】虚拟机相关知识整理
- 黑马程序员——Objective-C语言知识整理——构造方法相关知识总结
- JVM的相关知识整理和学习
- 2017春招 互联网名企面试问题整理即相关知识总结
- JVM 相关知识整理和学习