第一部分 走进Java
1、走进Java
一、概述html
java普遍应用于嵌入式系统、移动终端、企业服务器、大型机等各类场合,摆脱了硬件平台的束缚,实现了“一次编写,处处运行”的理想java
二、java技术体系结构程序员
按照功能来划分算法
- 包括如下几个组成部分:Java程序设计语言,各类硬件平台的java虚拟机,Java API类库,来自商业机构和开源社区的第三方Java类库,Class文件格式
- Java程序设计语言,java虚拟机,Java API类库统称为JDK,是用于支持java程序开发的最小环境
- Java API类库中的Java SE API子集和Java虚拟机统称为JRE,是支持java程序运行的基本环境
按照技术所服务的领域划分分为4个平台数据库
- Java Card:支持java小程序运行在java小内存设备(如智能卡)上的平台
- Java ME:支持Java程序运行在移动设备上的平台
- Java SE:支持面向桌面级应用的平台
- Java EE:支持使用多层架构的企业级应用的平台
第二部分 自动内存管理机制
2、内存区域和内存溢出异常
一、运行时数据区
程序计数器编程
- 记录的是正在执行的虚拟机字节码指令的地址,能够当作是当前线程所执行的字节码的行号指示器,每一个线程都有一个独立的程序计数器,各条线程的程序计数器互不影响,独立存储,这类内存区域成为“线程私有的内存”。
- 此内存区域是惟一在虚拟机规范中没有OutOfMemoryError的状况的区域
Java虚拟机栈小程序
- 同程序计数器同样,也是线程私有的。每一个方法在执行的时候都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法出口等信息。
- 每个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
- 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法须要在栈帧中分配多大的局部变量空间是彻底肯定的,在方法运行期间不会改变局部变量表的大小。
- 若是请求的栈深度超过虚拟机锁容许的深度,将抛出StackOverFlowError异常。若是拓展没法申请到足够的内存,将抛出OutOfMemoryError异常。
本地方法栈缓存
- 为虚拟机使用的native方法服务,和虚拟机栈同样,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常。
Java堆tomcat
- Java堆是全部线程共享的一块内存区域,用来存放对象实例,几乎全部的对象实例都在这里分配。
- Java堆是垃圾回收的主要区域,采用分代收集算法。
- Java堆分为新生代和老年代,新生代在细致一点分为Eden,From Survivor,To Survivor空间。
- 若是堆中没法完成对象实例的内存分配,且堆也没法扩展时,将抛出OutOfMemoryError异常。
方法区安全
- 是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说使用永久代来代替方法区。
- 在目前已经发布的JDK1.7的HotSpot中,已经把本来放在永久代的字符串常量池移出了。当方法区没法知足内存的分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
- 是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池存放。
- 运行时常量池相对于Class文件常量池,具备动态性,运行期间也能够将新的常量放入常量池,好比String类的intern()方法。
- 当运行时常量池没法申请到更多的内存时,将会抛出OutOfMemoryError异常。
直接内存
- 并非运行时区域的一部分,JDK 1.4加入的NIO 它可使用Native函数库直接分配堆外内存,而后经过Java堆中的DirectByteBuffer对象做为这块内存的引用进行操做。
二、HotSpot虚拟机对象探秘
对象的建立
- 当遇到new指令时,先判断这个类是否被加载、解析、初始化过,若是没有,先执行相应类的加载过程。
- 类加载检查经过后,为新生对象分配内存,若是Java堆内存是规整连续的,采用“指针碰撞”的分配方式,若是是不连续规整的,采用“空闲列表”分配方式。内存是否规整取决于垃圾收集器是否带有压缩整理功能。
- Serial,ParNew等带有Compact过程的收集器,采用的分配算法是“指针碰撞”。而CMS这种基于Mark-Sweep算法的收集器,一般采用“空闲列表”分配方式。
- 建立对象涉及到分配内存和指针指向两个操做,不是原子性的,不是线程安全的。针对这个问题,有两个解决办法:1是采用CAS加上失败重试来保证操做的原子性。2是采用TLAB(Thread Local Allocation Buffer)策略,在Java堆中预先为每个线程分配一小块内存,称为TLAB(Thread Local Allocation Buffer),哪一个线程要分配内存就在各自的TLAB上进行内存的分配,只有TLAB用完进行新的TLAB的分配时才须要同步锁定,虚拟机是否使用TLAB,能够经过 -XX:+/- UseTLAB
- 内存分配完成后,须要对对象头进行设置,包括这个对象是哪一个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
- 最后执行init方法,把对象按照程序员的意愿进行初始化。这样一个真正可用的对象才算彻底生产出来。
对象的内存布局
- 分为三块区域,对象头(Header)、实例数据(Instance Data)、对齐补充(Padding)。
- 对象头,存储对象自身的运行时数据,如哈希码、对象的GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit。
- 另外一个部分是类型指针,虚拟机经过这个对象来肯定这个对象是哪一个类的实例。
对象的访问定位
- Java程序须要经过栈上的reference数据来操做堆中的具体对象,具体实现有两种方式:使用句柄和直接指针两种。
- 使用句柄:Java堆中划分出一块内存做为句柄池,reference中存储的就是对象的句柄地址,句柄中包括了对象的实例数据和类型数据各自的地址信息。最大好处是当对象修改时,reference自己不须要修改,由于reference中存储的是稳定的句柄地址

