Class文件结构
-
视图文件结构-jclasslib
-
Magic Number:【CA FE BA BE】 java文件
-
Minor_Version:小版本号
-
Major_Version:大版本号
-
constant_pool_count:常量池数量
-
constant_pool:常量池表 (constant_pool_count-1)
-
access_flags:类的访问权限
-
this_class:当前类的名称
-
super_class:父类的名称
-
interface_count:接口的数量
-
interfaces:接口名称表
-
fields_count:字段数量
-
fields:字段表
-
methods_count:方法数量
-
methods:方法表
-
attributes_count:属性数量
-
attributes:属性表
JVM内存结构
类加载子系统
-
Loading 加载阶段
-
1、类加载器
- bootstrap :启动类加载器,加载 $JAVA_HOME/jre/lib/*.jar核心类库;
- extension:标准扩展类加载器,加载$JAVA_HOME/jre/lib/ext/*.jar或者-Djava.ext.dirs指定;
- application:应用程序(系统类)加载器,加载ClassPath中指定的jar包以及目录中的class;
- custom:自定义类加载器,继承ClassLoader抽象类,重写findClass方法,如果重写loadClass可以打破双亲委派机制;
-
2、双亲委派机制
- 防止重复加载同一个.class。通过委托上一层检查,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
-
3、 解释模式(-Xint)、编译模式(-Xcomp)、混合模式(-Xmixed,热点代码检测(HotSpot),默认-XX:CompileThreshold=10000)
-
-
Linking 连接阶段
- 1、verification 验证:验证文件是否符合JVM规范
- 文件格式验证、元数据验证、字节码验证、符号引用验证
- 2、preparation 准备:为类的静态变量分配内存,并将其赋默认值
- 3、resolution 解析:将类、方法、属性的符号引用替换为直接引用,常量池中各种符号引用解析为指针、偏移量等内存地址的直接引用
- 1、verification 验证:验证文件是否符合JVM规范
-
Initializing 初始化
- 静态成员变量赋初始值。
- 当有继承关系时,先初始化父类再初始化子类,所以创建一个子类时其实内存中存在两个对象实例。
- 当有成员变量时,new创建对象时,申请内存空间,成员变量赋默认值,调用构造器后成员变量赋初始值。
运行时数据区
-
程序计数器(Program Counter Register )
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为「线程私有」的内存。
- 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
- 如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)
-
虚拟机栈(JAVA Virtual Machine Stack)
- 与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。
- 虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身)和 returnAddress 类型(指向了一条字节码指令的地址)。
- 异常情况
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;
如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。 - Demo
- 指令
- bipush 8:把8压入操作数栈(Operand Stack);
- istore_1:出栈,把栈中8这个值赋值给局部变量(Local Variable Table)表位置是1的变量i,变量等于8;
- iload_1:把局部变量表(Local Variable Table)中位置是1的那个变量,压入操作数栈(Operand Stank);
- iinc 1 by 1:把局部变量表中(Loacal Variable Table)位置是1的那个变量加1,变量等于9;
- istore_1:出栈,把栈中8这个值赋值给局部变量(Local Variable Table)表位置是1的变量i,变量等于8;
- return:返回
- 指令
-
本地方法栈(Native Method Stacks)
本地方法栈与虚拟机栈所发挥的作用是非常相似的,虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务。本地方法栈则是为虚拟机使用到的本地(Native)方法服务。本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。异常情况与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。
-
堆(Heap)
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,java中所有的对象实例都在这里分配内存。- 异常情况
如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。
- 异常情况
-
方法区(Method Area)
方法区与堆一样,是各个线程共享的内存区域,它用于存储已JVM加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。- Perm Space (<1.8)
字符串常量位于PermSpace
FGC不会清理
大小启动的时候指定,不能变 - Meta Space (>=1.8)
字符串常量位于堆
会触发FGC清理
不设定的话,最大就是物理内存 - 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 - 异常情况
如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。
- Perm Space (<1.8)
执行引擎
执行引擎是Java虚拟机核心的组成部分,它是用于负责装载字节码到其内部,但是字节码并不能直接在操作系统上运行,那么执行引擎就是将字节码指令解释/编译为对应平台上的本地机器指令。简单来说,JVM执行引擎充当了将高级语言翻译为机器语言的翻译者。
- 解释器
当Java虚拟机启动时会根据预定义对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“解释”为对应平台的本地机器指令执行。响应速度快,但是总体效率较低。 - JIT编译器
虚拟机将源代码直接编译成本地机器平台相关的机器语言。寻找热点代码(就是高频执行的代码)将其放入元空间中,也就是元空间中存放的JIT缓存代码。响应速度稍慢,但是总体效率很高。有寻找热点代码的特性。
HotSpot虚拟机内置了两个即时编译器,分别称为Client Compiler和Server Compiler。
Java语言是半编译半解释型语言。不要理解成是先编译后解释,是Java在执行Java代码时,HotSpot虚拟机默认是解释执行与编译执行二者结合起来进行的。
默认就是使用的Service模式、JIT编译和解释器执行混合模式(这样的模式执行效率最高)。
方法计数器,默认阀值,在Client模式下是1500次,Server是10000次,可以通过参数“-XX:CompileThreshold”来设定。
本地方法接口
一个Native Method就是Java调用非Java代码的接口(Java Native Interface),在定义一个native method的时候,并不提供实现体,其实现体是由非Java语言在外面实现的。
CPU内存结构图
缓存一致性协议
CPU中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位(bit)表示):
-
M: 被修改(Modified)
该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。
当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。 -
E: 独享的(Exclusive)
该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。
-
S:共享的(Shared)
该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中数据,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。 -
I: 无效的(Invalid)
该缓存是无效的(可能有其它CPU修改了该缓存行)。
乱序问题
CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据),去同时执行另一条指令,前提是,两条指令没有依赖关系,乱序执行。
内存屏障
-
1、硬件内存屏障 X86
sfence: store| 在sfence指令前的写操作,必须在sfence指令后,写操作前完成。
lfence:load | 在lfence指令前的读操作,必须在lfence指令后,读操作前完成。
mfence:store/load | 在mfence指令前的读写操作,必须在mfence指令后的读写操作前完成。
-
2、原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。
-
3、JVM级别如何规范(JSR133)
-
LoadLoad屏障:
对于这样的语句Load1; LoadLoad; Load2,
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 -
StoreStore屏障:
对于这样的语句Store1; StoreStore; Store2,
在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 -
LoadStore屏障:
对于这样的语句Load1; LoadStore; Store2,
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 -
StoreLoad屏障:
对于这样的语句Store1; StoreLoad; Load2,
在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
-
伪共享问题
读取缓存以cache line为基本单位,目前64bytes
位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题,使用缓存行的对齐能够提高效率。
Java内存模型
java内存模型 - 同步操作与规则
Java内存模型-同步八种操作
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
- read(读取) : 作用于主内存的变量 , 把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用域工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量
- use (使用) :作用于工作内存的变量 , 把工作内存中的一个变量值传递给执行引擎use (使用) : 作用于工作内存的变量 , 把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
- store (存储):作用于工作内存的变量 , 把工作内存中的一个变量的值传送到主内存中 , 以便随后的write的操作
- write (写入): 作用于主内存的变量, 它把store操作从工作内存中一个变量的值传送到主内存的变量中
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来 , 释放后的变量才可以被其他线程锁定
Java内存模型-同步规则
- 如果要把一个变量从主内存中复制到工作内存, 就需要按顺序地执 行read和load操作 , 如果把变量从工作内存中同步回主内存中, 就要按顺序地执行store和write操作. 但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行
- 不允许read和load、 store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
- 一个新的变量只能在主内存中诞生, 不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。 即就是对一个变量实施use和store操作之前 , 必须先执行过了assign和load操作
- 一个变量在同一时刻只允许一条线程对 其进行lock操作 , 但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值, 在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定 , 则不允许 对它执行unlock操作 ; 也不允许去unlock一个被其他线程锁定的变量
- 对一个变量执行unlock操作之前 , 必须先把此变量同步到主内存中(执行store和write操作)
对象内存中的布局
-
对象的创建过程
- loading 加载
- linking 链接 (verification验证、preparation准备、resolution解析)
- initializing 初始化
- 申请对象内存,成员变量赋默认值
- 成员变量初始化,实例代码块初始化,构造方法执行,优先调用父类构造器
-
虚拟机配置
java -XX:+PrintCommandLineFlags -version
- -XX:InitialHeapSize=132046336(初始堆大小)
- -XX:MaxHeapSize=2124486656 (最大堆大小)
- -XX:+UseCompressedClassPointers class指针
- -XX:+UseCompressedOops 普通对象指针
- -XX:-UseLargePagesIndividualAllocation
- -XX:+UseParallelGC
-
普通对象
-
对象头:MarkWord,8个字节
-
ClassPointer:-XX:+UseCompressedClassPointers 指针开启压缩4个字节,不开启为8字节
-
实例数据 instance:
- -XX:+UseCompressedOops【 Oops:Ordinary Object Pointers普通对象指针】:指针开启压缩4个字节,不开启为8字节
-
padding:对齐,8的倍数
-
-
对象头MarkWrod
- lock:无锁(01)->偏向锁(01)–>轻量级锁(00)–>重量级锁(10)–>GC标记(11)
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
- age:4位的Java对象年龄,最大值15。
- identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。
- thread:持有偏向锁的线程ID。
- epoch:偏向时间戳。
- ptr_to_lock_record:指向栈中锁记录的指针,62,表示指向轻量级锁。
- ptr_to_heavyweight_monitor:指向管程Monitor的指针,62,表示指向重量级锁。