Java虚拟机学习笔记整理

本文主要整理自炼术成金JVM教学资料和《深刻理解Java虚拟机》,部分资料整理自网络,已不明来源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

  • 表示方式
    单精度

当指数位tomcat

  • 全为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启动流程

JVM启动流程

2.2 JVM基本结构

JVM基本结构

方法区物理上存在于堆里,并且是在堆的持久代里面;但在逻辑上,方法区和堆是独立的 方法区method area只是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,具体放在哪里,不一样的实现能够放在不一样的地方。而永久代是Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西bash

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寄存器

  1. 每个线程拥有一个PC寄存器
  2. 在线程建立时建立
  3. 指向下一条指令
  4. 执行本地方法时,PC值为undefined ?

2.2.2 方法区

  1. 保存装载的类信息:字段、方法信息、方法字节码
  2. 一般和永久区(perm)关联在一块儿

2.2.3 Java堆

  1. 对象保存在堆中
  2. 全部线程共享java堆
  3. GC工做空间
    GC工做空间

2.2.4 Java栈

  1. 线程私有

  2. 栈由一系列帧组成(故也叫帧栈)

  3. 帧保存每一个方法的局部变量表,操做数栈,常量池指针,程序计数器

  4. 每一次方法调用建立一个帧,并压栈

  5. 帧中有局部变量表

    static方法

    no static方法

  6. 操做数栈
    Java没有寄存器,全部参数传递使用操做数栈

    操做数栈

    栈上分配空间

  7. 小对象(几十bytes),在没有逃逸的状况下,能够直接分配在栈上

  8. 直接分配在栈上,能够自动回收,减轻GC压力

  9. 大对象或逃逸对象没法在栈上分配
    逃逸对象:栈内对象被外部对象引用,其做用范围脱离了当前方法栈

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 内存模型特性

  • 可见性:一个线程修改了变量,其余线程能够当即知道
  • 保证可见性的方法:
  1. volatile
  2. synchronized(unlock以前,写变量值回主存)
  3. 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
  • 指令重排的基本原则
    1. 程序顺序原则:一个线程内保证语义的串行性
    2. volatile规则:volatile变量的写,先发生于读
    3. 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
    4. 传递性:A先于B,B先于C 那么A必然先于C
    5. 线程的start方法先于它的每个动做和/方法
    6. 线程的全部操做先于线程的终结Thread.join(),最后才终结
    7. 线程的中断interrupt()先于被中断线程的代码,中断当即中止
    8. 对象的构造函数执行结束先于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算法

  1. 引用计数法:java中未使用
  2. 标记清除:老年代
  3. 标记压缩:老年代
  4. 复制算法:新生代
  5. 分代思想
    • 依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代
    • 根据不一样代的特色,选取合适的收集算法
      • 少许对象存活,适合复制算法
      • 大量对象存活,适合标记清理或者标记压缩

4.2 可触及性

  1. 可触及的
  • 从根节点能够触及到这个对象
  • 根:(与方法区栈相关)
    • 栈中引用的对象
    • 方法区中静态成员或者常量引用的对象(全局对象)
    • JNI方法栈中引用对象
  1. 可复活的
  • 一旦全部引用被释放,就是可复活状态,即不可达
  • 但在finalize()中可能复活该对象
  1. 不可触及的
    • 在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";
    }
}
复制代码
  1. 避免使用finalize方法
  2. 对象中只能调用一次,操做不慎可能致使错误
  3. 优先级低,什么时候被调用不肯定,什么时候发生GC不肯定
  4. 可使用try-catch-finally来替代

对于用可达性分析法搜索不到的对象,GC并不必定会回收该对象。要彻底回收一个对象,至少须要通过两次标记的过程。
第一次标记:对于一个没有其余引用的对象,筛选该对象是否有必要执行finalize()方法,若是没有执行必要,则意味可直接回收。(筛选依据:是否复写或执行过finalize()方法;由于finalize方法只能被执行一次)。
第二次标记:若是被筛选断定位有必要执行,则会放入FQueue队列,并自动建立一个低优先级的finalize线程来执行释放操做。若是在一个对象释放前被其余对象引用,则该对象会被移除FQueue队列