- 直接指针:reference中存储的直接就是堆中的对象地址,堆对象的布局中须要考虑如何放置访问类型数据的相关信息。最大好处是速度更快,节省了一次指针定位的开销,HotSpot就采用的直接指针方式。

三、OutOfMemoryError异常
堆溢出
- 不断建立对象,保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,达到最大堆的容量限制后就会产生内存溢出异常。
- -Xms20m 堆的最小值;-Xmx20m 堆的最大值;-XX:+HeapDumpOnOutOfMemoryError 内存溢出异常时Dump出当前的内存堆转储快照以便往后分析
虚拟机栈和本地方法栈溢出
方法区和运行常量池溢出
- 屡次调用String.intern()方法能够产生内存溢出异常。JDK 1.6之间,能够经过 -XX:PermSize 和 -XX:MaxPermSize 限制永久代大小,从而达到限制方法区大小的目的
本地直接内存溢出
- 能够经过 -XX:MaxDirectMemorySize 指定。若是不指定,则默认和Java堆最大值(-Xmx 指定)同样
3、垃圾收集器和内存分配策略
一、对象已死吗?如何肯定对象是否还“活着”
引用计数器方法
- 给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减1。
- 优势是断定简单,效率也很高。缺点是没法解决相互循环引用的问题
可达性分析方法
- 经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,说明这个对象是可回收的。
- Java语言中,可做为GC Roots的对象包括如下几种:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。
再谈引用
- JDK1.2 以后把引用分为了四种:强引用、软引用、弱引用、虚引用
- 强引用:只要强引用还存在,就不会被垃圾回收器回收。相似 Object o=new Object()
- 软引用:指一些有用但并不是必须的对象,在系统将要发生内存溢出的时候,会将这部分对象回收。SoftReference 类来实现软引用
- 弱引用:被弱引用关联的对象只能生存到下一次垃圾回收。WeakReference 类来实现弱引用
- 虚引用:一个对象是否有虚引用的存在,彻底不会对其生存时间造车影响,也没法经过虚引用取得对象的引用。一个对象设置虚引用的惟一目的是在被垃圾回收的时候收到一个系统通知
对象被回收的过程
- 当对象进行可达性分析没有与GC Roots相连的引用链,将会被第一次标记,并根据是否须要执行finalize()方法进行一次筛选,对象没有重写finalize()或者虚拟机已经调用过finalize(),都被视为不须要执行
- 若是对象有必要执行finalize,会被放入到F-Queue队列中,并在稍后由虚拟机自动建立的低优先级的Finalizer线程去触发它,并不保证等待此方法执行结束。
- 若是对象在finalize()方法执行中,从新和GC Roots产生了引用链,则能够逃脱这次被回收的命运,但finalize()方法只能运行一次,因此并不能经过此方法逃脱下一次被回收
- 笔者不建议使用这个方法,建议你们彻底忘掉这个方法的存在。
回收方法区
- 主要包括废弃常量和无用类的回收。判断类无用:类的实例都被回收,类的ClassLoader被回收,类的Java.Lang.Class对象没有在任何地方引用。知足这三个条件,类才能够被回收(卸载)
- HotSpot虚拟机经过 -Xnoclassgc 参数进行控制是否启用类卸载功能。在大量使用反射、动态代理、CGLib等框架,须要虚拟机具有类卸载功能,避免方法区发生内存溢出
二、垃圾回收算法
标记-清除
- 先标记出全部要回收的对象,在标记完成后统一进行对象的回收。有两个不足:
1 是效率问题,标记和清除的效率都不高。
2 是空间问题,会产生大量不连续的内存碎片,碎片太多会都致使大对象没法找到足够的内存,从提早触发垃圾回收。
复制算法
- 新生代分为一个Eden,两个Survival空间,默认比例是8:1。回收时,将Eden和一个Survival的存活对象所有放入到另外一个Survival空间中,最后清理掉刚刚的Eden和Survival空间
- 当Survival空间不够时,由老年代进行内存分配担保
标记-整理
- 根据老年代对象的特色,先标记存活对象,将存活对象移动到一端,而后直接清理掉端边界之外的对象
分代收集
- 新生代采用复制算法,老年代采用标记-删除,或者标记-整理算法。
三、HotSpot算法实现
枚举根节点实现
- 可达性分析时会进行GC停顿,停顿全部的Java线程。
- HotSpot进行的是准确式GC,当系统停顿下来后,虚拟机有办法得知哪些地方存在着对象引用,HotSpot中使用一组称为OopMap的数据结构来达到这个目的
安全点
- HotSpot没有为每一个指令都生成OopMap,只在特定的位置记录这些信息,这些位置称为安全点。安全点的选定不能太少,也不能太频繁,安全点的选定以“是否让程序长时间执行”为标准
- 采用主动式中断的方式让全部线程都跑到最近的安全点上停顿下来。设置一个标志,各个程序执行的时候轮询这个标志,发现中断标志为真时本身就中断挂起
安全区域
四、垃圾收集器
若是两个收集器之间有连线,说明能够搭配使用。没有最好的收集器,也没有万能的收集器,只有对应具体应用最合适的收集器。

