JVM体系
2021-01-28 15:34:17 0 举报
AI智能生成
JVM脑图
作者其他创作
大纲/内容
性能调优
操作系统优化
Tomcat优化
JVM优化
代码优化
数据库连接池优化
程序编译与代码优化
早期(编译期)优化
语法糖
泛型与类型擦除
自动装箱、拆箱与遍历循环
条件编译
晚期(运行期)优化
编译优化技术
公共子表达式消除
数组范围检查消除
方法内联
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。
甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸
Java的认知
优点
1、它摆脱了硬件平台的束缚,实现了“一次编写,到处运行”的理想
2、它提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄露和指针越界问题
3、它实现了热点代码检测和运行时编译及优化,这使得Java应用能随着运行时间的增加而获得更高的性能
4、它有一套完善的应用程序接口,还有无数来自商业机构和开源社区的第三方类库来帮助它实现各种各样的功能
内存管理机制
运行时数据区域
线程隔离的数据区域
程序计数器
此区域是唯一一个在java虚拟机中没有规定任何OOM情况的区域
虚拟机栈
栈桢(每个方法在执行的同时都会创建一个栈桢 )
局部变量表(编译期 )
基本数据类型
boolean,byte,char,short,float,long,double,int
对象的引用
reference类型,它不等同于对象本身,
可能是一个指向对象起始地址的引用指针,
也可能是一个代表对象的句柄或者是其他与此队形相关对位置
returnAddress类型
指向一条字节码指令的地址
操作数栈
动态链表
方法出口
虚拟机栈为执行java方法服务
本地方法栈
线程共享的数据区
方法区
本地方法区
方法区的替代者
运行时常量池
Class文件通过类加载器加载后,其中的常量池会进入运行时常量池
动态性
运行时也可以将新的常量放入池中
堆
新生代
Eden
From Survivor
To Survivor
老年代
(堆外内存)直接内存
不是虚拟机运行时数据区的一部分,但该部分也会频繁使用也会发生OOM异常。
内存的分配不会受到java堆大小的限制,但是既然是内存也会受到本机内存总大小的限制。
OOM异常
产生原因
内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
分类
java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出
一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,
需要通过内存监控软件查找程序中的泄露代码,
而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出(jdk1.6)
一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,
因为上述情况会产生大量的Class信息存储于方法区。
此种情况可以通过更改方法区的大小来解决,
使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。
另外,过多的常量尤其是字符串也会导致方法区溢出。
java.lang.OutOfMemoryError: Java heap space ------>java永久代溢出(jdk1.7)
java.lang.OutOfMemoryError: Java heap space ------>java堆溢出(jdk1.8)
java.lang.OutOfMemoryError: MateSpace------>java元空间溢出(jdk1.8)
制定MetaspaceSize和MaxMetaspaceSize的大小
java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。
JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,
栈大小设置太小也会出现此种溢出。
可以通过虚拟机参数-Xss来设置栈的大小。
内存分配与回收策略
对象优先在Eden分配
(1)、将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;
(2)、每次使用Eden和其中一块Survivor;
(3)、当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;
(4)、而后清理掉Eden和使用过的Survivor空间;
(5)、后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;
默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;
大多数情况下,对象在新生代Eden区中分配;
当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC(新生代GC);
Minor GC时,如果发现存活的对象无法全部放入Survivor空间,只好通过分配担保机制提前转移到老年代
大对象直接进入老年代
大对象指需要大量连续内存空间的Java对象,如,很长的字符串、数组;
经常出现大对象容易导致内存还有不少空间就提前触发GC,以获取足够的连续空间来存放它们,所以应该尽量避免使用创建大对象;
"-XX:PretenureSizeThreshold":
可以设置这个阈值,大于这个参数值的对象直接在老年代分配;
默认为0(无效),且只对Serail和ParNew两款收集器有效;
如果需要使用该参数,可考虑ParNew+CMS组合。
长期存活的对象将进入老年代
JVM给每个对象定义一个对象年龄计数器,其计算流程如下:
在Eden中分配的对象,经Minor GC后还存活,就复制移动到Survivor区,年龄为1;
而后每经一次Minor GC后还存活,在Survivor区复制移动一次,年龄就增加1岁;
如果年龄达到一定程度,就晋升到老年代中;
"-XX:MaxTenuringThreshold":
设置新生代对象晋升老年代的年龄阈值,默认为15;
动态对象年龄判定
JVM为更好适应不同程序,不是永远要求等到MaxTenuringThreshold中设置的年龄;
如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代;
空间分配担保
只要老年代最大可用的连续空间大于新生所有对象空间或历次晋升到老年代对象的平均大小,就会进行Minor GC;否则进行Full GC;
Metaspace
JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。
垃圾收集
垃圾收集过程
哪些内存需要回收?
引用计数算法(Recference Counting)
可达性分析算法(Reachability Analysis)
分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题);
再谈引用
强引用
强引用就是指在程序代码之中普遍存在的,类似"Object obj=new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
软引用
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常
弱引用
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
虚引用
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
判断对象生存还是死亡
第一次标记
(A)、没有必要执行
没有必要执行的情况:
(1)、对象没有覆盖finalize()方法;
(2)、finalize()方法已经被JVM调用过;
这两种情况就可以认为对象已死,可以回收;
(B)、有必要执行
对有必要执行finalize()方法的对象,被放入F-Queue队列中;
稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;
第二次标记
GC将对F-Queue队列中的对象进行第二次小规模标记;
finalize()方法是对象逃脱死亡的最后一次机会:
(A)、如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;
(B)、如果对象没有,也可以认为对象已死,可以回收了;
一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;
什么时候回收?
如何回收?
垃圾收集算法
垃圾收集算法
标记-清除
这个方法是将垃圾回收分成了两个阶段:标记阶段和清除阶段。
在标记阶段,需要标记出所有需要回收的对象。
在清除阶段,清除掉所有的被标记的对象。
这个方法的缺点是,垃圾回收后可能存在大量的磁盘碎片,准确的说是内存碎片。因为对象所占用的地址空间是固定的。对于这个算法还有改进的算法,就是我后面要说的算法三。
标记-整理(Java中老年代采用)
在算法二的基础上做了一个改进,可以说这个算法分为三个阶段:标记阶段,压缩阶段,清除阶段。标记阶段和清除阶段不变,只不过增加了一个压缩阶段,就是在做完标记阶段后,
将这些标记过的对象集中放到一起,确定开始和结束地址,比如让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,将不会产生磁盘碎片。但是我们也要注意到几个问题,压缩阶段占用了系统的消耗,
并且如果标记对象过多的话,损耗可能会很大,在标记对象相对较少的时候,效率较高。
复制算法(Java中新生代采用)
核心思想是将内存空间分成两块,同一时刻只使用其中的一块,在垃圾回收时将正在使用的内存中的存活的对象复制到未使用的内存中,然后清除正在使用的内存块中所有的对象,
然后把未使用的内存块变成正在使用的内存块,把原来使用的内存块变成未使用的内存块。很明显如果存活对象较多的话,算法效率会比较差,并且这样会使内存的空间折半,但是这种方法也不会产生内存碎片。
分代法(Java堆采用)
主要思想是根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率。
比如Java虚拟机中的堆就采用了这种方法分成了新生代和老年代。然后对于不同的代采用不同的垃圾回收算法。
新生代使用了复制算法,老年代使用了标记-整理算法。
垃圾收集器
使用
新生代收集器
Serial
场景
依然是虚拟机运行在Client模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
ParNew
场景
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作
但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销
Parallel Scavenge
场景
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间
老年代收集器
Serial Old
CMS
特点
针对老年代
基于"标记-清除"算法(不进行压缩操作,产生内存碎片)
以获取最短回收停顿时间为目标
并发收集、低停顿
需要更多的内存(看后面的缺点)
应用场景
与用户交互较多的场景;
希望系统停顿时间最短,注重服务的响应速度;
以给用户带来较好的体验;
如常见WEB、B/S系统的服务器上的应用;
处理过程
初始标记(CMS initial mark)
仅标记一下GC Roots能直接关联到的对象;
速度很快;
但需要"Stop The World";
并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
应用程序也在运行;
并不能保证可以标记出所有的存活对象;
重新标记(CMS remark)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
并发清除(CMS concurrent sweep)
回收所有的垃圾对象;
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;
所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
缺点
对CPU资源非常敏感
并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
CMS的默认收集线程数量是=(CPU数量+3)/4;
当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
产生大量内存碎片
由于CMS基于"标记-清除"算法,清除后不进行压缩操作;
总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;
但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;
Parallel Old
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;
特点
针对老年代
采用"标记-整理"算法
多线程收集
应用场景
JDK1.6及之后用来代替老年代的Serial Old收集器
特别是在Server模式,多CPU的情况下
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合
整堆收集器
G1
特点
并行与并发
能充分利用多CPU、多核环境下的硬件优势;
可以并发来缩短"Stop The World"停顿时间;
也可以并行让垃圾收集与用户程序同时进行;
分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象;
虽然保留分代概念,但Java堆的内存布局有很大差别;
将整个堆划分为多个大小相等的独立区域(Region);
新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
结合多种垃圾收集算法,空间整合,不产生碎片
从整体看,是基于标记-整理算法;
从局部(两个Region间)看,是基于复制算法;
这是一种类似火车算法的实现;
都不会产生内存碎片,有利于长时间运行;
可预测的停顿:低停顿的同时实现高吞吐量
G1除了追求低停顿处,还能建立可预测的停顿时间模型;
可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;
运作过程
初始标记(Initial Marking)
仅标记一下GC Roots能直接关联到的对象;
且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
需要"Stop The World",但速度很快;
并发标记(Concurrent Marking)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
耗时较长,但应用程序也在运行;
并不能保证可以标记出所有的存活对象;
最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
上一阶段对象的变化记录在线程的Remembered Set Log;
这里把Remembered Set Log合并到Remembered Set中;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
筛选回收(Live Data Counting and Evacuation)
首先排序各个Region的回收价值和成本;
然后根据用户期望的GC停顿时间来制定回收计划;
最后按计划回收一些价值高的Region中垃圾对象;
回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
可以并发进行,降低停顿时间,并增加吞吐量;
应用场景
面向服务端应用,针对具有大内存、多处理器的机器;
最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
用来替换掉JDK1.5中的CMS收集器;
在下面的情况时,使用G1可能比CMS好:
(1)、超过50%的Java堆被活动数据占用;
(2)、对象分配频率或年代提升频率变化很大;
(3)、GC停顿时间过长(长于0.5至1秒)
可预测的停顿
可以有计划地避免在Java堆的进行全区域的垃圾收集;
G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
分类
并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态; 如ParNew、Parallel Scavenge、Parallel Old;
并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上; 如CMS、G1(也有并行);
串行(Serial)
垃圾收集种类
Minor GC
又称新生代GC,指发生在新生代的垃圾收集动作;
因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
Full GC
又称Major GC或老年代GC,指发生在老年代的GC;
出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上;
GC日志分析
GC日志开头的"[GC"和"[Full GC"说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有"Full",说明这次GC是发生了Stop-The-World的。
Serial收集器:DefNew
ParNew收集器:ParNew
Parallel Scavenge收集器:PSYoungGen
虚拟机执行子系统
类文件结构
无关性的基石
平台无关性
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石
语言无关性
实现语言无关性的基础仍然是虚拟机和字节码存储格式
Class类文件的结构
“Class文件”是一组以8位字节为基础单位的二进制流
伪结构存储数据
无符号数
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
表
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾
Class文件本质上就是一张表
魔数与Class文件的版本
魔数:每个Class文件头4个字节(咖啡宝贝),唯一作用用于确认是否可以被虚拟机识别
版本:紧接着的4个字节,又分2个字节的次版本号和2个字节的主版本号,高版本的JDK能向下兼容以前版本的Class文件
常量池
1、Class文件中的资源仓库
2、常量池中主要存放两大类常量:字面量和符号引用
1)、字面量比较接近java中常量的概念,如文本字符串、声明为final的常量等
2)、符号引用则属于编译原理方面的概念,主要包括下面三类常量
a、类和接口的全限定名
b、字段的名称和描述符
c、方法的名称和描述符
访问标志
这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等
类索引、父类索引与接口索引集合
用于确定类的继承关系
字段表集合
字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。包含的信息有:字段的作用域、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile)、可否被序列化、字段数据类型、字段名称。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
方法表集合
类似字段表集合,与字段表集合相对应的,如果父类方法在子类中没有被重写,方法表中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器""方法和实例构造器""方法
特征签名
java:方法名、参数个数、参数类型
jvm:方法名、参数个数、参数类型、返回值
属性表集合
Code数据表
虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令
字节码指令
字节码指令
同步指令
synchronized这种同步机制是使用管程(Monitor)支持的
当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程
Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持
类加载机制
运行期间完成,动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性
类加载的时机
java虚拟机规范中没有强制约束什么时候进行类加载,只是严格规定了有且只有5中情况必须立即对类进行"初始化"
遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
类加载过程
加载
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,
首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
其次,这里所说的初始值“通常情况”下是数据类型的零值
如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析过程中如果找不到直接引用,则调用类加载器加载
初始化
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕
类加载器
启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。
双亲委派机制
工作流程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
大致是“先上后下“的过程
具体过程
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
好处
Java类随着它的类加载器一起具备了一种带有优先级的层次关系
破坏双亲委派模型
线程上下文类加载器
类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置
JNDI服务
字节码执行引擎
字节码指令
加载和存储指令
运算指令
类型转换指令
对象创建与访问指令
操作数栈管理指令
控制转移指令
方法调用和返回指令
异常处理指令
同步指令
执行引擎
解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)
运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素
局部变量表
操作数栈
动态链接
方法返回地址
方法调用
确定被调用方法的版本,不涉及方法体的执行
解析
调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution),在类加载的解析阶段完成
静态方法
类型直接管理
私有方法
外部不可访问
实例构造器
父类方法
分派
静态分派
重载(Overload)
静态类型
实际类型
重载时是通过参数的静态类型而不是实际类型作为判定依据的
动态分派
重写(Override)
动态类型语言
特征
它的类型检查的主体过程是在运行期而不是编译期,如JavaScript、Python
变量无类型而变量值才有类型
高效并发
Java内存模型与线程
Java内存模型
主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节
线程、工作内存、主内存三者的交互关系
内存间交互操作
8种操作:lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)
Java内存模型还规定了在执行上述8种基本操作时必须满足8种规则
不允许一个变量从主内存读取了但工作内存不接受的情况或不允许一个变量回写到主内存但主内存不接受的情况发生
变量在工作内存中改变后,一定同步回主内存(没说立即同步)
不允许一个线程无原因(无赋值操作)的把工作内存的数据同步到主内存中
一个变量只能在主内存中诞生,工作内存不能使用未初始化过的变量
同一个变量同一时刻只允许一个线程对其锁定
如果对一个变量执行加锁操作,那将会清空工作内存中该变量的值,在执行引擎使用该变量前重新读取
如果一个变量事先没有被锁定,那么这个变量就不能执行解锁操作
对一个变量执行解锁之前,必须先把这个变量从工作内存同步回主内存中
对于volatile型变量的特殊规则
可见性
是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的
不符合以下两条规则的运算场景中,通过加锁来保证原子性
运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
变量不需要与其他的状态变量共同参与不变约束
有序性
禁止指令重排序优化
线程内表现为串行的语义
内存屏障
内存间交互的特殊规则
使用volatile变量时,先从主内存取出数据并刷新工作内存
修改volatile变量时,立刻同步回主内存
对于long和double型变量的特殊规则
原子性、可见性与有序性
先行发生原则
程序次序规则
管程锁定规则
volatile变量规则
线程启动规则
线程终止规则
线程中断规则
对象终结规则
传递性
Java与线程
线程的实现
Java线程调度
协同式线程调度
抢占式线程调度
状态转换
新建(New)
创建后尚未启动的线程处于这种状态
运行(Runable)
此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间
等待(Waiting)
线程不会被分配CPU执行时间,等待被其他线程显式地唤醒或由系统自动唤醒
阻塞(Blocked)
等待着获取到一个排他锁
结束(Terminated)
已终止线程的线程状态,线程已经结束执行
线程安全与锁优化
线程安全
Java语言中的线程安全
定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,
也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,
调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的
共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
线程安全的实现方法
互斥同步
非阻塞同步
无同步方案
锁优化
自旋锁与自适应自旋
忙循环
锁消除
根据逃逸分析判断锁是否可以消除
锁粗化
轻量级锁
偏向锁
0 条评论
下一页