4.3 Stop-The-World

Java中一种全局暂停的现象
全局停顿,全部Java代码中止,native代码能够执行,但不能和JVM交互
多半因为GC引发,也能够是Dump线程、死锁检查、堆Dump

4.4 串行搜集器

  1. 最古老,最稳定
  2. 效率高
  3. 可能会产生较长的停顿
  4. 适用于数据量较小,对响应时间无要求的小型应用
  5. -XX:+UseSerialGC
    • 新生代、老年代使用串行回收
    • 新生代复制算法
    • 老年代标记-压缩
      串行搜集器

4.5 并行收集器

适用于对吞吐量有较高要求, 多CPU、对应用响应时间无要求的中、大型应用。
举例:后台处理、科学计算 吞吐量:=运行用户代码时间/(运行用户代码时间+GC时间)

  • 并发Concurrent:交替作不一样事的能力,用户程序能够不暂停,不必定并行,但能够交替执行
  • 并行Parallel:同时作不一样事的能力,垃圾回收线程并行工做,但应用程序等待暂停

4.5.1 ParNew

  • -XX:+UseParNewGC
    • 新生代并行
    • 老年代串行
  • Serial收集器新生代的并行版本
  • 复制算法
  • 多线程,须要多核支持
  • -XX:ParallelGCThreads 限制线程数量
    ParNew
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收集器(可自定义的、灵活)

  1. 相似ParNew
  2. 新生代复制算法
  3. 老年代 标记-压缩
  4. 更加关注吞吐量
  5. -XX:+UseAdaptiveSizePolicy自适应调节策略是Parallel与ParNew的重要区别
  6. -XX:+UseParallelGC
    • 新生代使用Parallel收集器+ 老年代串行
  7. -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]
复制代码
  1. 特殊参数
    • -XX:MaxGCPauseMills
      • 最大停顿时间,单位毫秒
      • GC尽力保证回收时间不超过设定值
    • -XX:GCTimeRatio
      • 0-100的取值范围
      • 垃圾收集时间占总时间的比
      • 默认99,即最大容许1%时间作GC
    • 这两个参数是矛盾的。由于停顿时间和吞吐量不可能同时调优

4.6 CMS并发收集器

适用于对响应时间有高要求,多CPU、对应用响应时间有较高要求的中、大型应用。
举例:Web服务器/应用服务器、电信交换、集成开发环境

  • 特性
    Concurrent Mark Sweep 并发标记清除(与用户线程一块儿执行 )
    标记-清除算法(不是标记压缩)
    并发阶段会下降吞吐量(?)
    只是针对老年代收集器(新生代使用ParNew/或串行)
    -XX:+UseConcMarkSweepGC

  • 运行过程

    1. 初始标记
      根能够直接关联到的对象
      速度快
      独占CPU,全局停顿
    2. 并发标记(和用户线程一块儿)
      标记的主要过程,标记所有对象
    3. 从新标记
      从新修正标记
      独占CPU,全局停顿
    4. 并发清除(和用户线程一块儿)
      基于标记结果,直接清除对象
      CMS并发收集器
  • 优:
    尽量下降停顿,在并发标记过程当中并不须要全局停顿

  • 劣:

  1. 会影响系统总体吞吐量和性能
    • 好比,在用户线程运行过程当中,分一半CPU去作GC,系统性能在GC阶段,反应速度就降低一半
  2. 清理不完全
    • 在清理阶段,用户线程还在运行,会产生新的垃圾,没法清理,由于和用户线程一块儿运行,不能在空间快满时再清理
    • -XX:CMSInitiatingOccupancyFraction设置触发GC的阈值
    • 若是不幸内存预留空间不够,就会引发concurrent mode failure,此时应该使用串行收集器做为后备,因为空间不足,此时通常停顿时间较长
  • 碎片清理问题
    CMS使用的是标记-清除算法,在清除后堆内存有效对象地址不连续,有内存碎片存在,故可设置内存压缩,整理内存碎片 即CMS为了性能考虑在老年代使用标记-清除算法,但仍能够设置使用标记-压缩算法
  1. -XX:+ UseCMSCompactAtFullCollectionFull GC后,进行一次整理
    • 整理过程是独占的,会引发停顿时间变长
  2. -XX:+CMSFullGCsBeforeCompaction
    • 设置进行几回Full GC后,进行一次碎片整理
  3. -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先后的详细堆栈信息