Serial 收集器
- 新生代收集器,单线程回收。优势在于,简单而高效,对于运行在Client模式下的虚拟机来讲是一个很好的选择(好比用户的桌面应用)
- 参数 -XX:UseSerialGC,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收
ParNew收集器
- 新生代收集器,Serial的多线程版本,除了Serial收集器以外,只有它能与CMS收集器配合工做。
- -XX:+UseConcMarkSweepGC 选项后默认的新生代收集器,也可使用 -XX:+UseParNewGC 选项来强制指定它
- ParNew收集器在单CPU的环境中,效果不如Serial好,随着CPU的增长,对于GC时系统资源的利用仍是颇有效的。
- 默认开启的收集线程数和CPU数相等,可使用 -XX:ParallelGCThreads 指定
Parallel Scavenge收集器
- 新生代收集器,并行收集器,复制算法,和其余收集器不一样,关注点的是吞吐量(垃圾回收时间占总时间的比例)。提供了两个参数用于控制吞吐量。
- -XX:MaxGCPauseMillis,最大垃圾收集停顿时间,减小GC的停顿时间是以牺牲吞吐量和新生代空间来换取的,不是设置的越小越好
- -XX:GCTimeRatio,设置吞吐量大小,值是大于0小于100的范围,至关于吞吐量的倒数,好比设置成99,吞吐量就为1/(1+99)=1%。
- -XX:UseAdaptiveSizePolicy ,这是一个开关参数,打开以后,就不须要设置新生代大小(-Xmn)、Eden和Survival的比例(-XX:SurvivalRatio)、 晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,收集器会自动调节这些参数。
Serial Old 收集器
- 单线程收集器,老年代,主要意义是在Client模式下的虚拟机使用。在Server端,用于在JDK1.5以及以前版本和Parallel Scavenge配合使用,或者做为CMS的后备预案。
Palallel Old 收集器
- 是Parallel Scavenge的老年代版本。在注重吞吐量的场合,均可以优先考虑Parallel Scavenge 和Palallel Old 配合使用
CMS 收集器
- Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器,尤为重视服务的响应速度。基于标记-清除算法实现。
- 分为四个步骤进行垃圾回收:初始标记,并发标记,从新标记,并发清除。只有初始标记和从新标记须要停顿。
- 初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记就是进行GC Roots的Tracing。
- 从新标记为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,远比并发时间短。
- 耗时最长的并发标记和并发清除过程当中,处理器能够与用户线程一块儿工做。
- 它并非完美的,有以下三个比较明显的缺点:
一、垃圾回收时会占用一部分线程,致使系统变慢,总吞吐量会下降。
二、没法处理浮动垃圾,须要预留足够的内存空间给用户线程使用,能够经过 -XX:CMSInitiatingOccupancyFraction 参数控制触发垃圾回收的阈值。
若是预留的内存没法知足程序须要,就会出现“Concurrent Mode Failure”失败,这时将启动应急预案,启用Serial Old 进行垃圾回收,停顿时间会变长
因此-XX:CMSInitiatingOccupancyFraction 参数的值设置的过高,会致使频繁“Concurrent Mode Failure”失败,性能反而下降。
三、标记-清理,容易产生内存碎片。-XX:+UseCMSCompactAtFullColletion 开启碎片整理功能,默认开启,-XX:CMSFullGCsBeforeCompaction,控制多少次不压缩的FullGC以后来一次带压缩的
G1 收集器
- 包括新生代和老年代的垃圾回收。和其余收集器相比的优势:并行和并发,分代收集,标记-整理,可预测的停顿。垃圾回收分为如下几个步骤:
- 初始标记:标记GC Roots可以直接关联到的对象,这阶段须要停顿线程,时间很短
- 并发标记:进行可达性分析,这阶段耗时较长,可与用户程序并发执行
- 最终标记:修正发生变化的记录,须要停顿线程,可是可并行执行
- 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所指望的停顿时间来执行回收计划
五、内存分配和回收策略
- 对象优先在Eden分配,当新生区没有足够的内存是,经过分配担保机制提早转移到老年代中去
- 大对象直接进入老年代。大对象是指须要大量连续内存空间的对象,虚拟机提供了参数 -XX:PretenureSizeThreshold(只对Serial,PerNew两个回收器起效),令大于这个值得对象直接在老年代分配,避免了Eden和两个Survival之间发生大量的内存复制。
- 长期存活的对象将进入老年代。虚拟机给每一个对象定义了对象年龄计数器(Age),若是对象在Eden出生,通过第一次Minor GC后依然存活,而且能被Survival容纳的话,将被移动到Survival,对象年龄设为1。对象在Survival中每熬过一次Major GC,年龄就增长1,达到必定程度(默认是15),就会被晋升到老年代。对象晋升老年代的阈值,能够经过参数-XX:MaxTenuringThreShold 指定
- 动态对象年龄判断。若是在Survival空间中相同年龄全部对象的大小综合超过了Survival空间的一半,年龄大于等于这个年龄的对象都会被晋升到老年代。无需等待年龄超过MaxTenuringThreShold指定的年龄
- 空间分配担保。只要老年代的连续空间大于新生代对象总和或者历次晋升的平均大小,就进行Major GC,不然进行Full GC。
4、虚拟机性能监控与故障处理工具
一、jps
命令用法: jps [options] [hostid]
功能描述: jps是用于查看有权访问的hotspot虚拟机的进程. 当未指定hostid时,默认查看本机jvm进程
经常使用参数:-lmvV
详细说明:JAVA JPS 命令详解
二、jstat。监视JVM内存工具。
语法结构:
Usage: jstat -help|-options
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
例子: jstat -gcutil 25444 1000 5
详细说明:JDK之jstat的用法
三、jinfo。查看和修改JVM运行参数
java -XX:+PrintFlagsFinal -version|grep manageable 【查看JVM中哪些参数能够被jinfo动态修改】
jinfo -flag +PrintGCDetails 105704 【修改参数 PrintGCDetails 的值】
四、jmap。命令用于生成heap dump文件
若是不使用这个命令,还可使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候自动生成dump文件。
jmap不只能生成dump文件,还能够查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪一种收集器等。
详细使用:JVM调优命令-jmap
五、jstack。Java堆栈跟踪工具
详细使用:使用jstack精确找到异常代码,jstack 工具使用,性能调优
注意:dead lock问题,占用cpu时间最多的线程,频繁GC
入手点总结:
wait on monitor entry: 被阻塞的,确定有问题,等待synchronized锁
runnable : 注意IO线程,IO阻塞的线程
in Object.wait(): 注意非线程池等待,调用Object.wait()的对象
5、常见JVM配置说明
一、JVM配置
1.一、G1
CPU 核数:8;内存(GB):16;磁盘(GB):400
-Xms10g
-Xmx10g
-Xss512k
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=128M
-XX:+UseG1GC
-XX:MaxGCPauseMillis=150
-XX:+ParallelRefProcEnabled
-XX:+UnlockExperimentalVMOptions
-XX:G1MaxNewSizePercent=70
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2
1.二、CMS+ParNew
CPU 核数:4;内存(GB):8;磁盘(GB):200
-Xmx4g
-Xms4g
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:SurvivorRatio=8
-XX:NewRatio=4
-XX:+HeapDumpOnOutOfMemoryError
-XX:+DisableExplicitGC
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:ParallelGCThreads=4
-XX:+CMSClassUnloadingEnabled
-XX:CMSFullGCsBeforeCompaction=1
-XX:CMSInitiatingOccupancyFraction=72
1.三、参数说明
通用参数
-Xms10g -Xmx10g |
-Xms10g , 初始化堆大小。一般状况和-Xmx大小设置同样,避免虚拟机频繁自动计算后调整堆大小。 -Xmx10g ,最大堆大小。 |
-XX:MaxDirectMemorySize=128M |
本地直接内存大小。适合频繁的IO操做,例如网络并发场景(NIO) 直接内存和堆内存比较:
|
-XX:+PrintGCApplicationStoppedTime |
打印垃圾回收期间程序暂停的时间 |
-XX:+PrintGC |
打印GC基本日志 |
-XX:+PrintGCDetails |
打印GC详细日志 |
-XX:+DisableExplicitGC |
关闭System.gc() |
-Xss512k |
每一个线程堆栈大小。每一次方法的调用都对应一个入栈和出栈 |
-XX:PermSize=256m -XX:MaxPermSize=256m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m |
JDK8之前:
JDK8及之后:
|
-XX:+HeapDumpOnOutOfMemoryError |
JVM异常自动生成堆转储 |
CMS
-XX:+UserConcMarkSweepGC |
年老代指定CMS垃圾回收器,新生代默认用ParNew收集 |
-XX:ParallelGCThreads=4 |
设置垃圾收集线程数 |
-XX:CMSInitiatingOccupancyFraction=72 |
老年代垃圾占比达到这个阈值开始CMS收集,设置太高容易致使并发收集失败,会出现SerialOld收集的状况 |
-XX:+UseCMSCompactAtFullCollection(默认开启,不须要设置) |
在FULL GC的时候, 对年老代开启碎片整理功能,默认开启 |
-XX:CMSFullGCsBeforeCompaction=1 |
控制FullGC压缩的间隔。多少次不压缩的FullGC以后来一次带压缩的 |
-XX:+CMSClassUnloadingEnabled |
CMS收集器默认不会对永久代进行垃圾回收。若是但愿对永久代进行垃圾回收,能够设置标志 |
-XX:+CMSParallelRemarkEnabled |
为了减小第二次暂停的时间,开启并行remark,下降标记停顿 |
-XX:NewRatio=4 |
新生代:老年代=1:4 |
-XX:SurvivorRatio=8 |
2个survivor和eden的比值。表示2:8 默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10 |
G1
-XX:+UseG1GC |
使用G1垃圾回收 |
-XX:MaxGCPauseMillis=150 |
GC最大暂停时间 |
-XX:+ParallelRefProcEnabled |
打开并行引用处理 |
-XX:+UnlockExperimentalVMOptions |
有些时候当设置一个特定的JVM参数时,JVM会在输出“Unrecognized VM option”后终止。若是参数输入是正确的,而且JVM并不识别,须要设置-XX:+UnlockExperimentalVMOptions 来解锁参数。 |
-XX:G1NewSizePercent |
新生代最小值比例。默认5% |
-XX:G1MaxNewSizePercent=70 |
新生代最大值比例 |
-XX:ParallelGCThreads=8 |
STW期间,并行GC线程数 |
-XX:ConcGCThreads=2 |
并发标记阶段,并行执行的线程数 |
6、JVM调优案例分析与实践
一、Minor GC、Major GC和Full GC之间的区别
- 每次 Minor GC 会清理年轻代的内存
- Major GC 是清理老年代,Full GC 是清理整个堆空间—包括年轻代和老年代。大部分时候Major GC和Full GC区分的不是很明显
- 不只仅Major GC和Full GC会stop-the-world。全部的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,中止应用程序的线程。对于大部分应用程序,停顿致使的延迟都是能够忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。若是正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长不少。
二、经常使用命令
查看java进程号
两种方式均可以查看tomcat进程号
ps -ef | grep java
jps -lmvV |grep java
结果以下:2556
查看进程内线程状况
top -Hp 2556(2556为上一步查询出来的进程号)
找到占用cpu时间最长的线程号:3345
获得线程号的十六进制数
printf "%x\n" 3345(输出为a05)
使用jstack定位问题
jstack 2556 | grep a05
查看内存和swap使用状况。参考:https://www.cnblogs.com/coldplayerest/archive/2010/02/20/1669949.html
free -h
三、问题排查
内存和SWAP问题
7、虚拟机类加载机制
一、虚拟机把表示类的class文件加载到内存,通过校验、转换解析、初始化,最终造成能够被虚拟机直接使用的java类型,这就是虚拟机的类加载机制
二、类加载的时机
- 使用new关键字实例化对象的时候、读取一个类的静态字段的时候、调用类的静态方法的时候
- 使用java.lang.reflect包的方式对类进行反射调用的时候
- 初始化类,发现其父类还未初始化,须要对父类进行初始化
三、类加载的过程
- 加载。经过类的全限定名获取到定义此类的二进制字节流。将字节流所表明的静态存储结构转化成方法区的运行时数据结构。在方法区生成这个类的java.lang.Class对象。加载阶段和链接阶段的部份内容是交叉进行的。用户能够经过本身写的类加载器去控制字节流的获取方式(重写类加载器的loadClass()方法),
- 验证。是链接阶段的第一步。目的是确保class文件中的二进制字节流符合虚拟机的要求,不会危及虚拟机自身安全。包括文件格式验证、元数据验证、字节码验证
- 准备。是链接阶段的第二步。是正式为类变量分配内存空间和设置初始值的阶段。这个初始值和初始化阶段的赋值不一样,这里指的是变量的默认初始值。另外,若是时final修饰的变量,那么会在准备阶段赋予代码里指定的初始值
- 解析。是链接阶段的第三步。是虚拟机将符号引用替换为直接引用的过程
- 初始化。根据程序代码去初始化类变量和其余资源
四、类加载器
- 被不一样类加载器加载的同名类,也认为是不一样的类。
- 双亲委派模型。分为两种类加载器: 1 是启动类加载器 ,是虚拟机自身的一部分;2 是全部的其余类加载器,这些类加载器都由java语言实现。独立于虚拟机外部,所有继承自java.lang.ClassLoader抽象类。类加载器具体层次关系:启动类加载器->扩展类加载器->系统类加载器->自定义类加载器。每个类的加载,会优先由父加载器来加载。这种方式就称为双亲委派,双亲委派保证了java基本类的不会被破坏和替代
第五部分 高效并发
12、Java内存模型与线程
一、硬件的效率与一致性
- 完成计算任务,处理器必须和内存交互才能完成,好比读取运算数据,写入计算结果等。这个I/O操做是很难消除的。计算的处理器和存储设备的运算速度有几个数量级的差距。因此现代计算机加入了一层读写速度尽量接近处理器的高速缓存
- 高速缓存解决了处理器和内存的速度矛盾,却引入了新的问题:内存一致性。多处理器系统中,各个处理器都有本身的高速缓存,又同时共用内存。为了解决这一问题,在读写内存时须要遵循缓存一致性协议。
- 处理器会对输入的代码进行乱序执行优化,相似的,Java虚拟机也存在着指令重排序优化。
二、Java内存模型
Java内存模型规定,全部的变量(这个变量和java编程中的变量有区别,它包括了实例字段、静态字段。不包括局部变量和方法参数,由于后者是线程私有的)都存储在主内存,每条线程有本身的工做内存,工做内存中保存了该线程使用到的变量的拷贝副本,线程对变量的全部操做都必须在工做内存中进行,线程间变量值得传递需经过主内存来完成
主内存和工做内存间交互协议,8种原子操做:
- lock(锁定主内存)
- unlock(解锁主内存)
- read(读取主内存,为load准备)
- load(载入主内存至工做内存)
- use(执行引擎使用工做内存)
- assign(接受执行引擎计算后的值赋值给工做内存)
- store(存储工做内存至主内存,为write准备)
- write(把工做内存写入主内存)
volatile是java虚拟机提供的轻量级的同步机制,对于volatile变量的特殊规则:
- 保证了变量对全部线程的可见性,当一个线程修改了这个变量的值,修改后的值对其余线程来讲是当即可见的。普通变量,须要经过把新值会写到主内存,其余线程从主内存读取以后才能够看到最新值
- 禁止指令重排序优化。
- 没法保证符合操做的原子性,好比i++
- 经过内存屏障实现的可见性和禁止重排序。不一样硬件实现内存屏障的方式不一样,Java内存模型屏蔽了这些差别,由JVM来为不一样的平台生成相应的机器码来完成。X86 处理器只会对写-读进行指令重排序,写volatile变量时,会加lock总线锁,将cpu缓存写入主存,其余cpu的读都会被阻塞,而后其余核的缓存某些对应数据会被标记为失效,那么其余核下次读的时候先读缓存发现失效了,而后去主存读
关于long和double类型变量的特殊规则:容许虚拟机将没有被volatile变量修饰的64位数据的读写操做划分为两次32位的操做来进行。这点就是long和double的非原子性协定
三、Java与线程
Java虚拟机实现线程,有三种方式:
(1)经过内核线程实现。jvm中的一个线程对应一个轻量级进程,一个轻量级进程对应一个内核线程。CPU经过调度器对线程进行调度。缺点:
- 因为基于内核线程实现,各类线程操做须要系统调用,系统调用代价较高,须要在用户态和内核态之间来回切换
- 每一个线程都须要一个内核线程的支持,所以轻量级进程会消耗内核资源,一个系统支持的轻量级进程是有限的

