JVM笔记

一 、Jvm 基础到入门

1.1 Jvm 基础到入门

什么是jdk java的开发工具包

什么是jre 运行时环境

什么是jvm java的虚拟机

javac 将.java文件转换为.class文件

java 运行 java文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0QrDeoDp-1657180050785)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/image-20220109203606636-164174729827157.png)]

将源代码.java文件 通过javac的指令生成的class文件

通过java指令 将字节码文件.class 进行解析,首先class文件先通过classloader加载到内存中,会用到一些java的相关类库,比如说object 或者String 等等。进行调用字节码的解释器

JIT即时编译器,来对字节码文件进行编译,编译之后,再由执行引擎执行,执行引擎面对的是操作系统和硬件。我们把整个的java指令的这一部分流程称为jvm。

面试题 Java语言是解析执行,还是编译执行?

解析和编译是混合的,针对常用的代码,会把代码做成一种即时编译的,支持本地的,那么在下次使用的时候就不需要解释器对代码进行一句句的解析来执行,执行引擎可以将代码直接交给操作系统。让其进行调用,效率能够提高,当然也不是所有的代码都要被JIT进行即时编译的,原因是java本身需要跨平台。

1.2 从跨平台的语言到跨语言的平台

jvm 是跨语言的平台 java是跨平台的语言

除了java语言 jvm还支持 多门语言 将近 100多种 JVM也是提供了一些规范。

它也帮我们屏蔽了一些操作系统 Linux 或者windows mac

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mR9k2OEO-1657180050787)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image002-16417318971402.jpg)]

JVM怎么决定其他语言也可以在JVM上跑 ,就是因为class

其他的语言只要 能生成 class文件 就可以用jvm运行
在这里插入图片描述

java程序 ,可以在各种平台是执行 并且不用修改东西

这一点以前的c语言和c++都是做不到的

如果是程序内部编译成的类class 文件 也是可以jvm执行的 所以说java 和jvm 没有任何关系

jvm 是一种 规范 定义了java虚拟机能够执行什么东西

规范在oracle官网

https://docs.oracle.com/javase/specs/index.htm
在这里插入图片描述

The Java Language Specification
JavaSE16版本的语法规范
The JAVA Virtual Machine Specification
16版本JVM规范

JVM 是一种规范
虚拟机其实就是虚拟出来的机器
内存的管理 堆栈方法区。

1.3常见的虚拟机

Hotspot oracle 官方的,我们实验用的 jvm -java-version

命令行窗口 去输入 java version

jrockit -VEA号称最快的jvm 被oracke收购合并 hostpot

TaoBaoVM -hotspot深度定制版本

LiquidVM 直接针对硬件

azul zing 追星垃圾回收的业内标杆 银行用 土豪版本

一般的大厂都不会依赖oracle的东西 都有自己的平台 因为版权问题

java收费 是针对虚拟机来说

Hotspot8 以后 就不升级了 升级需要加钱

在这里插入图片描述

二、 Class文件结构

2.1 Class file format

首先先写代码使用IDEA

package com.openlab;
public class JVMTest01 {
    public static void main (String [] args){
        System.out.println(1);
    }
}

通过编译能够生成class文件

package com.openlab;
public class JVMTest01 {
    public JVMTest01() {
    }
    public static void main(String[] args) {
        System.out.println(1);
    }
}

我们可以使用一些软件来查看classfile

Sublime_Text 软件
IDEA 插件 Bined

十六进制的数据流 ----可以
转化为二进制

IDEA插件 在file下选择setting 然后在Plugins搜索 Bined 安装
在这里插入图片描述

图可能不一样 但是名字一样

安装好以后 file - Open as … 选择class文件 就可以看到 Class文件了
在这里插入图片描述

Class文件是一组以8个字节为基础单位的二进制字节流。

各项数据会严格的按照顺序紧凑的排列在class文件中

中间没有分隔符,使得class文件存储的内容几乎全部都是程序运行的。

2.2 Classfile 文件结构解析

Java虚拟机规范规定的,class文件格式采用的类似C语言的结构体的伪结构来存储的,这种结构只有两种数据类型。

无符号数 和 表

无符号数:

属于基本数据类型 主要用于描述数字 索引符号 数量值 或者按照UTF-8编码构成的字符串值

数据类型 U1 U2 U4 U8 也只是逻辑上的区分。

U1–表示一个字节

U2–表示二个字节

U4–表示四个字节

U8–表示八个字节

由多个无符号数或者其他表作为数据项构成的复合数据类型。所有的表都习惯以_info结尾 表主要用于描述有层次关系的复合结构数据。 比如 方法、字段 需要注意的是class文件没有分隔符,所以每个二进制数据类型都是严格定义的 具体的顺序如下:

2.2.1 魔数

1 .每一个class文件的头4个字节 被称为魔数 magicNumber

2.唯一作用是 确定这个文件是否为一个能被虚拟机接受的class文件

3.class文件魔数值为 0xCABFABABE 如果这个文件不是以CABFABABE开头 ,那么他就肯定不是 Java的class文件 这也是java.class的识别魔数

很多文件存储标准中都有魔数来识别文件 。

比如 图片的.gif或jpeg等文件头部都存在魔数 是使用魔数识别的而不是文件扩展名识别,这种情况有安全隐患

2.2.2 class文件版本号

紧挨着魔数的4个字节表示class的文件的版本号 版本号:

  1. 次版本号 --minor_version 前2个字节用于表示次版本号

例如:00 00

  1. 主版本号 --major_version 后2个字节用于表示主版本号

例如: 00 34

这个版本号随着jdk版本的不同而表示不同版本的范围。Java的版本号是从45开始的

如果class的版本号超过虚拟机的版本 会被拒绝执行。

JDK1.2 ----0X002E 46

JDK1.3 ----0X002F 47

JDK1.4 ----0X0030 48

JDK1.5 ----0X0031 49

JDK1.6 ----0X0032 50

JDK1.7 ----0X0033 51

JDK1.8 ----0X0034 522.2.3 常量池