--Xloggc: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)。
PSYoungGen:是使用-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
    1. 当准备要触发一次young GC时,若是发现统计数听说以前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(由于HotSpot VM的GC里,除了CMS的concurrent collection以外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,因此不须要事先触发一次单独的young GC);
    2. 若是有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;
    3. 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能够定制,知足不一样的字节码流获取方式(好比网络)

jdk默认类加载过程

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流的格式是正确的
    1. 文件格式的验证
      • 是否以0xCAFEBABE开头
      • 版本号是否合理:class文件由什么版本jdk编译生成,与执行class的jdk是否兼容
    2. 元数据验证(基本信息验证)
      • 是否有父类:class中指定了父类,检查父类是否存在
      • 继承了final类?
      • 非抽象类实现了全部的抽象方法
    3. 字节码验证 (复杂)
      • 运行检查
      • 栈数据类型和操做码数据参数吻合
      • 跳转指令指定到合理的位置
    4. 符号引用验证
      • 常量池中描述类是否存在:引用的类必须存在
      • 访问的方法或字段是否存在且有足够的权限:private…
  • 准备
    1. 分配内存,并为类设置初始值 (方法区中)
      • 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的参数

jps命令

6.1.2 jinfo

查看进程参数 能够用来查看正在运行的Java应用程序的扩展参数,甚至支持在运行时,修改部分参数
-flag 进程ID:打印指定JVM的参数值
-flag [+|-] 进程ID:设置指定JVM参数的布尔值
-flag = 进程ID:设置指定JVM参数的值

jinfo

6.1.3 jmap

生成Java应用程序的堆快照和对象的统计信息

jmap

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选项

jstack

6.1.5 JConsole

图形化监控工具
能够查看Java应用程序的运行概况,监控堆信息、永久区使用状况、类加载状况等

JConsole

6.1.6 Visual VM

Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具

Visual VM

6.1.7 MAT

MAT

6.2 Java堆分析

  • 内存溢出OOM缘由
    Jvm内存区间:堆、永久区、线程栈、直接内存
    堆+线程栈 +直接内存<= 操做系统可分配空间
  1. 堆溢出
    占用大量堆空间,直接溢出
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)
复制代码

解决方法:增大堆空间,及时释放内存,分批处理

  1. 永久区溢出
//生成大量的类
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回收

  1. 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

注意:堆+线程栈+直接内存 <= 操做系统可分配空间

  1. 直接内存溢出
    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)。

对象内存结构
Mark Word 32bit

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加锁
    每个线程在准备获取共享资源时:
  1. 检查MarkWord里面是否是放的本身的ThreadId ,若是是,表示当前线程是处于 “偏向锁”
  2. 若是MarkWord不是本身的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知以前线程暂停,以前线程将Markword的内容置为空。
  3. 两个线程都把对象的HashCode复制到本身新建的用于存储锁的记录空间,接着开始经过CAS操做,把共享对象的MarKword的内容修改成本身新建的记录空间的地址的方式竞争MarkWord,
  4. 第三步中成功执行CAS的得到资源,失败的则进入自旋
  5. 自旋的线程在自旋过程当中,成功得到资源(即以前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,若是自旋失败
  6. 进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待以前线程执行完成并唤醒本身

八.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

Class文件结构

九. 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:默认,混合
相关文章
相关标签/搜索