JVM知识体系
2022-03-18 01:19:39 0 举报
AI智能生成
JavaJVM全套体系,慢慢追加
作者其他创作
大纲/内容
类加载机制深度探究
类加载运行全过程
加载
在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的 main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口
.class文件构成
魔数(Magic Number)
1、使用文件名后缀来区分文件类型很不靠谱,后缀可以被随便修改,可以用魔数(Magic Number)实现,根据文件内容本身来标识文件的类型;很多文件都以固定的几字节开头作为魔数,比如PDF文件的魔数是 %PDF-(十六进制 0x255044462D), png文件的魔数是 \x89PNG(十六进制0x89504E47)。
2、使用十六进制工具打开class文件,首先看到的是充满浪漫气息的魔数0xCAFEBABE(咖啡宝贝)
2、使用十六进制工具打开class文件,首先看到的是充满浪漫气息的魔数0xCAFEBABE(咖啡宝贝)
版本号(Minor&Major Version)
1、在魔数之后的四个字节分别表示副版本号(Minor Version)和主版本号(MajorVersion)
2、主版本号是52(0x34),虚拟机解析这个类时就知道这是一个Java 8编译出的类
常量池(Constant Pool)
常量池是类文件中最复杂的数据结构。对于JVM字节码来说,如果操作数是很常用的数字,比如0,这 些操作数是内嵌到字节码中的。如果是字符串常量和较大的整数等,class文件则会把这些操作数存储在 常量池(Constant Pool)中,当使用这些操作数时,会根据常量池的索引位置来查找。
类访问标记(Access Flag)
访问标记(Access flags),用来标识一个类为final、abstract等,由两个字节表示,总共有16个标记位可供使用
类索引(This Class)
this_class表示类索引,
超类索引(Super Class)
super_name表示直接父类的索引
接口表索引(Interface)
interfaces表示类或者接口的直接父接口
字段表(Field)
字段表(fields),类中定义的字段会被存储到这个集合中,包括静态和非静态的字段
方法表(Method)
类中定义的方法会被存储在方法表,方法表也是一个变长结构。 方法method_info结构
方法访问标记
方法名与描述符
方法属性表
属性表(Attribute)
属性表使用两个字节表示属性的个数attributes_count,接下来是若干个属性项的集合,可以看作是一 个数组,数组的每一项都是一个属性项attribute_info,数组的大小为attributes_count ConstantValue属性ConstantValue属性出现在字段field_info中,用来表示静态变量的初始值
Code属性Code属性是类文件中最重要的组成部分,它包含方法的字节码,除native和abstract方 法以外,每个method都有且仅有一个Code属性
验证
校验字节码文件的正确性
文件格式验证
1、是否以魔数0xCAFEBABE开头。
2、主、次版本号是否在当前Java虚拟机接受范围之内。
3、常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
4、指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
5、CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
6、Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
元数据验证
1、第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,这个阶段可能包括的验证点如下:
2、这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
3、这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
4、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
字节码验证
1、第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
2、保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。
3、保证任何跳转指令都不会跳转到方法体以外的字节码指令上。4、保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但 是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类 型,则是危险和不合法的
符号引用验证
1、最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用[插图]的时候,这个转化动作将在连 接的第三阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引 用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、 方法、字段等资源2、符号引用中通过字符串描述的全限定名是否能找到对应的类。
3、在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
4、符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类访问。
准备
1、准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值 的阶段2、首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对 象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值
解析
将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过 程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下 节课会讲到动态链接
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
1.类或接口的解析 2.字段解析 3.方法解析 4.接口方法解析
初始化
类的初始化阶段是类加载过程的最后一个步骤
直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序
使用
卸载
总结
类加载器和双亲委派机制
引导类加载器
负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
扩展类加载器
负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
应用程序类加载器
负责加载ClassPath路径下的类包,主要就是加载你自己写的那 些类
自定义加载器
负责加载用户自定义路径下的类包
总结
1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接 返回。
2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加 载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的 findClass方法来完成类加载。
2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加 载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的 findClass方法来完成类加载。
为什么要设计双亲委派机制?
沙箱安全机制
自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载
当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
全盘负责委托机制
指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入
自定义类加载器示例
自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空 方法,所以我们自定义类加载器主要是重写 方法。
打破双亲委派机制
再来一个沙箱安全机制示例,尝试打破双亲委派机制,用自定义类加载器加载我们自己实现的 java.lang.String.class
Tomcat打破双亲委派机制
Tomcat是个web容器, 那么它要解决什么问题?
1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。
2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程 序,那么要有10份相同的类库加载进虚拟机。
3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的 类库和程序的类库隔离开来。
4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中 运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
Tomcat自定义加载器详解
commonLoader
Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader
Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader
各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader
各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;
总结
1、CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用, 从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则 与对方相互隔离。
2、WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader 实例之间相互隔离。
3、而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的 就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例, 并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
2、WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader 实例之间相互隔离。
3、而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的 就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例, 并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
6大整体结构深度探究
线程私有区域
程序计数器
是当前线程所执行的字节码的行号指示器,无OOM
虚拟机栈
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
栈帧
操作数栈
局部变量
动态链接
方法出口
本地方法栈
本地native方法
线程共享区域
堆-运行时数据区
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代
方法区/永久代(1.8之后元空间)
用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。
运行时常量池(Runtime Constant Pool)
方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
直接内存
jdk1.4后加入NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。可以避免在Java堆和Native堆中来回复制数据。直接内存的分配不会受到Java堆大小的限制.避免大于物理内存的情况。
总结
创建与内存分配机制深度探究
对象的创建
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个
符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
如何分配内存?
指针碰撞(默认)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点 的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟 机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录
解决并发问题的方法?
CAS(compare and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过XX:+/ UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize 指定TLAB大小。
3.初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也 可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问 到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中
对象布局
对象头
标记字段Mark Word
类型指针klass point
数组长度
实例数据instance data
对齐填充(Padding)
5.执行<init>方法
执行<init>方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋
零值不同,这是由程序员赋的值),和执行构造方法。
总结
对象内存分配
对象栈上分配
对象逃逸分析
标量替换
对象在Eden区分配
大对象直接进入老年代
长期存活的对象将进入老年代
对象动态年龄判断
老年代空间分配担保机制
对象内存分配流程图
对象内存回收
引用计数法
定义
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
优点
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决 对象之间相互循环引用的问题
缺点
循环引用
可达性分析算法(GC Roots)
定义
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的 对象都是垃圾对象
根节点
线程栈的本地变量、静态变量、本地方法栈的变量等等
总结
常见引用类型
强引用
普通的变量引用
软引用(SoftReference)
定义
将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放 新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
场景
如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
弱引用(WeakReference)
将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
虚引用
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
finalize()方法最终判定对象是否存活
标记的前提
对象在进行可达性分析后发现没有与GC Roots相连接的引用链
处理过程
1. 第一次标记并进行一次筛选
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,对象将直接被回收。
2. 第二次标记
如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救 自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第 二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:
一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次
如何判断一个类是无用的类
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
字节码文件结构深度探究
垃圾收集算法
垃圾收集器ParNew、CMS、G1、ZGC
调优
JIT
GraalVM初步学习
Java实现JVM
深入理解内存模型与GC
深入理解多线程
0 条评论
下一页