2.2.3 常量池

CONSTANT_POOL_COUNT和CONSTANT_POOL

紧跟着魔数与版本号之后的是常量池入口,常量池简单理解为class文件的资源库。

  1. 它是class文件结构中与其他项目关联最多的数据类型

  2. 是占用class文件空间最大的数据项目之一

  3. 是在文件中第一个出现的表类型数据项目。

常量池的数量是不固定,所以在常量池的入口需要放置一个u2类型的数据,代表常量池的计数值CONSTANT_POOL_COUNT。

CONSTANT_POOL_COUNT 从1开始计数的。 class文件结构中只有常量池的容量计数是从1开始的。第0项腾出来满足后面某些指向常量池的索引值的数据,在特定的情况下需要表达“不引用任何一个常量池项目” 把索引值的第0项留给JVM自己用。

CONSTANT_POOL是没有索引值为0的入口的,但是在CONSTANT_POOL_COUNT缺失的第0项也是要被计算在内的。

比如CONSTANT_POOL 中有14项 那么CONSTANT_POOL_COUNT的数值就是15

常量池中主要存放两大类常量:

  1. 字面量: 比较接近java语言层面的常量的概念 比如 字符串 被final关键字声明的常量值。

  2. 符号引用: 属于编译原理方面的概念 包括三项。

    1. 类和接口的全名
    2. 字段的名称和描述
    3. 方法的名称和描述符

在加载class文件的时候 是进行动态连接的。在class文件中不会保存各个方法和字段的最终内存布局信息。(需要经过转换) 当虚拟机运行时 需要从常量池获得对应的符号引用,再在类创建时或者运行时解析并翻译到具体的内存地址中。

CONSTANT_POOL_COUNT 占2个字节 本例中为0x20 转换成十进制为32 说明常量池中有31个常量 ----从1开始计数 其他集合类型均从0开始。 索引值为1-31 第0项常量具有特殊意义。

CONSTANT_POOL 表示的是类型数据集合,在该常量池中,每一项常量都是一个表 共有14种 -----JDK1.7版本,这14种结构的表都是不相同的结构数据。14个表都有一共同的特点,都是由u1的标志位开始的,可以通过这个标志位来判断这个常量属于哪种常量的类型。

在这里插入图片描述

2.2.4access_flag

用于表示对该类或接口的访问权限以及该类或接口的属性
在这里插入图片描述

2.2.5 this_class

该this_class 项目的值 必须是constant_pool表中的有效索引,该
CONSTANT_Class_info 结构class

2.2.6 super_class

必须是constant_pool表中的有效索引, 如果super_class的值不为0 则constant_pool中的条目必须为CONSTANT_Class_info 结构 这个结构表示此类的文件定义的类的直接超类。直接超类不能在其classfile结构的access_flag项中设置 ACC_FINAL 标志。

其实要描述的意思就是说 如果superclass指代的超类,那么它就不能被final修饰。

2.2.7 ByteCode插件安装

\1. javap指令

2.JBE 插件可以直接修改classfile

3.JClasslib IDEA插件之一

l Javap

在这里插入图片描述

javap -v 字节码文件的路径

在这里插入图片描述

从上图可以看出 对文件进行了MD5的加密

Minor Version -----0

Major version -----52

Access_flag -----public super

#4 = Class ----#24 this_class 编号为多少

#5 = Class ----#25 super_class 编号为多少

l JBE

工具 不仅可以查看bytecode 也可以进行修改,可以通过汇编语言编写。

l JClasslib

在setting选项上 选择plugins然后在输入框输入jclasslib

在这里插入图片描述

如果字节码文件没有生成 可以build Project

选中类名

点击view 选择show bytecode with jclasslib

在这里插入图片描述

在这里插入图片描述

访问标志为什么是0x0021 这是一个按位与的运算,通过ACC_PUBLIC&ACC_SUPER这两个属性的按位与运算得来的值。使用2个字节来表示,可以代表很多内容。

2.2.8 常量池详细解析常量类型

总共有18个编号的常量类型。

编号1: CONSTANT_UTF8_INFO

TAG1 ------占用一个空间字节

Length: utf-8字符串占用的字节数

Bytes 长度为length字符串

用于表示utf-8的编码的字符串

编号3 CONSTANT_integer_info

Tag3

Bytes 4个字节 Big_Endian(高位在前) 存储int类型的值

编号4 CONSTANT_float_info

Tag4

Bytes 4个字节 Big_Endian(高位在前) 存储float类型的值

编号5 CONSTANT_long_info

Tag5

Bytes 8个字节 Big_Endian(高位在前) 存储long类型的值

编号6 CONSTANT_double_info

Tag6

Bytes 8个字节 Big_Endian(高位在前) 存储double类型的值

编号7 CONSTANT_Class_info

Tag7

Index 2个字节 指向类的全限定名的项的索引

类和接口符号引用

编号8 CONSTANT_String_info

Tag8

Index 2个字节 指向字符串的字面量的索引

编号9 CONSTANT_Fieldref_info

Tag9

Index 2个字节 指向声明字段的类或接口的描述符 CONSTANT_Class_info的索引项

Index 2个字节 指向字段描述符CONSTANT_NameAndType的索引项

字段的符号引用

编号10 CONSTANT_Methodref_info

Tag10

Index 2个字节 指向声明字段的类或接口的描述符 CONSTANT_Class_info的索引项

Index 2个字节 指向字段描述符CONSTANT_NameAndType的索引项

类中方法的符号引用

编号11 CONSTANT_InterfaceMethodref_info

Tag11

Index 2个字节 指向声明字段的类或接口的描述符 CONSTANT_Class_info的索引项

Index 2个字节 指向字段描述符CONSTANT_NameAndType的索引项

接口中方法的符号引用

编号12 CONSTANT_NameAndType

Tag12

Index 2个字节 指向该字段或方法名称常量项的索引
Index 2个字节 指向该字段或方法描述符常量项的索引

字段或方法的符号引用

编号15 CONSTANT_MethodHandler_info

Tag15

