深入理解Java虚拟机学习笔记
2021/5/20 12:28:55
本文主要是介绍深入理解Java虚拟机学习笔记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
参考《深入理解Java虚拟机》
JVM运行时数据区
程序计数器(PC寄存器)
程序指针,每个线程单有一个,若执行本地方法应该为空,唯一没有规定OutOfMemoryError的区域
虚拟机栈
线程私有,每个方法一个栈帧主要存储,局部变量表,操作数栈,动态链接,方法出口信息,会有栈深度异常(StackOutflowError)与栈容量不足异常(OutOfMemoryError)
本地方法栈
与虚拟机栈类似不过主要用于本地方法执行,同样有栈深度异常(StackOutflowError)与栈容量不足异常(OutOfMemoryError)
Java堆
所有线程共享,主要存放对象实例(有些对象采样栈上分配,标量替换),可以为多个线程分配私有缓存区来提高效率,会发生堆空间不足异常(OutOfMemoryError)
方法区
所有线程共享,主要存放已加载的类型信息,常量,编译代码缓存等,堆的逻辑空间(非堆空间)
实现JDK6以前永久代(包含字符串常量池,静态变量)以后使用元空间(本地内存)并把字符串常量池,静态变量移动到堆空间
会发生内存不足异常(OutOfMemoryError)
运行时常量池
方法区的一部分,用于存放类文件的常量池表运行时依旧可以把新的常量放入,String.intern()(使用永久代时)因为是方法区的一步分同样会发生内存不足异常(OutOfMemoryError)(1.7时包含字符串常量池,之后移除了)
分配空间需要同步
使用CAS失败重试分配空间
为每个线程分配本地缓存区,先分配到本地缓存区,满了需要分配新的缓存区时才需要锁定
内存分布完毕必须设置零值保证 不赋值也可以直接使用
为对象分配内存
指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
为什么大对象直接进入老年代
大对象分配在伊甸园区,会造成来回复制影响效率
对象的存储布局
对象头,实例数据,对齐填充(为了满足整体8的倍数)
对象头信息
一般两个部分,第一个部分称为Mark Word 主要存储对象HashCode,分代年龄,锁状态,为了存放这些信息 会对空间进行复用,第二个部分存放对象类型信息指针,数组有3个字节,第三部分存放数组长度,前两部分多少位JVM占多少bit,最后一部分恒定32bit
对象访问定位
通过句柄访问,堆上会有句柄池与实例池,句柄包含实例指针,类型指针,优点对象移动只需要改实例指针(所以引用对象都会修改)
通过直接指针访问,直接指向堆空间对象实例,对象实例包含类型指针,优点节约一次指针定位开销
String.intern()
若对象在字符串常量池存在,直接放回,若不存在添加到常量池放回,JDK7以后字符串常量池在堆中,不再需要复制,只需要记录一下引用
可达性分析标记存活对象
GCRoot
虚拟机栈(栈桢中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI的引用的对象
finalize()
若对象没有覆盖该方法,或执行过了,都不会在执行该方法,该方法在对象销毁前调用由优先级最小的Finalizer线程调用不保障执行,若调用过程中可以自救(把自己挂载到全局变量等)
方法区回收
一般不回收,因为效率太低,静态变量必须所有都没有引用才能回收,类型对象需要
1所有实例被回收,2类加载器被回收(必须自定义类加载器),3没有反射使用类型
分代搜集理论
弱分代假说:绝大多数对象朝生夕灭
强分代假说:熬过越多垃圾回收过程的对象越难消亡
跨代引用假说:跨代引用相对于同代引用是极少的(若一个对象长时间没有消亡,它引用的对象往往也不会轻易消亡)
标记清除算法
分为两步,标记-》清除,可以标记要清除的,也可也标记要保留的
缺点:执行效率不稳定,受对象数影响 会产生大量内存碎片
标记复制算法(复制算法)
需要使用一半内存空间,一般用于新生代,E:S0:S1=8:1:1 每次回收E与S[num]直接复制到另一个区域,因为没法保证每次存活对象都不多于10%所以需要其他区域进行内存分配担保(大部分是老年代)
标记整理算法
一般用于老年代,先标记,然后向内存的一段移动并修改引用(需要STW),CMS老年代采用标记清除,若碎片太多会采用标志整理
根节点枚举
根节点枚举需要STW,并不需要从CGRoot遍历,有OopMap的数据结构在对象创建后保存其偏移
安全点(方便垃圾搜集)
在程序执行过程中,所有堆对象内容一致的点
安全区域扩展拉伸的安全点
让线程处于安全点的方法
抢先式中断:中断所有用户线程,若发现有线程位处于安全点,允许它再跑一会重新中断
主动式中断:设置一个标志,所有线程轮询,若发现处于中断标志,会运行到最近的安全点主动挂起
HotSpot采用内存保护陷阱
为了实现安全点轮询触发中断,HotSpot采用内存保护陷阱,设置内存不可读让线程触发异常,在预先注册的异常处挂起线程实现等待
记忆集用于解决跨代引用问题
卡表是记忆集的具体实现,记忆集只是规范,一个卡页通常不止一个对象,只要有一个对象有跨代引用就标记为1,没有标记为0,垃圾搜集时,把脏的元素一并加入GCRoot进行扫描,主要解决老年代的跨代引用问题
HotSpot通过写屏障更新卡表
注意这里的写屏障与禁止重排序的内存屏障不一样,类似AOP,有写前屏障,写后屏障
伪共享问题
现代处理器缓存系统是以缓存行为单位的,若操作多个变量处于同一缓存行,并发操作会变成串行操作,影响效率
三色标记
黑色,完全扫描且存活 灰色,部分扫描至少还有一个引用没有扫描过 白色还未扫描(若直到最后还是白色,证明不可达)
原来应该是黑色但标识为白色的情况(非常严重)(必须同时满足)
1,插入了一条或多条,黑色对象(黑色对象证明已经扫描完毕不会再扫描)到白色对象的引用(这里的白色应该是黑色的)
2,删除了全部灰色(灰色真正扫描中,该对象将错失扫描)到该白色的引用(这里的白色,也应该标记为黑色)
方法:(随便破坏一条就行了)(都是通过写屏障实现)
增量更新,破坏第一条,若发生黑色对象对白色对象的引用记录下来,等扫描全部结束,再把这些记录的黑色对象为根进行扫描(CMS)
原始快照(SATB),破坏第二条,若发生灰色对象删除白色对象的引用时,记录下来,等扫描结束,再把这些灰色节点为根进行扫描(G1,Shenandoah)
垃圾搜集器
Serial
单线程,直接使用STW进行垃圾搜集,效率高,不过延迟高,适合客服端使用
ParNew
Serial的并行版本,由于与CMS搭配非常好出名(现在已被G1取代),在单核CPU下工作还不如Serial(需要处理线程上下文切换)
ParallelScavenge
类似ParNew不过可以同-XX:MaxGCPauseMillis设置最大暂停时间,或使用-XX:GCTimeRatio设置吞吐量,注意不可以设置的太变态,是均衡的其他指标,可以使用-XX:UseAdaptiveSizePolicy,使其自适应
SerialOld
是Serial老年代版本,同样使用标记整理,是CMS的逃生门
ParallelOld
ParallelScavenge的老年代版本,同样主打吞吐量
CMS
初始标记 STW标记一下GCRoot直接关联的非常快(单线程的)
并发标记 与用户线程并发运行(耗时长)
重新标记 STW纠正一下错误标记(重新标记是并行的)
并发清除 与用户线程并行清除已经死亡的对象
CMS无法清除浮动垃圾(标记结束生成的垃圾),需要消耗过多内存,若没有预留足够内存会引起“并发失败”这样会开启后备方案使用SerialOld清除引起一次大的STW,CMS一般采用标记清除会产生大量内存碎片,会在碎片很多时采用一次标记整理,也可以指定参数,在标记清除几次后进行一次标志整理
G1分块
对年轻代老年代全搜集,会搜集垃圾最多的内存,收益最大化,每个分区大小1M~32M(使用-XX:G1HeapRegionSize设置必须是2的次幂数),把超过一个分区大小一半的设置为大对象,存放在特殊的Humongous Region中,新生代老年代不再固定,会维护一个优先队列,在用户允许的停顿时间(-XX:MaxGCPauseMillis设置)进行利益最大化搜集,每个分区都使用记忆集(双向卡表,卡表只记录我指向谁,它还记录谁指向我)解决跨代对象引用问题(CMS只需要一个卡表单向引用解决老年代引用问题,只记录老年代)
初始标记 STW标记一下GCRoot直接关联的非常快(单线程的)
并发标记 与用户线程并发运行(耗时长)
重新标记 STW纠正一下错误标记(重新标记是并行的)处理遗留内容
筛选回收 把要清除的那一部分的分区的存活对象复制到空区域,并回收原来区域,涉及存活对象的移动,必须STW(并行的)
G1里程碑 应付对象分配的速度,并不追求一次把垃圾清理干净
G1写屏障修改卡表更加麻烦需要使用队列异步执行,CMS可以直接同步执行
衡量垃圾回收器的三大指标
内存占用
吞吐量(用户代码执行率)
低延迟(STW时间减少)(越来越重要,前两者可以通过性能弥补,这个内存变大,搜索时间也会变大)
指针转发
默认使用保护陷阱,一旦访问,进入特定的异常处理器,再把访问转发到目标对象,需要管态,目态频繁切换,指针转发在对象头前设置新的对象地址,使用CAS并发操作,通过写屏障监听访问
ZGC
同样追求低延迟(10ms以内)同样分区,不过分为 小型(2M用于存放256k以下的对象),中型(32M存放大于等于256k小于4M的) 大型(容量不固定,必须为2M的整倍数,用于存放4M或以上的对象) 所有区域不固定会动态转换
同样使用指针转发与读屏障,不过使用指针染色技术(直接把少量信息存储到指针上)
并发标记 与G1类似不过是标记染色指针STW
并发预备重分配 并发扫描全堆,统计要清理哪些分区
并发重分配 把存活对象复制到新的分区,并为重分配的每个分区维护一个转发表,若访问重分配对象,会被内存屏障截获,转发,并修正引用直接指向新对象(指针自愈)只保留转发表就行了,访问都会自愈
并发重映射 因为指针自愈,并不迫切重映射,可以合并到下一次的并发标记,若转发表所有指针都自愈,就可以释放转发表了
指针染色
利用64位计算机寻址没有全部占用64位,占用4位存储信息(主要存储三色标记状态,是否被移动过),不支持32位,不支持指针压缩,ZGC实际只使用了读屏障(一部分是指针染色,一部分是不支持分代搜集(可能产生大量浮动垃圾),没有跨代引用问题)
jps
jps -l 查看所有Java进程信息
jstat
jstat <操作例容-gc> < pid > < 每次触发间隔 > < 触发次数 >
jinfo
jinfo < 操作 > < pid > 查看,动态修改信息
jmap
jmap -dump < pid > 生成内存转储文件(dump文件)
jhat
用于分析内存转储文件(小型web服务器)
jstack
用于生成线程快照,也可以判断死锁
可视化处理工具
JConsole JVisualVM JMC
javap -verbose
查看指定类的字节码内容
Class文件结构
魔数
次版本号
主版本号(用于向下兼容,不允许低版本运行高版本的)
常量池数量(因为非常拥挤,对于长度不确定的部分必须指定长度,下同不再说明)
常量池(主要存放符号引用,字面量(常量)每个内容都要指明长度(因为长度不定))
访问标识(final,抽象等,是哪一类文件(接口,类,注解等)以及访问权限)
类索引(索引指向常量池)
父类索引
接口索引集(同样开始需要接口记数器)
字段表集合(基本数据类型有简写,对象数据类型使用L全类名,每维数组加一个[,同样需要字段计数器)
方法表集合(类似字段表集合,返回值不包含在特征签名中,所有只靠放回值不同无法重载,同样需要方法计数器)
属性表集合(同样需要属性计数器)
? Code属性(最终方法编译字节码部分,抽象方法没有该属性,变量槽是局部变量分配的最小单位,32位,long,double要占用两个变量槽,若是实例方法默认第一个变量槽为this,异常变量表记录从from->to发生什么异常跳转到哪一行)
? LineNumberTable属性,方便Debug
? LocalVariableTable记录局部属性名称,否则变量名直接是var[数字]
? LocalVarableTypeTable记录属性类型,服务泛型擦除
? SourceFile可选文件名
? ConstantValue存放static final变量,只支持基本变量与字符串(字符串只存索引)
? InnerClasses属性,记录内部类与外部类的关系
? Deprecated是否过时,Synthetic是否自动生成的文件(有些文件是虚拟机自动生成的)
? StackMapTable用于加载字节码时进行类型验证,替换以前流式分析类型推导,提高性能
? Signature属性记录泛型信息
? MethodParameters属性,记录方法参数名,可以通过反射获取参数名称
? 运行时注解相关属性,用于支持运行时注解
数据类型公用操作
操作boolean,char,byte,short,char实际使用的int的操作指令
常用指令
[类型字符]load_[数字] 用于加载第几个变量槽的数据到栈顶,数字 0-3 其他的就要借助操作数了
类似的 [类型字符]store_[数字] 用于把栈顶数据存放到第几个变量槽
[类型]操作 类型可取 i int l long f float d double
pop 将操作数的栈顶元素弹出
dup 复制栈顶元素并入栈
swap 交换栈顶的两个元素
tableswitch 对于规律的查表 lookupswitch 不规律的一个个查
比较结果放回值 最终都会转换为int
还有运算指令 都是操作栈顶数据,把结果再压入栈顶
类型转换
对于NaN若目标对象有直接NaN,没有转换为0,对于最大/小值 尽可能取接近的
类的加载过程
加载 把类的二进制流加载进内存,对于数组,是动态生成的,需要加载其元素类型
验证 排除非法代码 文件格式验证(类结构),元数据验证(继承未实现方法,继承final类等错误),字节码验证(为了优化检查速度,把大量检查挪到javac编译器,添加StackMapTable辅助校验),符号引用验证(防止访问权限非法,符号引用不存在)
准备 分配内存并设置0值,静态常量直接javac时生成ConstantValue属性没有赋0值的过程所以必须手动赋值
解析 将符号引用替换为直接引用,会对除 invokedynamic 外的指令解析进行缓存 1,类或接口解析(若属性类,接口没有加载会去递归加载) 2,字段解析 3,方法解析 4,接口方法解析
初始化 完成程序员自定义初始化 生成clinit完成静态代码块,静态变量的赋值 不需要显示调用父类clinit,加载过程保证,先调用父类的再调用子类的(该方法非必须)
使用
卸载
(其中验证,准备,解析 统称连接)
触发类加载
1,遇到new,getstatic,putstatic,invokestatic,若没有初始化 会进行初始化(使用new创建对象,操作静态变量,使用静态方法等,注意静态变量,方法要看在那个类定义而不是谁调用(子类调用父类的静态方法,子类不会初始化),使用常量不会引起类初始化,使用对象会把常量复制到自己的常量池)
2,使用反射
3,类初始化,若父类还没有初始化,先触发父类初始化
4,加载Main方法的类
5,一个有默认接口的实现类加载,会先加载该接口(接口不允许静态代码块,普通代码块)
类加载器
启动类加载器 用于加载核心jar包 /lib 下的(C++实现,无法获取,jdk9下虽然有对应类,为了兼容,核心类获取类加载器依旧放回null)
扩展类加载器 主要加载 /lib/ext 下的jar包
应用类加载器 用户代码默认类加载器
双亲委派模型
类加载器收到类加载请求,会先让父类加载器加载,返回为null自己再加载,保证核心类库不被修改
自定义类加载器
双亲委派机制的实现在ClassLoader.loadClass中实现,若要破坏可以重写loadClass,若要保留重写ClassLoader.findClass(官方推荐)
运行时栈帧
每个方法对应一个栈帧,包含,局部变量表,操作数栈,动态连接,方法放回地址,栈顶的栈帧,叫做当前栈帧,对应的方法叫当前方法,每个线程都有一个方法调用栈
局部变量表
一个槽位32位,对于64位的允许分为两次操作(jdk5以后只允许写分开),volatile必须原子读写,实例方法变量表0号为this,变量槽可以复用,但是可能会影响垃圾回收(引用不被覆盖会继续存在),所以建议手动把不使用的对象置空,局部变量没有初始化阶段,必须手动赋值
操作数栈
类似逆波兰表达式的运算操作,大部分虚拟机会进行一些优化,把部分操作数栈与局部变量表重叠使用,不仅节约空间,而且可以公用一部分数据,无需额外的参数传递
动态链接
方法引用在运行是转换为直接引用,叫做动态链接 符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
方法返回地址
指向方法正常放回的地址,帮助恢复到上层执行状态,方法执行完毕会弹出当前栈帧,恢复上层的局部变量表与操作数栈,若有放回值压入操作数栈,调整PC计数器
方法解析
对于编译期可知,运行期固定的方法会在类加载时进行静态解析(final方法虽然也是invokevirtual但是也是静态解析,并且明确规定它是非虚方法)
静态分派
方法入参匹配,默认按声明类型匹配,因为要保证,调用方法存在,所有依赖静态类型决定调用方法版本的都是静态委派,典形应用方法重载,发生在编译期,若方法不存在,会依次对对象类型进行基本数据类型转换(可以的话),封装类型转换,父类,接口转换,变长参数匹配优先级最低
动态分派(多态)
根据实际类型确定方法执行版本的分派过程叫动态分派(多态)(只有虚方法invokevirtual,没有虚字段,所以字段没有多态)
实例变量的程序员初始化在init方法中,若父类在构造方法访问子类变量会获取到0值变量
JVM通过查找虚方法表进行分派
虚方法表中保存各个方法的入口,如果方法没有被重写,指向父类相同的方法地址,重写了,会被替换为子类方法实现地址
动态语言
类型检查的主体过程是在运行期,而不是在编译期,Java 使用invokedynamic支持动态性,Java会在编译期就把方法信息生成到Class文件,无法动态修改,动态语言的核心:变量没有类型,变量值才有类型(看变量值调用方法,而不是看声明)蓝不大表达式会使用invokedynamic指令
MethodHandle与传统Java反射的区别
两者都是模拟Java方法调用,前者是字节码层面的调用(有findStatic(),findVirtual()等),后者是代码层面的调用
前者更加轻量,后者包含信息更多(方法签名,描述符,权限,等代码端的全面映像)是重量级的
前者因为是字节码模拟调用,虚拟机可以使用各种优化,后者几乎无法优化
MethodHandle是站在字节码方向看的,服务于所有JVM上的语言
Java字节码指令
基于栈,大部分指令都是0地址指令,依赖操作数栈进行工作
栈架构优点 容易实现,跨平台(寄存器依赖硬件)代码紧凑
栈架构缺点 执行速度慢(不过最终都会编译成汇编指令流,就无所谓了),入栈出栈需要大量指令
字节码通过 程序计数器,局部变量表,操作数栈 执行
查看Jdk代理文件
System.setProperty("sun.misc.ProxyGenerator","true"); // 设置保存jdk代理文件
基本数据类型的拆装箱
自动装箱实际调用的对应的valueOf()方法
自动拆箱实际调用对应的XXXValue()方法
Java可以在自动拆箱与装箱代码中做手脚,例如Integer缓存池
代码编译
前端编译 把java文件编译为class文件
后端编译 JIT(即时编译器,用到再编译),AOT(提前编译器)把class文件编译为目标机器代码
大部分代码编译优化在运行时的即时编译器进行,这样其他语言的class文件也能享受好处
Java中即时编译器在运行期的优化支持程序的执行效率,前端编译器的优化支持程序员的编码效率(语法糖)
前端编译过程
准备阶段 初始化插入式组件处理器
解析与符号填充过程 词法语法分析,填充符号表,产生符号地址,符号信息
插入式注解处理过程 该过程可能生成新的符号需要回到上一步继续解析
分析与字节码生成过程 对语法静态信息检查,控制流分析,解析还原语法糖,字节码生成
常量折叠(javac前端编译器极少的优化手段之一)
int a=1+3; => int a=4; 会自动优化 与后者效果一样
局部变量的final
局部变量的final有前端编译器检查判断,编译的class文件没有该标识,因为局部变量表没有定义存储该信息,少有的只有在编译期,不能在运行期检查的例子
泛型
Java泛型实现使用 类型擦除式泛型 (不支持同样容器不同泛型的方法重载),存储的都是裸类型(所以类型不能是基本数据类型,必须为其包装类),获取时进行类型转换,自动拆箱装箱,只需要修改前端编译器,这样实现是因为,要兼容jdk5以前的老代码,必须二进制兼容(C#才用的添加新的容器类,与原来的并存)
遍历循环
实际底层调用的list.iterator();迭代器用于遍历循环,所以要使用foreach遍历的都要实现Iterable< T >接口(注意数组没有实现也可也遍历,它会转换为下标遍历,与这个不同)
解释器与编译器
原来只是解释器发挥作用,编译器逐渐编译热点代码,大部分编译都能优化提升速度,若激进优化不成立(出现罕见陷阱)会做逆优化,回退到之前的状态,默认采用混合模式,可以使用 -Xint强制解释运行 或使用-Xcomp强制编译运行
热点代码
多次调用的方法
多次执行的循环体
栈上替换
对热点方法进行编译,方法的栈帧还在栈上,方法被替换了
热点探测判定方式
基于采样的,若发现某个方法经常在栈顶,它就是热点方法 优点 简洁高效 缺点 不精确会受方法阻塞影响
基于计数器的 每个方法(甚至代码块)建立计数器,统计执行次数 优点 更加精确 缺点 每个方法都要维护一个计数器(HotSpot使用)
方法调用计数器
默认阈值客服端1500,服务端10000,可以通过-XX:CompileThreshold调整,超过阈值发起编译请求,并不阻塞调用,先使用解释器运行,编译完毕,系统自动修改方法调用入口地址为新值,若一段时间后还没有超出阈值,会热力衰退(方法调用计数器减半)
回边计数器
为了统计循环体运行次数,回边计数器没有热力衰减,一样超出阈值发起编译请求(非阻塞)
提前编译(AOT)
即时编译消耗原来程序运行的时间和运算资源,提前编译不需要(性能更好),不过提前编译使Java没有了跨平台的特性,提前编译器没有执行时间与运算资源的压力,可以随便使用重量级优化手段
即时编译器对比提前编译器的优点
即时编译器可以不断搜集监控性能,动态优化
即时编译器可以预防过激优化,有回退的余地
链接时优化,动态方法调用(多态)必须即时编译才能优化
方法内联
把方法调用逻辑复制到调用处,1除去方法调用成本,2为其他优化建立基础(所以一般把方法内联放到第一步,若不这样做很多优化无法进行)
虚方法,无法进行静态方法内联,而且Java默认大部分方法都是虚方法(C#必须标明虚方法才是虚方法)
使用类型继承关系分析(CHA)若发现非虚方法直接内联,发现虚方法但是只有一个版本守护内联(必须留逃生门,当继承关系改变必须抛弃已经编译的代码),若虚方法有多个版本会使用内联缓存,判断调用信息进行缓存(类似普通缓存)
公共子表达式消除
a=x+y+x+y => z=x+y,a=z+z (对应重复方法调用无法消除,因为内部逻辑未知)
传播复写
x=y a=x+y => x=x a=x+x;
无用代码消除(去除无意义代码)
x=x a=x+x => a=x+x
逃逸分析(还不稳定,收益不确定,需要手动开启)
非直接优化的手段,为其他优化提供分析依据
对参数作用域进行分析 从不逃逸《方法逃逸《线程逃逸
根据逃逸程度可以进行 栈上分派,标量替换,同步消除等优化
栈上分配
若一个对象不会逃逸出线程,可以考虑直接在栈上分配,对象随栈帧弹出而销毁
标量替换
若一个对象不会逃逸出方法,可以不创建对象,在栈上只存储其属性(加快访问)(栈上分配),为进一步优化创建条件
同步消除
若一个对象不会逃逸出线程,可以消除其同步操作(Java默认也会生成同步操作,例如字符串拼接)
数组边界检查消除
若分析判断数组没有发生越界,在运行时可以去除数组边界检查
Java线程调度
协同式 线程把自己的事情干完再进行线程切换,缺点操作时间不可控,优点设计简单(Lua就是)
抢占式 系统分配线程执行时间,线程的切换不由线程决定,操作时间可控
Java线程映射到原生线程使用,线程调度最终由操作系统说了算
实现线程主要有3种方式
使用内核线程实现(1:1,Java默认使用)基于内核线程实现,各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换,需要内核线程支持,能够创建的数量有限
使用用户线程实现(1:N)所以操作都需要用户自己实现,若实现的好可以避免用户态与内核态的来回切换,数量没有限制
使用用户线程加轻量级进程混合实现(N:M)既能使用系统线程带来的便利,也可也使用自定义线程
final
不可变对象带来的安全是最直接纯粹的
相对线程安全
保证单次调用是线程安全的,大部分Java并发容器都是这样的
System.runFinalization();
强制调用已经失去引用的对象的finalize方法
synchronized是非公平锁
Unsafe
必须启动类加载器加载的类才能访问它,其他类可以通过反射获取
可重入代码
不依赖全局变量,公共资源,用到的状态量都由参数传递,不调用不可重入方法(数学定义的函数),不需要加锁也是线程安全的
自旋锁优化,自适应锁
自旋次数不固定,若刚刚成功获取锁,可能允许更多的自旋,若很少通过自旋获取锁,可能直接省略自旋过程
锁粗化
虚拟机若发现有一串零碎的操作对同一个对象加锁,会把锁同步访问扩大到整个操作
对象头(Mark Word部分)
轻量锁
使用自旋代替使用锁的开销,依据:对于绝大部锁,在整个同步周期内都是不存在竞争的
偏向锁
锁会偏向第一个获取的线程,若该锁一直没有被其他线程占用此有该锁的线程不需要同步,注意,若一个对象已经计算过一致性哈希码,将无法进入偏向锁状态,处于偏向锁状态的对象,收到计算一致性哈希码时,偏向状态会立马撤销,并且膨胀为重量锁
对象头中的hashCode
若对象重写的hashCode()每次获取都会调用重写方法,若没有,底层C++计算hashCode并保存到对象头,获取锁时,该信息会被保存到其他地方,释放锁时在恢复(因为锁信息存储会覆盖原信息)
这篇关于深入理解Java虚拟机学习笔记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-26大厂数据结构与算法教程:入门级详解
- 2024-12-26大厂算法与数据结构教程:新手入门指南
- 2024-12-26Python编程入门指南
- 2024-12-26数据结构高级教程:新手入门及初级提升指南
- 2024-12-26并查集入门教程:从零开始学会并查集
- 2024-12-26大厂数据结构与算法入门指南
- 2024-12-26大厂算法与数据结构入门教程
- 2024-12-26二叉树入门教程:轻松掌握基础概念与操作
- 2024-12-26初学者指南:轻松掌握链表
- 2024-12-26平衡树入门教程:轻松理解与应用