(2)使用用户线程实现。不须要切换回内核态,也能够支持规模更大的线程数量。部分高性能数据库的多线程就是使用用户线程实现的。缺点是没有系统内核的支援,全部问题须要本身考虑,程序实现比较复杂
(3)内核线程和用户线程结合

(4)JVM,对于Sun JDK来讲,在Windows和LInux系统下,都是使用的一对一的线程模型实现的。
Java线程调度
- 协同式线程调度。线程的执行时间由本身控制,线程执行完毕,会主动通知系统
- java使用的是抢占式调度。每一个线程有系统分配执行时间,线程的切换也有系统来决定,线程的执行时间是可控的。线程能够设置优先级,来争取更多的执行时间。Java一共设置了10个优先级,操做系统的优先级数量可能和java定义的不一致,另外操做系统还能够更改线程的优先级,因此Java中优先级高的线程并不必定被优先执行。
Java线程状态转换



十3、线程安全与锁优化
高效并发是从jdk1.5 到jdk1.6的一个重要改进,HotSpot虚拟机开发团队耗费了大量的精力去实现锁优化技术
- 自旋锁与自适应自旋。同步互斥对性能最大的影响就是线程挂起、恢复须要从用户态切换到内核态,切换的过程会形成系统消耗。每每锁定的代码段执行时间很是短,为了这个短的时间去挂起和恢复是不值得的。因此提出了自旋锁的概念,当线程申请获取一个其余线程占用的锁时,这个线程不会当即挂起,而是经过必定次数的循环自旋,这个过程不会释放cpu的控制权,自适应自旋就是根据上一次自旋的结果来决定这一次自旋的次数
- 锁消除。虚拟机即时编译器在运行时会把检测到不可能发生共享数据竞争的锁消除
- 锁粗化。一系列的操做都是对同一个对象的加锁和解锁,虚拟机检测到这种状况会将锁的范围扩大(粗化)
- 轻量级锁
- 偏向锁。若是程序中大多数的锁老是被多个线程访问,那偏向锁模式就是多余的。可使用参数 -XX:-UseBiasedLocking来禁止偏向锁