Reference_kind 1个字节 1-9之间的一个值 决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为

Reference_index 2个字节 对常量池的有效索引。

表示方法句柄

编号16 CONSTANT_MethodType_info

Tag16

Descriptor_index 2个字节 指向UTF8_info 结构表示的方法描述符

编号18CONSTANT_InvokeDynamic_info

Tag18

Bootstrap_method_attr_index: 2个字节 当前class文件中引导方法表的bootstrap_methods[] 数组的有效索引

Name_and_type_index: 2个字节 指向NameAndType_info 表示方法名和方法描述符。

表示动态方法的调用点。

2.2.8.1 案例解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ju1IHjnS-1657180050800)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image002-16417443101929-164174436219217.jpg)]

常量池中有31个常量信息,第一个是Constant_methodref_info 为方法引用信息

#5类名 指向常量池第五个的信息 —Object的类的信息,

#19名字和描述符 CONSTANT_NameAndType_info

在这里插入图片描述

在这里插入图片描述

NameAndType_info

在这里插入图片描述

当我们看到init字样的时候,是一个
构造方法

当我们看到()V

() 表示方法没有参数

V 表示void 方法没有返回值。

整个常量池在classfile中是如何表示的

cafe babe 0000 0034 0020 0a00 0500 1309
0014 0015 0a00 1600 1707 0018 0700 1901
0006 3c69 6e69 743e 0100 0328 2956 0100
0443 6f64 6501 000f 4c69 6e65 4e75 6d62
6572 5461 626c 6501 0012 4c6f 6361 6c56
6172 6961 626c 6554 6162 6c65 0100 0474

常量池的第一行

从0a00 那么0a对应的是十进制的10 这个10映射到常量表中的Tag10的标志 CONSTANT_Methodref_info;

0005 表示 index 2个字节 指向声明方法的类或接口的描述符#5 CONSTANT_Class_info的索引项。指向#5行的内容。

0013 13 转成十进制是19 指向#19行的内容是,表示的index2个字节 指向字段描述符CONSTANT_NameAndType_info的索引项

那么以上的常量池是5个字节。

常量池的第二行

从09 开始 09对应的是十进制9 这个9映射到Tag9的标志

CONSTANT_Fieldref_info;

0014 对应的十进制是20 对应的#20的内容

0015 对应十进制是21 对应的就是#21的内容

常量池的第三行

从0a开始那么0a对应的是十进制的10 这个10映射到常量表中的Tag10的标志 CONSTANT_Methodref_info;

0016 ----十进制22 对应#22行的内容

0017 ----十进制23 对应的#23行的内容

常量池的第四行

从07开始 对应的tag7 CONSTANT_Class_info

0018 ---- 十进制24 对应着#24的内容 表示的是this_class

常量池的第五行

从07开始 对应的tag7 CONSTANT_Class_info

0019 ---- 十进制25 对应着#25的内容 表示的是super_class

常量池中第六行

01 ----CONSTANT_UTF8_INFO

0006 表示占用的字节数和长度

3c69 6e69 743e 表示字符串内容的字节。init字符串的构造方法

这段总共是9个字节。

常量池的第七行

01 ----CONSTANT_UTF8_INFO

0003 表示常量池第七个常量的字符串长度占用的字节

28 2956 表示 ()V构造方法 V表示无返回值

这段总共6个字节。

Method属性

Method属性包含三个字段值

名称access_flag 类型u2 数量1个attributes_count 1

名称 name_index 类型u2 数量1个 attribute_info attributes ----attributes_count

名称 descriptior_index 类型u2 数量1个

descriptior_index

\1. 参数列表(参数类型) 后-返回值

\2. void m() 等同于 ()V

\3. String toString() ->()Ljava/lang/String;

