JVM
1.什么是jvm?
JVM全称即是java虚拟机,它区别于一些其它 虚拟机 ,例如VMware 等 ,VMware这些虚拟机是通过软件层面来模拟真正的物理机所具备的功能,而java的jvm是用来加载运行.class文件,内部有它自己的指令集,是针对.class文件而言的独有指令集,而VMware中模拟的是真正物理机中的指令集,两个指令集是不一样的..class文件相当于c语言编译后的文件,C语言编译链接后,可以运行在物理机的操作系统平台,.class文件是java语言编译后生成的文件,它通过链接后也可运行在jvm虚拟机上.
JVM是用c编写的,它就相当于一个强大的第三方,或者说类似于一个强大的编译器,在不同的操作系统平台上,它有各自的适应版本,它们的适应版本虽然不同,但是它们是可以解析运行同一段java代码,这就是它可移植性的根本依赖,即实现了一次编写,处处运行.
需要注意的是,JVM与java语言相对独立,java语言编写的程序的运行需要依赖jvm,但是jvm是可以独立出来的,它的上面并不仅仅可以运行java语言,只要符合它的规则的语言都可以运行,如Groovy Clojure Scala这几种语言都可以运行在jvm上.
2.JVM的发展史
1996年 SUN JDK 1.0 Classic VM 纯解释运行,使用外挂进行JIT 1997年 JDK1.1 发布 AWT、内部类、JDBC、RMI、反射
1998年 JDK1.2 Solaris Exact VM JIT 解释器混合 Accurate Memory Management 精确内存管理,数据类型敏感 提升的GC性能. java1.2开始,称为 java2 , java2SE , java2ME ,java2EE .
2000年 JDK 1.3 Hotspot 作为默认虚拟机发布
2002年 JDK 1.4 Classic VM退出历史舞台
2004年发布 JDK1.5 即 JDK5 、J2SE 5 、Java 5 支持泛型 ,注解, 装箱 ,枚举 ,可变长的参数 ,Foreach循环 ,后又出JDK1.6 ,即JDK6 支持 脚本语言,JDBC 4.0 ,Java编译器 API
2011年 JDK7发布 ,延误项目推出到JDK8 ,有了G1 ,动态语言增强, 64位系统中的压缩指针 ,NIO 2.0
2014年 JDK8发布,有了 Lambda表达式 语法增强 Java类型注解
2016年JDK9 发布,有了模块化
♥ 使用最为广泛的JVM为HotSpot ,HotSpot 为Longview Technologies开发,后被SUN收购 .
2006年 Java开源 并建立OpenJDK HotSpot 成为Sun JDK和OpenJDK中所带的虚拟机
2008 年 Oracle收购BEA 得到JRockit VM
2010年Oracle 收购 Sun 得到Hotspot , Oracle宣布在JDK8时整合JRockit和Hotspot,优势互补, 在Hotspot基础上移植JRockit优秀特性
KVM SUN发布 IOS Android前,广泛用于手机系统
CDC/CLDC HotSpot 手机、电子书、PDA等设备上建立统一的Java编程接口 J2ME的重要组成部分
JRockit BEA
IBM J9 VM , 应用于IBM内部
Apache Harmony 兼容于JDK 1.5和JDK 1.6的Java程序运行平台 与Oracle关系恶劣 退出JCP ,Java社区的分裂 OpenJDK出现后,受到挑战 2011年 退役 没有大规模商用经历 对Android的发展有积极作用
3.java语言规范
4.JVM规范
5.JVM启动流程:
JVM启动流程图
6.JVM基本结构
一 . PC寄存器 :
每个线程都有一个pc寄存器,在线程创建时创建,指向下一条指令的地址,执行本地方法时,pc的值为undefined
二 . 方法区 :
保存装载的类的信息,包含有类型的常量池,字段方法信息,方法字节码,通常和永久区perm关联在一起
三 . JAVA堆
是和程序开发密切相关的,应用系统对象都保存在java堆中,所有线程共享java堆
对分代GC来说,堆也是分代的
GC的主要工作区间
四. JAVA栈
java栈是线程私有的,栈由一系列帧组成,因此也叫帧栈,帧保存一个方法的局部变量,常量池指针,操作数栈,每一次方法调用都创建一个帧,并压栈.在多线程访问中,每一个线程过来,都有它自己的栈.函数调用组成帧栈
操作数栈,java没有寄存器,所有参数传递使用操作数栈, 用来操作数据,做一些加减,出入栈等.
在java中,栈上分配,函数调用完自动清理
一般在栈上分配的是没有逃逸(本来这个对象只能本线程操作访问,但是如果其它线程也能操作的话,就叫做逃逸)的小对象,这样直接分配在栈上,可以减轻GC回收压力
大对象和逃逸对象是无法栈上分配的.
五. 内存模型
每个线程都有一个工作内存和主存独立,工作内存存放主存中变量值的拷贝
为了更好的表现出来,再看下面这张图,手抖,图画的有点...,咳咳,见谅
可见性: 一个线程修改了变量,其他线程可以立即知道.保证可见性的方法有volatile,final(一旦初始化完成,其他线程就可见),synchronized
有序性 : 在本线程,操作都是有序的,在本线程外观察,操作都是无序的
指令重排 :
在jcpu指令的执行中,是呈流水线工作状态,从而提高指令的运行数量,提高效率.这种流水线工作状态也有一个弊端,那就是在一个流程的前一个指令操作的数据,需要依赖后面一个流程未执行的指令执行后的结果的时候,这个前面流程的指令不能执行,需要等待后面流程指令执行后才可以,这时候,在这个位置会插入一个气泡,也就是没有指令执行,cpu为了优化执行效率,将这个气泡用其他不相关,不影响的指令填充了,这样整个效率就提升了,这个指令重排是硬件层面的重排.当然,在这个过程中,有些指令是可以重排,有些是不能重排的.这些不用我们来考虑. 指令重排破坏了线程间的有序性,可以通过同步来保证有序性
指令重排的基本原则,依赖于程序顺序原则,volatile规则,锁规则,传递性等.......
解释运行 : 解释执行的意思是读一句,执行一句
JIT: 即是编译运行,将字节码编译成机器码,直接执行机器码,编译后,性能有数量级的提升
7.JVM的配置参数
trace跟踪参数 :
-verbose:gc -xx:printGC 可以打印GC的简要信息
-xx:printGCDetails 可以打印GC的详细信息
-xloggc:log/gc.log 指定GC log的位置,以文件输出
-xx:+TraceClassLoading 监控类的加载
堆的分配参数 :
-xmx - xms 指定最大堆和最小堆
-xmn 设置新生代大小
-xx:NewRatio 新生代(eden+2*s)和老年代的比值
-xx:Survivor区和eden的比
-xx:+HeapDumpOnOutOfMemoryError OOM时导出堆到文件
-xx:+HeapDumpPath 导出OOM的路径
-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p 在oom时执行一个脚本,oom就是内存溢出
根据实际情况调整新生代和幸存代的大小
官方推荐新生代占堆的3/8
幸存代占新生代的1/10
在OOM时,要Dump出堆,确保可以排查问题
-xx:PermSIze -xx:MaxPermSize 设置永久区的初始空间和最大空间,表示一个系统可以容纳多少个类型
使用CGLIB等库的时候,可能会产生大量的类,这些类有可能撑爆永久区导致OOM
有时候OOM,打开堆的Dump,可以看到堆空间实际占用非常少,但是永久区溢出一样抛出OOM
栈大小分配:
-xss 通常只有几百K,决定了函数调用的深度,每个线程都有独立的栈空间,局部变量,参数分配在栈上
8.GC算法与种类:
GC即是jvm的垃圾回收机制
GC算法有:
引用计数法
标记清除法
标记压缩法
复制算法
在java中,GC的对象是堆空间和永久区
老牌的垃圾回收算法,通过引用计数来回收垃圾,COM,ActionScript#,Python中都有使用
一 . 引用计数法
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,
当引用失效时,引用计数器就减1.只要对象A的引用计数器的值为0,则对象A就不可能再被使用.
引用计数法问题: 引用和去引用伴随加法和减法,影响性能,很难处理循环引用
二. 标记-清除
标记-清除 算法使现代垃圾回收算法的思想基础.标记-清除算法将垃圾回收分为两个阶段:
标记阶段和清除阶段.一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的 可达对象.因此,未被标记的对象就是未被引用的垃圾对象.然后,在清除阶段,清除所有未被标记的对象.
三. 标记-压缩
标记-压缩算法适合用于存活对象较多的场合,如老年代.它在标记-清除算法的基础上做了一些优
化.和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记.但之后
,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端.之后,清理边界外所有的
空间.
四. 复制算法
与标记-清除算法相比,复制算法使一种相对搞笑的回收方法,但是它不适合用于存活对象较多的场合,
如老年代.它的原理是将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内
存中的存活对象复制到未使用的内存中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色
,完成垃圾回收
复制算法的最大问题是 : 空间浪费
引用计数算法没有被java采用
9.分代思想
依据对象的存货周期进行分类,短命对象归为新生代,长命对象归为老年代.
根据不同代的特点,选取合适的收集算法
少量对象存活,适合复制算法
大量对象存活,适合标记清理或者标记压缩
10.可触及性
可触及的 : 从根节点可以触及到这个对象
可复活的 : 一旦所有引用被释放,就是可复活状态,在finalize()中可能复活该对象
不可触及的 : 在finalize后,可能会进入不可触及状态,不可触及的对象不可能复活,可以回收
尽量避免使用finalize(),操作不慎会导致错误,可以使用try-catch-finally 来替代它
根 : 指的是栈中引用的对象 , 方法区中静态成员或者常量引用的对象(全局对象),JNI方法栈中引用的对象
11.Stop-The-World
Stop-The-World 是java中一种全局暂停的现象,意思是,所有java代码停止,native代码可以执行,但是不能喝JVM
交互,多半是由于GC引起, Dump线程,死锁检查,堆Dump等
GC有全局停顿,是因为在清理垃圾对象的时候,如果不停止活动,则有新的垃圾产生,在jvm层面来说,它就无法知道
何时能清理干净了.
全局停顿的危害, 长时间服务器停止,没有响应,遇到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
这两个参数是矛盾的,因为停顿时间和吞吐量不可能同时调优.
CMS收集器
COncurrent Mark Sweep 并发标记清除,就是与用户线程一起执行
使用了标记-清除算法,与标记-压缩相比,并发阶段会降低吞吐量,老年代收集器,新生代使用了ParNew
-xx:+UseConcMarkSweepGC
CMS运行过程比较复杂,着重实现饿了标记的过程,可分为
初始标记:
根可以直接关联到的对象,速度快
并发标记:
就是和用户线程一起,主要标记过程,标记全部对象
重新标记:
由于并发标记时,用户线程依然运行,因此在正式清理前,再一次修正
并发清除:
和用户线程一起执行
基于标记结果,直接清理对象
CMS的特点:
尽可能降低停顿,但是会影响系统整体吞吐量和性能.
例如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
还会导致清理不彻底
因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
因为和用户线程一起运行,不能在空间快满时再清理
-xx:CMSInitiatingOccupancyFraciton设置出发GC的阈值,如果内存预留空间不够,就会引起
concurrent mode failure
可以使用串行收集器作为后备
关于碎片
标记-清除会有碎片产生,标记压缩没有
-xx:+UseCMSCompactAtFullCollection Full GC 后 ,进行一次整理,整理过程是独占的,会引起停
顿时间变长
-xx:+CMSFullGCsBeforCompaction , 设置进行几次Full gc后,进行一次碎片整理
-xx:ParallelCMSThreads 设定CMS的线程数量
通过前面这些,可以看出,GC的处理也与系统的性能和稳定有很大关系,所以在软件架构设计和代码编写
以及堆空间分配中,我们就要注意到,从而减轻GC压力....
GC整个参数的设置整理:
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
-XX:NewRatio:新生代和老年代的比
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC :新生代使用并行回收收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收 -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
减小堆的大小,会增加GC压力,升级jdk也会带来额外的性能提升,性能的根本在应用,GC参数属于微调,但是它的设置不
合理,会影响性能,产生大的延迟
12.类装载器
BootStrap ClassLoader 启动类加载器
Extension ClassLoader 扩展类加载器
App ClassLoader 应用或者系统类加载器
Custom ClassLoader 自定义类加载器
每个类加载器都有一个parent作为父亲
全盘委托的类加载机制,也就是双亲模式,顶层ClassLoader无法加载底层ClassLoader的类
Thread.setContextClassLoader() 上下文加载器,用来解决这个问题,双亲模式是默认模式,但是并不是说必须这
这么做,例如Tomcat就打破这一模式
13.性能监控工具
性能监控工具,java自带的就有很多
死锁 :死锁的结果会导致程序卡死 ,可以用jstack查找死锁
Jmeter这个工具可以用来测试服务器性能,
14.锁
JVM内部的锁有偏向锁,轻量级锁,自旋锁
Mark Word ,对象头的标记,32位,描述对象的hash,锁信息,垃圾回收标记,年龄,指向锁记录的指针,包括指向monitor
的指针,GC标记,偏向锁线程ID等
无锁的实现方式:CAS,非阻塞的同步
15.class文件结构
魔数
版本 .....