JVM!
概览
历史发展和趋势
- Java的体系结构
1.在各个硬件平台上的java虚拟机
2.JavaAPI类库
3.来自商业和开源社区的第三方Java类库
4.Java语言设计语言
JDK:Java语言、java虚拟机、JavaAPI类库
JRE:JavaAPI类库的javase子集和java虚拟机
java领域划分:JAVAME/JAVASE/JAVAEE
- JAVA的趋势
1.模块化 2.混合语言 3.多核并行 4.丰富的语法 5.64位虚拟机
自动内存管理机制
内存结构
主要分析JAVA虚拟机运行时的数据区:
线程共享的:堆、方法区
线程私有的:栈、寄存器、本地方法栈
- 概念
- 程序寄存器
当前线程所执行的字节码的行指示器。目的:线程切换可以恢复到正确的执行位置。
如果是Native方法,计数器值为空Undefined,java规范中唯一没有规定任何outofmemoryerror情况的区域。
- Java虚拟机栈
每个方法执行的同时创建一个栈帧(局部变量表、动态链接、返回地址、操作数栈等);
局部变量表:基本数据类型、对象引用指针、returnAddress类型;long和double占2个局部变量空间Slot,其余的都是1个。
栈的深度大于虚拟机所允许的深度:StackOverflowError;如果虚拟机栈动态扩展,如果无法申请足够内存,OutOfMemoryError异常。
- 本地方法栈
与虚拟机栈类似,调用的Native方法。也会抛出StackOverflowError和OutofMemoryError。
- 堆
内存中最大的一块。唯一的目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存、数组。栈上也会分配一部分。
垃圾回收的主要区域,也称为GC堆。从内存回收的角度,垃圾回收器的分代算法:细分新生代、老年代。从内存分配的角度看,java堆又划分出多个线程私有的分配缓冲区TLAB。这样进一步的划分其实就是为了回收内存,更快的分配内存。通过-Xmx和-Xms控制,如果无法分配抛出OutOfMemoryError。
- 方法区
虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。它是堆的一个逻辑部分。
永久代和元空间就是去实现方法区,方法区是规范是接口。这个区域的垃圾回收行为很少出现,可以针对常量池的回收和对类型的卸载。
- 运行时常量池
是方法区的一部分。Class文件中除了类的版本、字段、方法、接口等描述信息外,还有一项信息常量池,由于存放编译器生成的各种字面量和符号引用。运行期间的String的intern方法也会存入常量池中。
- 直接内存
这个区不是虚拟机运行时数据区的一部分。如NIO类,引入了通道的I/O的方式,它可以使用Native函数库直接分配堆外内存,可以通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。也会导致OutOfMemoryError异常。
- 对象
new的流程:
1,去常量池定位一个类的符号引用,并且检查这个类是否被加载、解析、初始过。如果没有就先执行加载过程。 2,加载检查通过后,虚拟机为新生对象分配内存。类的内存大小在加载完成是可以确定的。分配空间就是堆内存划分出来,如果java对内存一边是分配的一边是空闲的,中间放一个分界点的指示器,指针向空闲空间移动对象的大小就是“指针碰撞”,如果内存空间不是规整的交错在一起,有个“空闲列表”,去更新记录记录。因此在使用Serial/ParNew等带Compact,规整功能使用指针碰撞,而是用CMS等基于Mark-Sweep算法的使用空闲列表。
所以多线程分配内存的时候会出现指针修改问题。一种是分配内存时进行同步操作,其实虚拟机是使用CAS配上失败充实方法保证更新操作的原子性;另一种是把在内存分配划分不同空间之间进行,即每个线程在Java堆中分配一个小块内存TLAB,本地线程分配缓冲。只有在TLAB用完兵分配新的TLAB时,才需要同步锁定,可以使用-XX:+/-UserTLAB参数来设定。
3,内存分配完之后,虚拟机将分配到的内存空间都初始化为零值,如果是TLAB分配时,提前到TLAB中进行。这个操作为的是保证对象的实例在java代码中可以不赋值直接使用,这些字段的数据类型所对应的零值。
4,接着对对象进行必要的设置,对象头中的信息。
从虚拟机来说一个对象已经产生了,从java程序的视角,对象创建才刚开始,所有字段都是零,
-
对象的内存布局
-
对象头中的信息
-
实例数据
-
对齐填充
-
-
对象的访问定位
栈上的reference数据来操作推上的具体对象。访问方式有使用句柄和直接指针两种。
句柄:有个句柄池,先去句柄池中找到指针,在去实例池和对象类型中匹配实例。在对象移动时,只改变句柄中实例数据的指针就行,reference本身不需要修改。
直接指针:java堆中必须考虑如何防止访问类型的数据的相关信息, 而reference中存储的直接是对象地址。访问速度更快节省一次指针定位的时间开销。
OutOfMemoryError异常
- java堆溢出
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
dump出来的日志可以使用Eclipse Memory Analyzer对日志文件进行分析。可以很清晰的看到是内存溢出还是内存泄漏。
如果是内存泄漏,可查看GC Roots的引用链。找到具体泄漏对象的类型信息及导致的原因。
如果不是泄漏,还又想系统运行,对象都必须是活的,检查虚拟机的参数,-Xms -Xmx是否还可以增大。
- 虚拟机栈和本地方法栈溢出
可以使用-Xoss参数设置栈的大小
StackOverflowError
系统内存-系统所需的内存-Xmx(最大堆容量)-MaxPermSize(最大方法区容量)-其他基本可忽略的内存=虚拟机栈和本地方法区域
线程的数量将受限于最大堆和减小栈的内存换取。
- 方法区和运行时常量池溢出
1.6之前常量池存于永久代,可以-XX:MaxPermSize -XX:PermSize限制方法区的大小限制常量池的大小
String.intern是一个Native方法,如果常量池中包含一个String对象字符串,则返回此String对象的引用。
1.7及以后的intern不会再复制实例。
方法区用于存放Class的相关信息,对于这些区域的测试使用大量的类去填满方法区。CGLib,javaSSist等对可以操作字节码运行时生成大量的动态类。
- 本地直接内存溢出
-xx:MaxDirectMemorySize指定,如果不指定,则默认与java堆最大值一样。
垃圾回收器
对象死亡标识
- 引用计数法
有一个地方引用该对象就+1,失效一个引用-1,当为0,认为该对象可以死了。微软的Com技术也是使用了它管理内存。但是主流的Java虚拟机里面都没用选用它管理内存,很难解决对象之间相互引用的问题。
- 可达性分析算法
从GC root对象为起点,引用关系可以组成引用链,如果一个对象没有任何引用连到gc root节点,则该对象就不可用了。
GC Root节点的对象是:
1,虚拟机栈(栈帧中的本地变量表)中引用的对象 2.方法区中类静态属性引用的对象 3,方法区中常量引用的对象 4,本地方法栈中JNI引用的对象
- 引用
无论是引用计数法还是可达性分析,判断对象存活都与引用有关。如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,这块内存代表一个引用。
如果这么做,就是要么引用,要么被引用,显然太狭隘了,如果内存大部分空闲的时候可以保留的,用做以后可能用的缓存。
因此java1.2后加入了引用类型划分:
强引用,垃圾回收器永远不会回收掉被引用的对象,如Object o=new Object,代码中普遍存在
软引用,softReference,对于这类关联的对象,在系统发生内存溢出异常之前,将会把这些对象列劲回收范围之中进行第二次回收。
弱引用,weakReference,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
虚引用,为一个对象设置虚引用关联的唯一目的就是能在这个对象收集器回收时收到一个系统通知。除此之外没有任何作用。
- 判定生死
当可达性分析不可达对象时,也不是马上杀死的。要经历两次标记过程。
如果发现一个对象没有与GCRoot关联的引用链,那它被第一次标记并且进行一次筛选,是否有必要执行finalize()方法,当对象没有复写该方法,或者已经被虚拟机调用过了,则虚拟机将视为没必要执行。如果对象要执行finalze()方法,将该对象防止到F-Queue队列之中,稍后虚拟机内部一个线程专门执行该方法。但并不能认为该方法肯定能执行结束,防止死锁后,整个虚拟机也无法销毁其他的对象了。finalze(),方法是对象逃离死亡最后一次机会,如果想逃离死亡,就要关联一个对象进行关联如(this)。那么第二次标记的时候就会被移除即将回收的集合。如果不行,那真的被回收了。
finalize方法会执行一次,也就是逃脱一次,免死金牌,下次再回收就无法使用了。尽量避免使用它。忘了它吧,太惨了
- 回收方法区
很多人认为方法区(永久代)没有垃圾回收的。
主要回收废弃的常量和无用的类。没有人用就判定为没人用了
无用类的判定(同时满足): 1,该类的所有实例都已经被回收 2,加载该类的ClassLoader已经被回收 3,该类的java.lang.Class对象没有任何地方医用。通过反射引用
使用该类要不要回收,使用-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看卸载和加载信息。
垃圾收集算法
- 标记-清除算法
最基础的算法,先标记,再清除,两个不足:1,两个过程的效率都不高,2,空间碎片太多,导致分配较大的对象时又需要提前触发一次垃圾收集动作
- 复制算法
为了解决效率问题,将可用内存按照容量划分为大小相等两块,只能使用一块。当一块内存用完了,把存活的复制到另一块,清除掉已使用这块。代价明显:可用内存降为一半了。
研究表明:98%的对象都是朝生夕死,,所以没必要1:1划分内存。而是将内存划分为Eden和一个Survivor,当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间中,然后清除掉Eden和刚才用过的Survivor空间。默认Eden:Survivor=8:1,也就是只有另外空闲的10%被浪费了。其实98%在一般情况可以回收的。
当然了,没办法保证每次回收都只有不多余10%的对象存活,当Survivor空间不够用时,需要依赖于其他内存(老年代)进行分配担保(Handle Promotion)
就是当另一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过担保机制进入老年代。
- 标记整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率就会变低。有了分配担保后,老年代一般不能直接选用这种算法。
那么针对老年代的特点,提出标记整理算法,标记过程还跟以前一样,标记完成之后不是直接对对象进行清理,而是让所有存活的对象都向一端移动,然后直接清除掉边界意外的内存。
- 分代收集算法
当代都是用分代收集算法,新生代使用复制算法,老年代使用标记清理或者标记清除算法。
垃圾收集器
垃圾收集器是垃圾收集方法的具体实现。没有万能的收集器,需要根据业务场景具体选择。
- Serial收集器
单线程收集,当用户线程使用到safepoint节点时,stop the world,然后使用Serial清理垃圾。在单个Cpu的环境中Serial收集器简单高效。在用户桌面应用场景中,内存一般不会很大,收集一两百兆的新声带,可以控制到几十毫秒最多一百多毫秒,只要不频繁,在Client模式下的虚拟机来说是个很好的选择。
- ParNew 垃圾收集器
它是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其他的与Serial收集器(-XX:survivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、stop the world、对象分配规则、回收策略一样。
它是Server模式下的虚拟机中首选的新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合使用。
ParNew 也是使用-XX:+UserConcMarkSweepGC选项后的默认新生代收集器。也可以使用-XX:+UseParNewGC选项来强制指定它
在多CPU的情况下,可以使用-XX:ParallelGCThreads的参数设置垃圾回收器的线程数。
并行:多个垃圾收集线程并行工作,此时用户线程处于等待状态。
并发:用户线程与垃圾收集线程同时执行,(可能是交替执行),用户程序在继续执行,而垃圾回收器在另一个cpu上
- Parallel Scavenge收集器
也是新生代的收集器,也是复制算法的收集器,优势并行的多线程收集器
CMS收集器尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是打到一个可控制吞吐量。
吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间比值。
停顿时间越短适合需要与用户交互的程序,良好的响应速度能提升用户体验,高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务。主要适合后台交互不多的运算任务。
精确控制吞吐量:最大停顿时间,-XX:MaxGCPauseMillis,直接设置吞吐量大小:-XX:GCTimeRation参数
还可以自适应调节策略
- Serial Old 垃圾收集器
Serial收集器用于老年代的一个单线程收集器,使用标记整理算法。主要用于Client模式下的虚拟机使用。
用途:
1,1.5以及以前版本中与Parallel Scavenge收集器搭配使用 2,作为CMS收集器的后备预案,在CMS并发收集,Concurrent Mode Failure时使用
- Parallel Old 收集器
是Parallel Scavenge收集器老年代版本,多线程和标记整理算法,吞吐量有限。配合Parallel Scavenge使用。
- CMS 垃圾收集器
CMS(Concurrent Mark Sweep) :一种以获取最短回收停顿时间为目标的收集器。
互联网网站、B/S系统服务器端,关注用户体验。标记清除算法
1,初始标记
仍然要stop the world,GC Root直接关联的对象,速度快
2,并发标记
GC Roots Tracing的过程
3,重新标记
仍然要stop the world,
4,并发清除
并发标记和并发清除阶段,它与用户线程同时工作
优点:并发收集、低停顿
缺点:
由于会占用一部分CPU资源,总吞吐量会降低
无法清理浮动垃圾
有大量的空间碎片,无法有足够大的连续空间来分配当前对象不得不提前触发一次Full GC。为了解决这个问题,-XX:+UseCMSCompactAtFullCollection开关
- G1 收集
面向服务器端的垃圾回收器用于替代CMS.
特点: 并行与并发、分代收集、空间整合、可预测停顿
与前面的收集器不同,收集的范围是整个新生代或者老年代。G1收集器时,将整个Java堆划分多个大小相等的独立区域,有新生代和老年代的概念,但是不再是物理隔离的了,都是Region(不连续的)集合
Region里面的垃圾堆积的价值大小
Region之间的对象引用,都有一个对应Remembered set,虚拟机发现程序在堆Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,如果引用对象处于不同Region,通过CardTable把相关的引用信息记录到被引用对象所属的Remembered Set中
1,初始标记 2,并发标记 3,最终标记 4,筛选回收
- GC 日志
最前面的数字,代表GC发生的时间,这个数字的含义是从java虚拟机启动以来经过的秒数;
接着GC类型
Full GC 说明发生了Stop-The-world,Full GC一般出现在分配担保失败之类的问题
接着发生的区域:DefNew、Tenured、Perm
DefNew - 在新生代中使用Serial收集器的 ParNew,使用Parallel NewGeneration,PSYoungGen,使用Parallel Scavenge收集器
3324K->152K(3712k), 已使用的容量->GC收集后已使用的容量(该区域总容量)
之后GC前堆已使用的容量->GC后堆已使用的容量(java堆总容量)
1.111323Secs,GC所占用的时间秒[Times:user=0.01 sys=0.00,real=0.02 secs] 用户消耗的cpu时间、内核消耗的cpu时间和操作从开始到结束果果的墙钟时间。
墙钟时间包含了,IO,线程等待时间
垃圾回收器参数总结
UseSerialGC UseParNewGC UseComcMarkSweepGC UseParallelGC UseParallelOldGC survivorRatio PretenureSizeThreshold MaxTenuringThreshold
UseAdaptiveSizePolicy等等。
内存分配和回收策略
- 对象优先在Eden分配
当Eden区没有足够分配空间进行时,虚拟机将发起一次MinorGC,可以使用-XX:PrintGCDetails打印日志
- 大对象直接进入老年代
大对象指,那种很长的字符串以及数组。短命的大对象要尽量避免。使用-XX:PretenureSizeThreshold参数,大于这个设置值得对象直接在老年代分配。
- 长期存活的对象将进入老年代
一次GC后存活的对象,移动到Survivor区中,年龄+1,当大于年龄阈值,将晋升到老年代。通过-XX:MaxTrnuringThreshold设置;
- 动态对象年龄划分
为了更好的适应不同程度的内存状况,虚拟机并不是等到对象的年龄对象必须达到MaxTenuringThreshold菜晋升到老年代的,如果在Survivor空间中相同年龄的所有对象大小的综合大于Survivor空间的一半,年龄大于或者等于改年龄的对象就直接进入老年代。
- 空间分配担保
在发生minorGC之前,虚拟机先检验老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,MinorGC可以确保是安全的。如果不成立,则需要看HandlePromotionFailure设置的担保值是否允许担保。如果允许,那么继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于尝试一次minor GC,如果小于或者不允许,进行一次Full GC。
性能监控与故障处理工具
性能监控
JDK命令行工具
- jps 虚拟机进程状况工具
类似UNIX的ps命令,显示指定系统内所有的HotSpot虚拟机进程
jps -l 输出主类的全名,如果是jar包,输出jar路径
jps -m 输出虚拟机进程启动传递给主类main()函数的参数
jps -v 输出虚拟机进程启动时的JVM参数
- jstat 虚拟机统计信息监视工具
可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集等运行数据
jstat -gc pid 250 20 ,每250毫秒查询一次,一共查询20次
jstat -gcutil pid 监视内容和-gc基本相同,关注已使用空间占总空间的百分比
- jinfo Java配置信息工具
实时的查看和调整虚拟机各项参数
jinfo -v 查看显示的指定的参数列表
jinfo -flag 具体查询
如jinfo -glag CMSInitiatingOccupancyFranction pid
- jmap java内存映像工具
用于生成堆转储快照,如果不使用它,还可以使用-XX:+HeapDumpOnOutOfMemoryError 参数,让虚拟机出现OOM异常自动dump文件
通过-XX:+HeapDumpOnCtrlBreak参数,使用ctrl+break让虚拟机dump文件,还可以在linux系统下使用kill -3退出信号,dump文件
它不仅仅可以获取dump文件,还可以查询finalize执行队列、java堆和永久代的详细信息,如空间利用率、当前用的那种收集器
jmap -dump文件
- jhat 虚拟机堆转快照分析工具
配合jmap搭配使用,一般不这样干。
- jstack java堆跟踪工具
用于生成虚拟机当前时刻的线程快照
jstack -l 堆栈外,显示关于锁的附加信息
jstakc -f 当正常请求不响应时,强制输出线程堆栈
JDK可视化工具
-
JConsole java监视和管理控制台
-
VisualVM 多合一故障处理工具
类文件结构
Class文件是一组以8位字节为基础单位的二进制流
该文件格式有两种数据结构:无符号数和表
无符号数:u1,u2,u4,u8分别代表1,2,4,8个字节描述
表:由多个无符号数作为数据项构成的复合数据类型
- 模数与class文件的版本
CA FE BA BE 00 00 00 32
0xCAFEBABE 开始标志,0x0000代表次版本号,0x0032代表主版本号50表示1.7.0
- 常量池
紧接着主次版本号之后就是常量池入口,常量池的大小不固定,需要一个u2常量池大小的计数器,0x0016表示十进制的22,代表有21个常量,1~21.0代表不引用任何常量池,特殊考虑。
常量池中存放字面量和符号引用。
字面量:如文本字符串、声明final的常量值等。 符号引用:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符
通过javap -verbose命令查看class文件翻译后的文件
- 访问标志
常量池之后,紧接着两个字节用于识别一些类或者接口层次的访问信息;
ACC_PUBLIC 表示被publi,ACC_FINAL 表示final
- 类索引、父类索引与接口索引的集合
如 类索引是0x0001去常量池找 const #1 父类索引是0x0003、去常量池找 #3 接口索引是0x0000
- 字段表集合
用于描述接口或者类中声明的变量,B代表byte,C代表char,D代表double,F代表float,I代表int,J代表long,s代表short,Z代表boolean,V代表void,L代表对象类型
[一维数组,[[代表二维数组
void inc()使用()V表示,String toString()使用()Ljava/lang/String; int indexof(char[] s,int sof,int end,char[] t,int t,int tof,int from);使用([CII[CIII)I表示;
- 方法表集合
类构造器“
- 属性表集合
innerClasses 类文件,内部类列表
LineNumberTable 源码行号与字节码指令的对应关系
LocalVarrableTable 方法的局部变量描述
其余的查找对应表太多了,在官网都有定义。
字节码指令
- 数据类型
i代表int,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference
- 加载和存储指令
将一个局部变量加载到操作数栈:iload
将一个数值从操作数栈存储到局部变量表:istore
将一个常量加载到操作数栈:bipush,sipush、ldc,aconst_null,iconst_m1等
扩充局部变量表的访问索引的指令:wide
- 运算指令
加法:iadd,减法:isub,乘法:imul,除法:idiv,求余:irem,取反:ineg,位移:ishl,自增:iinc,比较指令:dcmpg
- 类型转换指令
i2b,i2c,i2s,f2i
- 对象创建于访问指令
new,newArray,getfield,putfield,getStatic
把一个数组元素加载到操作数栈的指令:baload,saload
将一个操作数栈的值存储到数组元素的指令:bastore
取数组长度指令:arraylength
检查实例类型的指令:instanceof checkcase
- 操作数栈的管理指令
出栈:pop、pop2,复制栈顶一个或者两个数值重新压入栈顶:dup,dup2
将站顶端的两个数值互换:swap
- 控制转移指令
条件分支:ifeq,iflt,ifne,ifge,符合条件指令:tablesswitch,无条件分支:goto/goto_w
- 方法调用和返回指令
invokevirtual 调用对象的实例方法
invokeinterface 调用接口方法
invokespecial用于调用一些需要特殊处理的实例方法,如实例初始化方法、私有方法和父类方法
invokestatic 调用类方法static
invokedynamic 动态解析出来的方法
ireturn 返回
- 同步指令
synchronized语句块的表示:monitorenter、monitorexit
虚拟机类加载机制
具体包括类从加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。
其中 加载 - 验证 - 准备 - 初始化 - 卸载 这5个阶段顺序是确定的
5钟情况下必须初始化:
1,遇到new、getstatic、putstatic、invokestatic 2,反射调用时 3,如果父类没有初始化,先需要触发父类的初始化 4,虚拟机先初始化main方法的那个类 5,ref_getstaic,ref_invokeStatic方法句柄的对应类先初始化
类的加载过程
加载阶段,1,通过一个类的全限定名获取定义此类的二进制字节流,2,将字节流所代表的静态存储结构转换为方法区的运行时数据结构
验证,检查
初始化:就是执行类构造器
虚拟机会默认调用
之后调用
- 类加载器
不同的类加载器加载相同的类,结果是不一样的。
双亲委派模型:
启动类加载器:Bootstrap ClassLoader:<java_home/bin> rt.jar中,由于是c++编写的,使用Test.class.getClassLoader().getClassLoader()。输出是null
扩展加载器:ExtClassloader,在lib/ext下的类库,ExtClassloader
应用程序加载器:AppClassLoader
加载一个类的任务先交给父加载器加载,父加载器加载了,子加载器不会再加载
虚拟机字节码执行引擎
那么方法的调用和字节码是如何执行的呢?
- 运行时栈帧结构
栈帧是虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的元素。它存储了:局部变量表、操作数栈、动态链接、返回地址等。每一个方法的调用其实就是一个栈帧的入栈和出栈。
局部变量表Slot复用对垃圾收集的影响因素之一。
把不使用的对象手动赋值为null。
- 操作数栈
两个栈帧共享局部变量表的共享区域
-
动态连接
-
方法返回地址
Java内存模型和线程
Java内存模型
它是来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台下都能达到一致的内存访问效果。
- 主内存和工作内存
目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取得变量这样的底层细节。
此处的变量包括:实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为是线程私有的。不会被共享,自然不会存在竞争问题。
所有变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存保存了被该线程使用到变量的主内存的副本拷贝,线程对变量所有操作都必须在工作内存中进行,而不能直接读写在主内存的变量。不同的线程直接也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
- 内存间的交互操作
lock(锁定):作用于主内存的变量,它把变量标识为一条线程独占的状态;
unlocak(解锁):作用于主内存的变量,把一个处于锁定状态变量释放出来,释放出来的变量,其他线程才能锁定。
read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存得到的变量值放到工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中的一个变量值传递到执行引擎。
assign(赋值):作用于工作内存
store(存储):作用于工作内存,把一个变量的值传递到主内存中,以便随后的wirte使用。
write(写入):作用于主内存的变量,把store从内存得到的变量赋值到主内存中。
将一个变量从主内存复制到工作内存,顺序执行read+load操作,如果需要把变量从工作内存同步到主内存中,顺序执行store+write操作。
这些命名的先后顺序也有相关的规则:
1,不允许read和load、store和write操作之一单独出现。 2,不逊于一个线程丢弃它最近的assign操作,即变量在工作内存中改变之后必须把它同步回主内存中。 3,不有允许一个线程无原因的(没发生过任何assign)把数据从工作内存同步到主内存中 4,一个新变量只能在主内存中诞生 5,一个变量同一个时刻只允许一条线程对其进行lock操作 6.如果一个变量执行lock操作,那将清空工作内存中此变量的值,只执行引擎使用这个变量前,需要重新load或者assign初始化变量的值 7,不允许一个变量unlock,除非lock过 8.对一个变量unlock操作之前,必须把该变量同步到主内存中。
- volatile变量的特殊规则
1,它可以保证此变量对所有线程的可见性,指当一个线程修改了这个变量的值,新值对其他线程来说可以立即得知。基于volatile变量的运算在并发计算的并发下并不是安全的。
它只能保证可见性
2,使用它禁止指令重排优化,普通变量仅仅保证在该方法的执行过程中。
使用lock修饰后,lock$add等操作,相当于一个内存屏障,相当于将执行一次空的(store+write)操作,所以其他线程cpu立即可见
- 对于long和double型变量的特殊规则
java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操作都是原子的,但是对于64位的数据类型long和double相对宽松的规定,并不保证这8个操作的原子性。不过现在也不会出现这种问题
- 原子性、可见性和有序性
原子性,有java内存模型来直接保证原子性变量操作包括lock、unlock、read、load、assign、use、store、write。大致认为基本数据类型的访问读写都是具备原子性的。
如果需要更大的原子操作,使用lock和unlock,或者使用monitorenter和monitorexit,也就是使用synchronized关键字
可见性,一个线程修改了共享变量的值,其他线程能够立即得知这个修改。使用volatile保证可见性,除了该变量外使用synchronized和final。
有序性,java程序在本线程内观察是有序的,如果观察另一个线程是无序的。指令重排和工作内存和主内存同步延迟的现象。使用synchronized和volatile
- 先行发生原则
没有顺序保证的,导致虚拟机对它们进行重排序
1,程序次序规则:控制流顺序,分支、循环 2,管程锁定规则,unlock发生在lock之后 3,volatile变量规则:写操作先行与后面这个变量的读操作 4,线程终止规则,线程的所有操作发在在此线程的终止检查,Thread.join的方法结束、Thread.isAlive()检查线程已经终止执行 5,线程中断规则,对线程interruput()方法的调用先行发生于被中断线程的代码检测到中断事件发生 6,对象终止规则,对象的初始化操作先行与他的finalze方法 7,传递性,a - b - c
线程
线程的实现
实现线程的3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
- 使用内核线程实现
系统调用较高,需要用户态和内核态来回切换。内核资源有限所以支持的数量也有限。
- 使用用户线程实现
没有内核的支援,所有的线程的创建、切换、调度都需要考虑。java最后放弃了
- 使用用户线程加轻量级进程混合实现
java线程调度
线程调度是指系统为线程分配处理器使用权的过程,协同式线程调度、抢占式线程调度
- 状态切换
新建(New):线程创建后未启动的线程;
运行(Runable):包括Runing和Ready,正在执行和等待CPU分配执行时间片
无限期等待(Waiting):没有timeout的object.wait(),没有timeout的Thread.joint()方法,LockSupport.park()方法;
限期等待(Timed Waiting):Thread.sleep()方法,设有timeout的Object.wait(),Thread.joint(),LockSupport.parkNanos(),LockSupport.parkUntill()方法
阻塞(Blocked):线程被阻塞了,阻塞状态是等待获取一个排他锁,这个事件将在另一个线程放弃这个锁的时候发生;等待是等待一段时间或者唤醒动作的发生,
结束(Terminated);
线程安全
按照线程安全的安全程度由强到弱来排序:不可变,绝对线程安全,相对线程安全、线程兼容和线程对立;
不可变:final修饰的变量。如String,枚举类型以及Number的部分子类,BigInteger
绝对线程安全:Vector(所有方法都是使用synchronized修饰)(注意:一个读取,一个删除,则需要synchronized(vector));
相对线程安全:我们通常意义上的线程安全,它需要保证对这个对象单独操作时线程安全的,调用的时候不需要额外的保障措施。Vector,HashTable,Collections的synchronizedCollection包装的集合类。
线程兼容:ArrayList和HashMap,需要考虑线程安全问题
线程对立。
-
线程安全的实现方法
- 互斥同步:
临界区、互斥量、信号量都是主要的互斥实现方式,使用synchronized关键字,ReentranLock
- 非阻塞同步:
互斥同步在线程阻塞和唤醒带来了性能问题。互斥同步是一种悲观的并发策略。当我们有了基于冲突检测的乐观并发策略,先操作,如果共享数据有争用,产生了冲突,那就采取其他的补偿方法(常用的补偿方法就是重试,直到成功为止),这个乐观的并发策略不需要线程挂起。具体时间操作和冲突检测
怎么做呢?
测试并设置、获取并增加、交换、比较并交换(CAS);
CAS:JUC包里面的整数原子类,其中的compareAndSet()和getAndIncrement()等方法使用了Unsafe类的CAS操作
- 无同步方案:
如果一个方法不涉及到共享的数据,那就是天生安全的,无需进行任何同步。
可重入代码,线程本地存储(ThreadLocal类)
锁优化
- 自旋锁和自适应自旋
如果两个并行的线程在很短的时间内可以执行完成,加入同步,线程的挂起和恢复显然是不值得的。稍等一下就可以了,引入了自旋。忙循环
自旋的时间和次数都是可以设置的。
- 锁消除
当有逃逸分析后,发现有些快代码是线程私有的,同步加锁自然无需进行。即使加了锁,但是可以被安全的消除掉,在即使编译之后忽略掉同步直接执行。
- 锁粗化
如果发现一段代码中,频繁的加锁和释放,其实在最外面加一个锁就行了,这其实就是锁粗化,虽然理论上要求锁的代码块越小越好,但是频繁而造成性能问题,需要锁粗化
- 轻量级锁
相对于使用互斥量的传统的重量级锁而言。它不是替换重量级锁;
如果一个线程进入一个锁,那就是轻量级锁,有两个以及以上就是重量级锁。轻量级锁是通过CAS进行的,CAS操作避免了使用互斥量的开销,如果有锁竞争,除了互斥量的开销外,还有额外的CAS操作,因此在竞争的情况下,轻量级锁比传统的重量级锁更慢。这是一种根据经验的取舍。
- 偏向锁
轻量级锁在无竞争的情况下使用CAS操作消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。
JVM OVER!