\4. Long pos(int[] arr1,int arr2,long length) ->([IIJ)J

在这里插入图片描述

[ 一维数组

[[ 表示二维数组

关于attributes_count 附加属性的数量

关于 attributes 附加属性

常量池的第八行

01 ----CONSTANT_UTF8_INFO

0004 表示常量池第八个常量的字符串长度占用的字节

43 6f 64 65 表示 code字符串内容

这段总共是7个字节

Fields属性

名称access_flag 类型u2 数量1个attributes_count 1

同method属性

名称 name_index 类型u2 数量1个 attribute_info attributes ----attributes_count

名称 descriptior_index 类型u2 数量1个

同method属性值。

关于attributes_count 附加属性的数量

关于 attributes 附加属性

第9个常量池LineNumberTable 是在后面要使用的常量的名字,在code当中被引用到。

在这里插入图片描述

第10号常量池LocalVariableTable是在后面要使用的常量的名字,在code当中被引用到。

在这里插入图片描述

第11个常量池 指向的字符串面值this

在这里插入图片描述

第十二个常量池 表示整个类的类名称

在这里插入图片描述

第13个常量池内容 表示的是main方法的字符串名称

在这里插入图片描述

第14个常量池内容 表示的是main方法中的参数 字符串数组 无返回值
在这里插入图片描述

第15个常量池 main方法的参数名称的字符串字面量

在这里插入图片描述

第16个常量池 mian方法中参数类型的字符串字面量值

![在这里插入图片描述](https://img-blog.csdnimg.cn/608a07c54bfd41f3bd3e94b8027在这里插入图片描述

第17个常量池 SourceFile 对应后面的附加属性的SourceFile 表示的是源文件的索引

在这里插入图片描述
在这里插入图片描述

第18个常量池内容 表示的是源文件的名称的字符串字面值

在这里插入图片描述

第19个常量池 CONSTANT_NameAndType_info 对应6号常量池构造方法的名字和7号常量池 方法的参数和返回值
在这里插入图片描述

第20个常量池 CONSTANT_Class_info 对应的System类 对应了26号常量池的System的字面值

在这里插入图片描述

第21号常量池 CONSTANT_NameAndType_info 对应的是27号常量池 out字面值属性的名称,对应了28号常量池的PrintStream的类的字符串字面值。

在这里插入图片描述

从22号开始到31号,按照字面意思去理解就可以了。

2.2.9 interfaces 和Fields选项 method选项

因为在我们案例中没有声明接口和成员变量,所以对应没有展开选项的。

因为有两个method

在这里插入图片描述

\1. init 无参的构造方法 —默认提供的构造方法

\2. 主函数main

方法中的access_flag

在这里插入图片描述

2.2.10 attributes_count 和attribute

附加属性 方法中的附加属性就是code,那么code在这里是比较重要的概念,code是具体代码的实现,当我们写入方法的时候,它能够把方法中代码转化为一条条指令。

在这里插入图片描述

在官方文档中,我们可以看到很多十六进制的数字,那么它们对应方法中code的内容实现。

也可以鼠标右键点击

在这里插入图片描述

在这里插入图片描述

从本地变量表中第0项 放入到栈空间中。

Attributes附加属性

附加属性中 有的代码中存在内容,有的不存在内容

\1. 既有预定义的属性,也可以自定义 java虚拟机会自动忽略它不认识属性

\2. Code 表示的是方法表 方法表能够编译成字节码指令,还存放了操作数栈和局部变量的信息。

在这里插入图片描述

u2 attribute_name_index 指向常量池中的CONSTANT_UTF8_info 存放的当前属性的名字就是code。

u4 attribute_length 表示的code属性的长度 (不包括前6个字节)。

u2 max_stack 指定当前方法被执行引擎执行的时候,在栈帧中需要分配的操作数栈的大小

u2 max_locals 指定当前方法被执行引擎执行的时候,在栈帧中需要分配的局部变量表的大小

u4 code_length 指定方法字节码的长度, class文件中每条字节码都占用一个字节

u1 code 存放字节码指令本身,它的长度是code_length个字节。

U2 exception_table_length指定异常表的大小

Exception_table异常表 作用对try-catch-finally的描述,可以把它看成是一个数组。每一个数组项都是一个exception_info结构, 一般来说每个catch块对应一个exception_info,编译器也可能会对当前的方法生成一些exception_info.

在这里插入图片描述

U2 start_pc 是从字节码code属性中的一部分 起始处到当前异常处理器的起始处的偏移量量

u2 end_pc 从字节码起始处到当前异常处理器 末尾的偏移量

u2 handler_pc 是指当前异常处理器用于处理异常(即catch块)的第一条指令相对于字节码开始处的偏移量。

U2 catch_type 是常量池的索引 指向的是常量池CONSTANT-Class_info 数据项,描述了catch块中的异常类型的信息。这个类必须是java.lang.Throwable的或者是它的子类。

总结:

如果偏移量从start_pc到end_pc之间,如果字节码出现了catch_type所描述的异常,那么就跳转偏移量到handler_pc的字节码中去执行。如果catch_type 为0 就代表不引用任何常量池的信息,那么这个exception_info 用于实现finally的子句。

U2 attribute_count 表示的是code属性中存在的其他属性的个数。会出现在 class中的属性,在field属性也有,在method属性也有

Attributes 可以把它看成是个数组,里面存放了code属性的其他属性。

ConstantValue ----字段表 final关键字自定义的常量值

Deprecated —类 方法表 字段表

Exception 异常表

EnclosingMethod 类文件 局部类或匿名类的外部封装方法

InnerClass 类文件 内部类列表

可选属性

LineNumberTable 源码的行号和字节码行号的对应关系 可以把这个属性看成是一个数组,

数组中的每项LineNumberinfo结构描述了一条字节码和源码行号的对应关系

LocalVariableTable 建立了方法中的局部变量与源代码中的局部变量的对应关系。

三、类加载和初始化

面试题:

  1. 描述一下类加载器的层次?

  2. 双亲委派

  3. 为什么要双亲委派

Class文件 如何加载到内存中的 并且是如何执行的

在这里插入图片描述

3.1 Class Cycle

Class文件在我们硬盘中,那么它是如何加载到内存中 总共需要三个大的步骤

  1. Loading步骤

Loading 是将本地的classfile的二进制的内容加载到内存中

  1. Linking 步骤

    1. Verification 校验

      Verification 的主要过程就是用来校验,如何加载的文件的字节码头不是CAFEBABE,该过程就会被拒绝。

    2. Preparation

      Preparation过程 主要作用就是将静态变量赋默认值 假设定义public static int i=8;在这个过程中并不是把i的值赋值成8,而是要对静态变量i进行默认值的赋值 也就是0;

    3. Resolution

      该过程 将class文件中常量池用到的一些符号引用转换为内存地址

  2. Initializing 步骤

静态变量在该步骤下进行赋值为初始值,才会调用静态代码块。

3.2 ClassLoader

JVM本身有个类加载器的层次 这个类加载器就是普通的Class,这个加载器的层次就是用来加载不同的class

在这里插入图片描述

在这里插入图片描述

注意: 任何一个classfile被加载到内存中的都会存在两个部分,

第一个部分 二进制的classfile确实被load内存中

第二个部分 生成的class类的对象,class中还会存在其他对象,引用到class对象,而class类对象指向classfile的内存加载

扩展为 Class对象究竟存储在哪里?

Class对象存储在metaspace里面

Metaspace 是JDK1.8版本出现的,Metaspace 就是方法区methodarea 1.8版本移出了永久代,原本在1.8版本之前 PermGenerationspace部分变更成了metaspace 而这两个地方指代的都是方法区。

怎么才能够知道哪些类是由哪些加载器进行加载的呢?

最顶层

BootstrapClassLoader

加载lib/rt.jar charset.jar等核心类 C++实现。

主要负责加载jdk中最核心的jar ,例如runtime.jar 或者是我们平时锁说的String.class,Object.class 都是位于lib/rt.jar

会出现null值 调用的是最顶层加载器,在java的类中没有这样的对象去应对他。

第二层

ExtClassLoader

加载扩展的jar包,jre/lib/ext/*.jar

或由-Djava.ext.dirs指定

第三层

AppClassLoader

加载classpath指定的内容

第四层

自定义加载器

加载自定义的类的内容。

 package edu.yau;

import sun.net.spi.nameservice.dns.DNSNameService;

public class ClassLoaderTest01 {

    public static void main(String[] args) {
        //由最顶层的加载器加载的,核心代码库,由c++编写,java中没有一个可以对应的对象
        System.out.println(String.class.getClassLoader());
        System.out.println(sun.awt.HKSCS.class.getClassLoader());

        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
        //当前本类对象获取的类的加载器
        System.out.println(ClassLoaderTest01.class.getClassLoader());

        //由最顶层的加载器加载的
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
        System.out.println(ClassLoaderTest01.class.getClassLoader().getClass().getClassLoader());
    }
}



类加载器的加载过程叫双亲委派。

在双亲委派中存在一个概念 叫父加载器 这里的父加载器不是继承关系

在这里插入图片描述

该图描述的是语法上一种继承关系,而继承关系和父加载器没关系。

父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent

3.3 双亲委派

Class文件通过自定义的classloader进行加载,如果他没有加载,那么则委托它的父加载器appclassloader 加载, appclassloader 判断是否为本地加载 如果有则直接加载,如果没有则继续向上委托,直到顶层的加载器bootstrapClassLoader,但是当顶层的加载器,也没有加载,就会向下委托,当所有的下级加载器都没有加载那么则抛出异常 classNotFound 异常,如果下级加载器能够加载,那么就由下级加载器进行加载。

双亲:指的有一个从子到父的过程 又有一从父到子的过程

委派:自己不想做的事情 委托别人去完成

向上委派的时候 父加载器都是到 Cache中取寻找

可以把这个缓存理解成是一个list或者是一个数组。

面试题 为什么要去使用双亲委派?

\1. 防止加载同一个class文件,保证数据的安全

\2. 保证核心的class文件不被篡改,即使被篡改了也不会加载,即使被加载也不会是同一个class对象 为了保证class的执行安全。

这部分代码是被写死的。

3.4 父加载器

父加载器不是了的加载器的加载器,也不是加载器的父类的加载器

父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent

  package edu.yau;

public class ClassLoaderTest02 {
    public static void main(String[] args) {
        //获取本类的加载器
        System.out.println(ClassLoaderTest02.class.getClassLoader());
       //获取本类的加载器的class对象的加载器--顶级加载器加载的
        System.out.println(ClassLoaderTest02.class.getClassLoader().getClass().getClassLoader());
        //获取本类加载器的父类加载器
        System.out.println(ClassLoaderTest02.class.getClassLoader().getParent());
        System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent());
        //System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent().getParent());
    }
}



[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o069rteU-1657180050820)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image010-164174456853548.jpg)]

3.5 类加载器范围

从上个案例的执行结果中,我们可以看出appclassloader和extclassloader 都是Launcher的内部类。 Launcher是classloader的包装类启动类

在Launcher源码中

private static String bootClassPath = System.getProperty("sun.boot.class.path");
final String var1 = System.getProperty("java.class.path");
String var0 = System.getProperty("java.ext.dirs");

sun.boot.class.path 是BootstrapClassloader的加载路径

java.class.path 是AppClassloader的加载路径

java.ext.dirs 是ExtClassLoader的加载路径

 import sun.misc.Launcher; 
 public class ClassLoaderTest03 {
 public static void main(String[] args){
 String pathBoot = System.getProperty("sun.boot.class.path");
 System.out.println(pathBoot.replaceAll(";",System.*lineSeparator()));
 System.out.println("---------------------------------");    
     String pathExt = System.getProperty("java.ext.dirs");    System.out.println(pathExt.replaceAll(";",System.*lineSeparator()));     
 System.out.println("---------------------------------"); 
 String pathApp = System.getProperty("java.class.path");    System.out.println(pathApp.replaceAll(";",System.*lineSeparator()));     } }

3.6 自定义加载器

小demo

 
public class ClassLoaderTest04 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = ClassLoaderTest04.class.getClassLoader().loadClass("com.openlab.Person");
        System.out.println(clazz.getName());
//       类加载器也可以用来加载资源
//       ClassLoaderTest04.class.getClassLoader().getResourceAsStream();

    }
}


Tomcat 加载的Servlet

Spring框架中加载ApplicationContext

比如在写一些类库的时候或者修改底层框架时。想加载哪个类就可以加载谁。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
// 加锁
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
//继续使用parent的classloader 递归调用loadClass方法
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

// 调用findClass方法去找class
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}



\1. 继承ClassLoader

\2. 重写模板方法 findClass

​ ----调用defineClass方法

从目录中读取class文件,将class文件通过自定义加载器进行加载

利用IO流

Java语言是比较容易被反编译

-防止反编译

-防止篡改

可以给class文件进行加密 解密

作业:1.自定义加载器的实现 视频到群里

2.classfile解析的内容 需要整理 博客的形式 Xmind的形式

3.预习JVM的基础知识点

import java.io.*;

public class MacluClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File file = new File(
                "c:/test",
                name.replaceAll(".","/").concat(".class"));

        try {
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;
            while ((b = fis.read())!=0){
                baos.write(b);
            }

          byte[] bytes = baos.toByteArray();

            baos.close();
            fis.close();

            return defineClass(name,bytes,0,bytes.length);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

         return super.findClass(name);// throw ClassNotFoundException
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        ClassLoader cl = new MacluClassLoader();
        Class clazz = cl.loadClass("com.openlab.Person");

        Person person = (Person) clazz.newInstance();
        person.m();

        System.out.println(cl.getClass().getClassLoader());
        System.out.println(cl.getParent());

    }
    }




我们可以定义自己格式的classloader,一般情况下class文件就是一个二进制文件流,可以采用一种比较简单的方式对class文件进行加密和解密

加密: 通过^ 异或 可以定义一个数字 在读取每一个字节的后的写入操作时,可以用流里面获取到的数据和这个数字进行异或的算法, 那么这种情况就可以进行加密的操作

解密: 字节数字这个数字这个数字 那么就完成了解密的操作。

import java.io.*;

public class MacluClassLoaderWithEncription extends ClassLoader{

    public static int seed = 0B10110110; // 进行参加加密算法的数字

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File file = new File(
                "c:/test",
                name.replaceAll(".","/").concat(".class"));

        try {
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;
            while ((b = fis.read())!=0){
                baos.write(b^seed);
            }

            byte[] bytes = baos.toByteArray();

            baos.close();
            fis.close();

            return defineClass(name,bytes,0,bytes.length);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return super.findClass(name);// throw ClassNotFoundException
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, IOException {

        encFile("com.openlab.Person");
        ClassLoader cl = new MacluClassLoaderWithEncription();

        Class clazz = cl.loadClass("com.openlab.Person");

        Person person = (Person) clazz.newInstance();
        person.m();

        System.out.println(cl.getClass().getClassLoader());
        System.out.println(cl.getParent());

    }

    private static void encFile(String name) throws IOException {
        File file = new File(
                "c:/test/",
                name.replace(".","/").concat(".class"));

        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(
                new File("c:/test",name.replaceAll(".","/").concat(".macluclass")));
        int b = 0;

        while ((b = fis.read())!=-1){
            fos.write(b^seed);
        }
        fis.close();
        fos.close();
    }
}

在这里插入图片描述

生成加密好的文件

在这里插入图片描述

验证加密文件 打开后是乱码。

在这里插入图片描述

3.7 编辑器

在这里插入图片描述

解释器: bytecode-interpreter

JIT 即时编辑器 Just In Time compiler

Java语言究竟是一个解释型语言还是编译式的语言

想解释器的可以用解释器,想编译也可以用编译器 看需求是怎么写的 可以通过JVM的一些参数进行设置。

默认的情况是一种混合模式

混合模式:使用解释器+热点编辑器 hotspot

起始阶段采用解释来执行

热点代码的检测 默认值为10000

多次被调用的方法(方法计数器:检测方法的执行频率)

多次被调用的循环(循环的计数器:检测循环的执行频率)

当这样的一个循环或者是一个方法,或者是一段代码,一直都会被多次调用的时候,也就是这段代码执行频率特别高的情况下,那么干脆直接将这段代码编译成本地的代码,在下次直接访问的时候,直接访问本地的代码就可以。就不需要解释器对其进行解释执行。从而达到效率的提升。这种执行代码的方式被称为混合模式。

那么为什么不直接编译成本地代码,编译的执行速度更快?能够提高效率?

  1. 现在的解释器的执行效率已经是非常高的了,在一些简单的代码执行上,它并不属于编译器。

  2. 如果要执行的程序 依赖的类库特别多的情况下,在虚拟机中编译一遍,那么启动的过程会非常的缓慢。

-Xmixed 为混合模式:

开始解释执行,启动速度比较快,对热点代码进行检测和编译。

-Xint 解释模式

启动速度很快,执行较慢

-Xcomp 纯编译模式,

启动较慢,执行较快

测试这三个jvm参数

  public class WayToRunTest01 {     public static void main(String[] args){ 
        *//**这段代码被短时间执行很多次,请JVM虚拟机对其进行优化 *     
      for (int i = 0;i<10_0000;i++)       *m*();    
       long start =System.*currentTimeMillis*(); 
           for (int i = 0;i<10_0000;i++){       *m*();     }       
           long end = System.*currentTimeMillis*();     S
           ystem.*out*.println(end-start);     
           }     
           *//* *该方法本身没有意义,就是耗时间用的。 * 
             public static void m(){ 
                   for (int i = 0;i<10_0000L;i++){      
                    long j = i%3;   
  }   
  } }

默认的混合模式

在这里插入图片描述

在JVM的执行参数中 -Xint 解释模式

很慢 回去洗洗睡吧

在这里插入图片描述

纯编译的模式-Xcomp


在这里插入图片描述

3.8 懒加载

严格来讲应该叫lazyInitializing

JVM规范中并没有规定什么时候加载

严格的规定了初始化的规则 扩展

\1. New对象 getstatic 访问静态变量时 putstatic 访问静态实例时,invokestatic指令

以上指令是必须要初始化这个类 访问final变量除外。

\2. 当反射调用的时候

\3. 初始化子类的时候 首先父类初始化

\4. 虚拟机启动时 被执行的主类必须要初始化

\5. 动态语言支持java.lang.invoke.MethodHandler解析结果为REF-getstatic REF-putstatic REF-invokestatic的方法句柄时 该类必须要初始化。

这个案例 主要看什么时候打印P和X

public class LazyLoadingTest {

    public static void main(String[] args) throws ClassNotFoundException {
//        P p;
//        X x = new X();
//        System.out.println(P.i);
//        System.out.println(P.j);
        Class.forName("com.openlab.LazyLoadingTest$P");


    }

    public static class P{
        final  static int i=8;// 打印final的值是不需要加载整个类的
        static int j = 9;
        static{
            System.out.println("P");
        }
    }

    public static class X extends P{
        static{
            System.out.println("X");
        }
    }
}


面试题:

如何打破classloader的双亲委派模式?

去重写Classloader中的loadClass方法 而不是findClass方法 这个时候就能够打破双亲委派的机制。

什么时候需要打破需要去打破双亲委派的机制:

1. 在JDK1.2版本之前 要自定义classloader的话 必须要重写loadClass方法

  1. 在一个线程中设定自己的线程的上下文的加载器对象 ThreadContextClassLoader 可以实现基础调用实现类的代码, 通过thread.setContextClassLoader 来设定。

  2. 模块化的 热部署 热启动

像osgi和tomcat 都有自己的模块指定classloader,可以加载同一个类库的不同版本的对象,目前这个方式用的比较多。

Tomcat中 Webapplication 对象是可以存在多个的,有两个Webapplication 被加载,但是他们的版本不同,这种情况下可以打破双亲委派的。

注意:类的名字的是相同的 只是说版本不同 如果采用双亲委派的机制,那么这两个对象是不可能加载到同一个空间里面 因为加载的过程中,发现在同一空间有同名的类,那么他一定不会被加载。

所以tomcat的每一个Webapplication 都有一个classloader

双亲委派模式加载

package com.openlab;

public class ClassReloadingTest {

    public static void main(String [] args) throws ClassNotFoundException {

        MacluClassLoader classloader = new MacluClassLoader();

        Class clazz = classloader.loadClass("com.openlab.Person");

        classloader = null;
        System.out.println(clazz.hashCode());
        classloader = null;

        classloader = new MacluClassLoader();
        Class clazz1 = classloader.loadClass("com.openlab.Person");
        System.out.println(clazz1.hashCode());

        System.out.println(clazz == clazz1);


    }
}



从上案例中可以看出,双亲委派,即便重新创建了classloader对象,那么曾经被加载的对象,再次加载的时候,加载的还是这个对象。

热部署应该如何实现

import java.io.*;
public class T012_ClassReloading2 {
    private static class MyLoader extends ClassLoader {
        @Override     public Class<?> loadClass(String name) throws ClassNotFoundException {
            File f = new File("C:/test/" + name.replace(".", "/").concat(".class")); 
            if(!f.exists()) return      super.loadClass(name);      
            try {      
                InputStream is = new FileInputStream(f);   
                 byte[] b = new byte[is.available()];         is.read(b);     
                return defineClass(name, b, 0, b.length);       
            } catch ( FileNotFoundException e) {         e.printStackTrace();       
                                               } catch (IOException e) {    
                e.printStackTrace();      
            }       return super.loadClass(name);    
        }   
    }     
    public static void main(String[] args) throws Exception {   
        MyLoader m = new MyLoader(); 
        Class clazz = m.loadClass("com.openlab.Person");   
        m = new MyLoader(); 
        Class clazzNew = m.loadClass("com.openlab.Person");     
        System.out.println(clazz == clazzNew);   } }

四、JMM Java内存模型

4.1 硬件层的并发优化的基础知识

寄存器如何读取硬盘中的内容, 首先将硬盘的数据load到内存中,然后寄存器先到高速缓存中去找,如果找到就直接使用,速度是非常快的,如果没找到,就去下层的高速缓存中去寻找,依次类推。

假如 有一个数字在主存中,这个数字会被load到L3缓存中,L2和L1高速缓存是在CPU的内部的,主存中的数字会被load到不同的CPU中,第一个cpu把x赋值为1,第二个CPU把x赋值为2 那么就会产生一个问题 数据一致性问题。

当多个CPU 访问主存的时候,会因为更改主存中的数据值,可以给总线加锁,也就是说当一个CPU访问主存中的数据的时候 其他的CPU不能对总线进行访问,老的CPU的做法,这个锁被称为总线锁, 但是这种方法效率是比较低的。

新的CPU 会使用各种各样的办法来解决一致性的问题。

MESI 数据一致性协议的一种。

MSI MESI MOSI Synapse FireFly Dragon 都是数据的一致性协议。

为什么会有这么多数据的一致性的协议,CPU的厂商特别多
MESI intel的CPU 使用的MESI一致性协议。
https://blog.csdn.net/xiaowenmu1/article/details/89705740

MESI 这个协议是给每个缓存的内容做了一个标记,如果CPU读取了一个数字进来 X,这个x和主存中的内容相比,到底有没有改变这个值,如果更改了,就标记为m也就是modified如果x可以被独享,就标记为Excusive,如果这个x 被其他的别人也可以使用,就标记为shared,如果x在读的时候被其他CPU改过了,就标记为Invalid。

MESI协议让各个CPU的缓存保持一致性的,如果要使用的数字是Invalid,马上要对这个数字进行计算,那么这种情况 需要到主存中再把这个数字读一遍,它就变得有效了。
MESI 也叫缓存锁。

现在的CPU的底层解决数据的一致性: 通过MESI缓存锁+总线锁 组合来实现的。

4.2 缓存行和伪共享

当我们要把内存中的数据放入到缓存里的时候,它不会只把这一个数据放入到缓存中,

例如int i= 12 它只有4个字节,不会只把这4个字节读取到缓存中,为了提高效率,而是会把这4个字节后面的一对内容都读进去 所读进去的这一行内容 就是一个基本的缓存单位,这个缓存的单位被称为缓存行。

Cacheline 缓存行 ----基本单位。

伪共享 面试题

X和Y 位于同一个缓存行 第一个cpu只使用x 读的时候会把x和y都读出来,第二个CPU只要y变量也会把x和y都读进来。这种情况下会出现伪共享问题。

第一个cpu在修改了x的值之后,会通知其他cpu,x已经被修改了,其他CPU会标记x为invalid 状态,在通知的时候 变更的是整个缓存行的内容,那么这个时候,第二个cpu要使用y这个变量,就会到主存中再次去读取整个缓存行的内容,那么y变量才有效,第二个cpu也会通知其他cpu y的变量又改了一遍,而第一个cpu跟y是没关系,他只要读x 结果他又要把整个缓存行重新读一遍。

两个不相干的cpu在读取内容的时候,会因为缓存行的关系 产生互相影响。这种情况叫做伪共享。

代码:

package com.openlab;

public class CacheLineTest01 {
    private static class T{
        public volatile long x = 0L;
    }
    public static T[] arr = new T[2];

    static{
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{

            for (long i = 0; i<1000_0000L;i++){
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(()->{

            for (long i = 0; i<1000_0000L;i++){
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

200多-300多

代码二 缓存行对其

package com.openlab;

public class CacheLineTest02 {
    private static class Padding {
        public volatile long p1, p2, p3, p4, p5, p6, p7; //缓存行对其
    }

    private static class T extends Padding {
        public volatile long x = 0L;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 1000_0000L; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(()->{
            for (long i = 0; i < 1000_0000L; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

类T 继承了Padding 类 T 对象new 处理就占了64个字节 正好对应一个缓存行,arr [0].x
在写回主存的时候,不需要通知Cpu2 就不会频繁的再去重主存中更新数据。

执行时间
120-130左右

解决伪共享的问题: 使用缓存行对其的方式 能够提高效率 它也会浪费一定的空间。

有volatile 去修饰变量 在进行写的操作 汇编代码 出现第二行汇编代码
Lock前缀的指令在多核的处理器下引发两件事情

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 这个写回内存的操作会使其他的cpu李的缓存的该内存地址的数据无效

为了去提高处理的速度,CPU不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1 和L2 或其他)后,再进行操作,但操作完不知道何时会写入到内存,如果声明volatile的变量的写的操作 JVM就会向CPU发送一条Lock前缀指令,将这个变量所在的缓存行的数据写回到系统内存,就算写回处理内存,其他CPU处理的缓存的值还是旧的,再执行计算操作就会有问题,所以在多个cpu 为了保证各个处理器的缓存是一致的,就会实现缓存的一致性协议。

Volatile的两条实现原则:
1 Lock前缀指令会引起CPU缓存回写到内存中
Lock前缀指令导致在执行指令期间,声明的CPU处理器的Lock信号,该信号在多CPU环境中,cpu可以独占任何共享内存,但CPU的lock信号 一般不锁总线,而是锁缓存,因为总线锁的开销比较大。
对于Intel486和Pentium处理器 在锁的操作时,经常的在总线上声明lock信号,相反它会锁定这块内存区域的缓存,并写回的内存,使用缓存的一致性机制来确保修改的原子性,所以这个操作被称为“缓存锁定”缓存一致性的机制,会阻止同时修改由两个以上的CPU缓存的内存区域数据。

2.一个CPU的缓存回写到内存会导致其他CPU缓存无效。
Intel处理器 使用MESI协议去维护内部缓存和其他处理器缓存的一致性

缓存行对其、追加字节
LinkedTransferQueue 使用的一个内部类类型来定义队列的头head和尾节点,这个内部类PaddedAtomicReferecne相对于父类AtimicReference 只做了一件事情。就是将共享变量追加到64个字节。

4.3 乱序问题

现在的cpu 为了提高效率 会有各种各样的优化,这个优化被叫做CPU乱序执行。

CPU为了提高效率会打乱指令的执行顺序。

读的时候 乱序执行还是好理解一些,写的时候并发叫做合并写。

WCBuffer Write Combine Buffer 合并写。

当cpu需要给某一个数做计算,然后把这个数存储到主存中,在写回主存的时候有L1和L2两个高速缓存。Cpu先把这个数写入L1中 假如L1中没有个数,缓存就没有命中,那么会写入L2中,但是因为L2的速度比较慢,所以在写的过程中,后续的指令也改变了这个数值。那么它就会把这些指令合并到一起 扔到一个合并缓存中,做一个最终的计算结果扔到L2中 这个情况合并写。

参考链接

WCBuffer 这个缓存的级别是高于L1高速缓存 Intel的cpu里面 其实只有4个WC可以被我们同时使用。

一种方式: 写操作的时候 小于4个wc 那么可以同时写1-3个WC 将其写入到L2中
二种方式: 写操作的时候 如果一次写超过4个wc 需要分2次把合并的内容写入到L2中

充分的利用合并写 能够看出来分开执行的效率是速度更快的 效率更高

正是由于CPU有一个特别告诉的缓存 只有4个字节 所以一次填写4个的执行效率更快一些 而4+2 模式 4个填充之后 还要等待另外两个其他的字节进行填充,而3+3的模式只要等待1个字节填充就可以执行,执行完了之后还要等待第二次的执行。

4.4 乱序证明

package com.openlab;
// 这个程序是美团的人写的
public class Disorder {

        private static int x = 0, y = 0;
        private static int a = 0, b =0;

        public static void main(String[] args) throws InterruptedException {
            int i = 0;
            for(;;) {
                i++;
                x = 0; y = 0;
                a = 0; b = 0;
                // 1 0  0 1  1 1  0 0
                Thread one = new Thread(new Runnable() {
                    public void run() {
                        //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                        shortWait(100000);
                        a = 1;
                        x = b;
                    }
                });

                Thread other = new Thread(new Runnable() {
                    public void run() {
                        b = 1;
                        y = a;
                    }
                });
                one.start();other.start();
                one.join();other.join();
                String result = "第" + i + "次 (" + x + "," + y + ")";
                if(x == 0 && y == 0) {
                    System.err.println(result);
                    break;
                } else {
                    System.out.println(result);
                }
            }
        }


        public static void shortWait(long interval){
            long start = System.nanoTime();
            long end;
            do{
                end = System.nanoTime();
            }while(start + interval >= end);
        }


}

1 0 0 1 1 1 如果一旦出现 a-0 b-0 乱序的问题就发生了。

4.5 如何保证特定情况下不乱序

Java的层面 应用volatile关键字去修饰变量 保证有序性

硬件层面:使用CPU的汇编指令

  1. 加锁 加锁百分百能够完成了 提高效率 在CPU的指令级别中很多cpu都做了同一件事,内存屏障 也叫内存栅栏。

  2. 这里说的cpu的内存屏障 和java的内存屏障没有关系。
    拿Intel处理器的内存屏障来说:
    不同的CPU 它的内存屏障指令是不一样的,而且有逻辑上也有区别。

Intel 的内存屏障的设计比较简单 只有三条指令。

指令1 :sfence 写屏障 在sfence 指令前的写操作 必须在sfence 指令的写操作前完成。
指令2: lfence 读屏障 在lfence指令前的读操作 必须在lfence指令的读操作前完成。
指令3:mfence 读写屏障 在mfence 指令的读写操作 必须在mfence指令的读写操作前完成。

参考链接

有序性保障:
Intel lock 指令
原子指令 比如X86上的lock指令是一个FullBarrire 执行的时候会锁住内存子系统来确保执行顺序 甚至可以跨越多个CPU
Software locks 通常使用内存屏障或原子指令来实现变量的可见性和保持程序的执行顺序。

内存屏障(cpu内存屏障 与java内存屏障)
如何实现并发的原子性 可见性 有序性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值