JVM知识点整理
- 1、JVM与java体系结构
- 1.1、java的体系结构
- 1.2、JVM
- 1.2.1、从跨平台的语言到跨语言的平台
- 1.2.2、常用的JVM实现
- 1.2.3、JVM的位置
- 1.2.4、JDK、JER、JDK
- 1.2.5、JVM的整体结构
- 1.2.6、java代码的执行流程
- 1.2.7、JVM的代码模型
- 1.2.8、JVM的生命周期
- 2、类加载子系统
- 2.1、内存结构
- 2.1.1、方法的实现
- 2.1.1.1、注意
- 2.1.2、实例2
- 2.2、类的加载器
- 2.2.1、类的加载过程
- 2.2.2、Loading
- 2.2.3、Linking
- 2.2.3.1、初始化过程
- 2.2.3.2、new对象的过程
- 2.2.3.3、单例模式:双重检查
- 2.2.3.4、为什么要加volatile关键词?
- 2.2.4、Initializing
- 2.2.5、类加载器的分类
- 2.2.5.1、类加载器之间的关系
- 2.2.5.2、Bootstrap ClassLoader
- 2.2.5.3、Extension ClassLoader
- 2.2.5.4、AppClassLoader
- 2.2.5.5、自定义类加载器
- 2.2.5.5.1、什么要自定义加载器?
- 2.2.5.5.2、自定义加载的步骤
- 2.2.5.5.3、自定义类加载器加载自加密的class(扩展)
- 2.2.5.5.4、自定义父加载类
- 2.2.5.5.5、自定义热部署(Tomcat热部署的简单实现)
- 2.2.6、ClassLoader
- 获取classloader的途径
- 2.2.7、双亲委派
- 2.2.7.1、机制
- 2.2.7.1.1、工作原理
- 2.2.7.1.2、父加载类
- 2.2.7.1.3、为什么要使用双亲委派?
- 2.2.7.1.4、沙箱安全机制
- 2.2.7.1.5、如何打破双亲委派机制?
- 2.2.7.2、编译器(扩展)
- 2.2.7.2.1、混合模式
- 2.2.7.2.2、热点代码编译
- 2.2.7.2.2.1、检测热点代码
- 2.2.7.2.3、编译模式的选择
- 2.2.7.3、懒加载(扩展)
- 2.2.8、类的主动使用和被动加载
- 2.2.8.1、判断两个class对象是否为同一个类的条件
- 2.2.8.2、对类加载器的引用——动态链接
- 2.2.8.3、主动使用/被动使用
- 3、字节码与类的加载器
- 3.1、Class文件结构
- 3.1.1、概述
- 3.1.1.1、字节码文件的跨平台性
- 3.1.1.2、java的前端编译器
- 3.1.1.3、透过字节码指令看代码细节
- 3.1.1.3.1、实例
- 3.1.2、Class文件:虚拟机的基石
- 3.1.2.1、Class文件本质
- 3.1.2.2、Class文件格式
- 3.1.2.3、Class文件的数据类型
- 3.1.3、Class文件结构
- 3.1.3.1、实例
1、JVM与java体系结构
1.1、java的体系结构
1.2、JVM
虚拟机就是一台虚拟的计算机。他就是一款软件,用来执行一系列虚拟计算机指令。大体上虚拟可以分为系统虚拟机和程序虚拟机。吞吐量优先
系统虚拟机:Visual Box、VMWare,完全是对物理计算机的仿真
程序虚拟机:JVM,专门为执行单个计算机程序而设计
1.2.1、从跨平台的语言到跨语言的平台
- java是一个跨平台的语言
- jvm是一个跨语言的平台
- java虚拟机平台上运行非java语言编写的程序
- 只要能编译成class文件,就可以在虚拟机上运行
- jvm和java是没有关系的,之和class文件有关系
- jvm是一种规范
- https://docs.oracle.com/javase/specs/index.html
- jvm是一个虚构出来的计算机
- 字节码指令集
- 内存管理:栈、堆、方法区等
- java虚拟机就是二进制字节码的运行环境
java不是最强大的语言,但是JVM是最强大的虚拟机
各种语言之间的交互不存在任何困难,就像使用自己语言的原生API一样方便,因为他们始终都运行在一个虚拟机上
1.2.2、常用的JVM实现
- Sun Classic VM
- 是世界上第一款商用的java虚拟机
- 在内部只提供了解释器,没有JIT(即时编译器)
- Exact VM
- 准确式的内存管理:可以知道内存中某个位置的数据具体是什么类型
- 具备现代高性能虚拟机的雏形:热点探测、编译器与解释器混合工作模式
- 最终被Hstspot虚拟机替代
- Jrocket
- 专注于服务器端应用
- 不太注重程序启动速度,因此内部不包含解析器,全部代码都是靠及时编译器编译后执行
- J9
- 广泛用于IBM的各种java产品中
- Azul VM
- 与特定硬件平台绑定、软硬件配合的专有虚拟机
- 每个Auzl VM实例都可以管理至少数十个CPU和数百个GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器、转悠硬件有花的线程调优等优秀特性
- Liquid VM
- Liquid VM不需要操作系统的支持,或者说让它自己本身实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持等。
- Taobao VM
- 基于openJDK开发的自己的定制版本的AlibabaJDK
- 深度定制且开源的高性能服务器
- 即将生命周期较长的java对象从heap中移到heap之外,并且GC不能管理GCIH内部的java对象,以此达到降低GC的回收频率和提高GC的回收效率的目的
- GCIH中给的对象还能够在多个java虚拟机进程中实现共享
- 硬件严重依赖Intel的cpu,损失了兼容性,但是提高了性能(凡是和操作系统、硬件耦合高的,性能都强)
- Graal VM
- 最有可能取代Hotspot的虚拟机
- 跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用
- 常用的是Hotspot
- Hotspot占有绝对的市场地位
- 大多数默认都是Hotspot,相关机制也主要指Hotspot的GC机制。(J9、Jrockit都没有方法区的概念)
- 名称中的Hotspot指的是他的热点代码探测技术
- 通过计数器找到最具编译价值代码,触发即时编译器或栈上替换
- 通过编译器与解释器协同工作,在最优先的程序响应时间与最佳执行性能中取得平衡
- 解释器:最优先的响应时间——速度快(假设去旅游,不管什么方法,即刻出发,只走路)
- 编译器:最佳的执行性能——方法(假设去旅游,只考虑方法,只坐公交车,不走路)
1.2.3、JVM的位置
JVM试运行在操作系统之上的,他与硬件没有直接的交互
1.2.4、JDK、JER、JDK
- JDK:(Java Development Kit)java开发工具包(开发所用的包)
- JRE:(Java Runtime Envirment)java运行环境(核心类库,Object、String……)
- JVM:(Java Virtual Machine)java虚拟机
1.2.5、JVM的整体结构
多个线程共享方法区和堆
栈、本地方法栈、程序计数器是每个线程独有一份的
1.2.6、java代码的执行流程
xx.java文件通过javac编译得到xx.class文件。当执行java命令的时候,.class文件会被ClassLoader(类加载器)装载到JVM中。装载后,通过调用字节码解释器、JIT即时编译器对代码进行解释和编译,编译过后用执行引擎进行执行,与OS硬件交互。
所用到的类库,会在class文件被加载到 JVM的同时,也被装载到类加载器中。
常用的代码会在第一次编译后被即JIT做成本地编译,不会一次次的去进行解释和即时编译。
如果每一句代码都要进行解释和编译,java就无法做到跨平台了
1.2.7、JVM的代码模型
java编译器输入的指令流基本上是一种基于栈的指令集框架,另外一种指令集架构则是基于寄存器的指令集架构。
- 基于栈式架构的特点(八位为一个单位)
- 设计和实现更简单,适用于资源受限的系统;
- 避开了寄存器的分配难题:使用零地址指令方式分配。
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
- 不需要硬件支持,可移植性更好,更好实现跨平台
- 基于寄存器架构的特点(十六位为一个单位)
- 典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虛拟机。
- 指令集架构则完全依赖硬件,可移植性差
- 性能优秀和执行更高效;
- 花费更少的指令去完成一项操作。
- 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。
由于跨平台性的设计,Java的指令都是根据栈来设计的。
栈:跨平台性、指令集小、指令多:执行性能比寄存器差
1.2.8、JVM的生命周期
-
虚拟机的启动
- Java虚拟机的启动是通过引导类加载器(bootstrap class loader) 创建一个初始类(initial class) 来完成的,这个类是由虚拟机的具体实现指定的。
-
虚拟机的执行
- 一个运行中的java虚拟机有着一个清晰的任务:执行java程序
- 程序开始执行时他才运行,程序结束时他就停止
- 执行一个所谓的java程序的时候,真真正正执行的是一个叫做java虚拟机的进程
-
虚拟机的退出
- 有如下几种情况:
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常结束
- 由于操作系统出现错误而导致java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且java安全管理器也允许这次exit或halt操作
- 除此之外,JNI规范描述了用JNI Invocation API来加载或卸载java虚拟机时,java虚拟机的退出情况
- 有如下几种情况:
2、类加载子系统
2.1、内存结构
当自己手写一个java虚拟机的话,主要考虑哪些结构呢?
类加载器和执行引擎
2.1.1、方法的实现
1.首先利用获取到的16进制码去汇编表中查找对应数字所代表的含义
查找此汇编语言的含义
2.查询到是哪条指令后,再将这条指令翻译过来
3.查询下一个b7–>invokespecial,重复上述操作
读到2a的时候,this压栈,之后读取下一条指令,直到b1
2.1.1.1、注意
2a是开始,b1是结束
这五个字节是文件的构造方法的具体实现
2a是aload-0,表示把this压栈,表示整个语句的开始
然后再编译b7……
01表示常量池里的第一项,java.lang.Object
b1表return,是整个语句的结束
2.1.2、实例2
代码1:
实现过程:
先加载父类的构造方法,之后再将自己的成员变量初始化
0:把this压栈
3:第二次压栈,把变量压入栈,后赋值
2.2、类的加载器
Loading:将二进制文件,加载到内存
Linking-verification:判断是否符合class文件的标准 cafe babe
Linking-perparation:给class静态变量赋默认值
Linking-resolution:class文件用到的符号引用,转换为直接能访问到的内容(内存地址)
Initializing:静态变量赋值初始值
2.2.1、类的加载过程
2.2.2、Loading
1.通过一个类的全限定名获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为元空间(方法区)的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2.2.3、Linking
2.2.3.1、初始化过程
- Linking——Verification
- 验证文件是否符合JVM规定
- Linking——Preparation
- 为静态成员变量分配内存并且设置该类变量的默认初始值
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化
- 这里不会为实例变量分配初始化,静态变量会分配在方法区中,而实例变量是会随着变量一起分配到java堆中
- Linking——Resolution
- 将类、方法、属性等符号引用解析为直接引用
- 常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
- 解析操作往往会伴随着JVM在执行完初始化之后再执行
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等……
- Initializing
- 调用类初始化代码,给静态成员变量赋初始值
实例1:
运行结果:3
运行过程:调用T.count时,首先将T.class通过app加载到内存,再经过Verification进行校验,之后再通过Preparation对T里面的静态成员变量赋默认值,此时count是空0,t是空值,再进行Resolution,之后再通过Initializing对静态成员变量赋初始值,此时count是2,之后t会被赋值new T(),对count++,因此count变为3。
实例2:
运行结果:2
运行过程:调用T.count时,首先将T.class通过app加载到内存,再经过Verification进行校验,之后再通过Preparation对T里面的静态成员变量赋默认值,此时t是空值,,count是0,再进行Resolution,之后再通过Initializing对静态成员变量赋初始值,此时t会被赋值new T(),对count++,此时count是1,之后count又被赋值为2,因此count为2。
2.2.3.2、new对象的过程
第一步:给new的对象申请内存,内存申请完毕后,先给成员变量赋默认值
第二步:调用构造方法,给成员变量赋初始值
静态变量是在类加载的初始化时分为两步赋值,对象中的成员变量的赋值是在类被创建的时候被分为两步
load:默认值—初始值
new:申请空间—默认值—初始值
2.2.3.3、单例模式:双重检查
两次检查 INSTANCE == null ,但在执行INSTANCE = new Mgr06()时,在初始化阶段将里面的成员变量赋值为0时,另一个线程来执行此方法,此时 INSTANCE != null ,并被返回,但是返回的成员变量均为初始值。
解决方法:加volatile关键词(指令重排)
2.2.3.4、为什么要加volatile关键词?
二进制码:
1:创建内存
3:调用构造方法,给成员变量赋初始值
4:将引用值赋值给 “t”
指令重排发生时,可能先发生4再发生3,此时赋的值为默认值
2.2.4、Initializing
init:构造方法
main:main方法
clinit:类构造器方法
类的构造器(init)
- 如果不手动的添加,都会默认的添加一个空参的构造器
类的构造器方法(clinit)
- 初始化阶段就是执行类构造器方法
<clinit> ()
的过程。 - 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。没有静态代码块就没有上述过程。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
- JVM会保证子类的clinit执行前,父类的clinit已经执行完毕。
- 虚拟机必须保证一个类的clinit方法在多线程下被同步加锁。
<clinit> ()
不同于类的构造器。(关联:构造器是虚拟机视角下的<init>()
)
public class ClassInitTest {
private static int num = 1;
private static int number;
public ClassInitTest() {
}
public static void main(String[] args) {
System.out.println(num);
System.out.println(number);//如果没有声明全局变量,会报错,非法的前项引用
}
static {
num = 2;
number = 20;
System.out.println(num);
number = 10;
}
}
- 若该类具有父类,JVM会保证子类的()执行前,父类的 ()已经执行完毕。
public class ClinitTest1 {
static class Father{
public static int A = 1;
static{
A = 2;
}
}
static class Son extends Father{
public static int B = A;
}
public static void main(String[] args) {
//加载Father类,其次加载Son类。
System.out.println(Son.B);//2
}
}
- 虚拟机必须保证一个类的() 方法在多线程下被同步加锁。(static代码块只能被执行一次)
public class DeadThreadTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread dead = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r,"线程1");
Thread t2 = new Thread(r,"线程2");
t1.start();
t2.start();
}
}
class DeadThread{
static{
if(true){
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while(true){
}
}
}
}
2.2.5、类加载器的分类
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defind ClassLoader)
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
这里的四者之间的关系是包含关系,不是上下层关系,也不是父子类的继承关系。
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
2.2.5.1、类加载器之间的关系
在创建的时候,会同时生成底层二进制码文件和一个class对象,当程序运行时,会通过这个class对象调用和访问底层的二进制码。过程是先自下而上(上图里的加载器)的查找,再自上而下的查找,当返回为null时,一定是Bootstrap加载器加载的
2.2.5.2、Bootstrap ClassLoader
-
启动类加载器/引导类加载器
-
这个类加载使用C/C++语言实现的,嵌套在JVM内部。
-
它用来加载Java的核心库(/…/JAVA HOME/jre/lib/rt.jar/resources. jar或sun.boot.class.path路径下的内容) ,用于提供JVM自身需要的类
-
并不继承自java. lang.ClassLoader,没有父加载器。
-
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
-
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。
2.2.5.3、Extension ClassLoader
- 扩展类加载器
- Java语言编写,由sun.misc. Launcher$ExtClassLoader实现。
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext. dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
2.2.5.4、AppClassLoader
- 应用程序类加载器/系统类加载器
- java语言编写,由sun . misc. LauncherSAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader #getSystemClassLoader ()方法可以获取到该类加载器
运行结果:
被bootstrap加载的jar包
被extension加载的内容
被app加载的内容
2.2.5.5、自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
2.2.5.5.1、什么要自定义加载器?
- 隔离加载类
- 修改类的加载方法
- 扩展加载源
- 防止源码泄露
2.2.5.5.2、自定义加载的步骤
1.继承ClassLoader
2.重写模板方法findClass
负责将底层二进制编码转换为class对象的方法叫做defineClass,底层必须有这个方法
3.调用defineClass(将二进制码文件转换为class类对象的方法)
测试类
执行过程:
先去app找,找不到再去ext找,找不到再去bootstrap找,找不到ext再找,找不到APP再找,找不到自定义找,在c://text目录下找到了class对象
2.2.5.5.3、自定义类加载器加载自加密的class(扩展)
简单加密:二进制文件经过两次异或还是原值,在异或前再加入一个seed的二进制代码片段
自己编译的时候,先异或,再减去seed二进制代码片段
防止反编译
防止篡改
2.2.5.5.4、自定义父加载类
2.2.5.5.5、自定义热部署(Tomcat热部署的简单实现)
重写loadClass之前:
方法类:
运行结果为true,说明第一次编译后,第二次编译时通过双亲委派访问到了第一次编译的结果,因此值相同。
重写loadClass之后:
重写的方法中,第一步就是先查找需要编译的class文件,如果找到了就直接编译,如果没有找到,就交给父加载类编译。排除了原有的判断是否加载过的这一步骤,而是直接进行编译
方法类:
运行结果为false,说明第一次编译后,第二次编译时直接重新编译了底层的class文件。
2.2.6、ClassLoader
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader (不包括启动类加载器)
方法名称 | 描述 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 |
findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 |
findLoadClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 |
defineClass(String name.byte[] b,int off.int len) | 把字节数组b中的内容转换为一个java类,返回结果为java.lang.Class类的实例 |
resolveClass(Class<?> c) | 链接一个指定的java类 |
获取classloader的途径
-
方式一:获取当前类的ClassLoader
clazz. getClassLoader () -
方式二:获取当前线程上下文的ClassLoader
Thread. currentThread() .getContextClassLoader () -
方式三:获取系统的ClassLoader
ClassLoader.getSystemClassLoader () -
方式四:获取调用者的ClassLoader
DriverManager . getCallerClassLoader ()
2.2.7、双亲委派
2.2.7.1、机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
2.2.7.1.1、工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达项层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
是一个孩子向父亲的方向,然后父亲向孩子方向的双亲委派过程
2.2.7.1.2、父加载类
父加载类不是“类加载器的加载器”,也不是“类加载器的父类加载器”
2.2.7.1.3、为什么要使用双亲委派?
- 保护程序安全,防止核心API被随意篡改
- 主要原因是为了安全,假设当有人自拟了一个java.lang.String类,直接上传到类库中将原有的类库覆盖,之后将自己的代码上传到互联网上,当用户设置账号密码时,就会用到这个类,万一这个类中有一个功能可以将该用户的所有账户信息发送给开发者,那么个人信息将暴露。而双亲委派就不会出现此类情况,当排查到bootstarp时,发现已经执行过了,就不会覆盖掉原有的类。
- 避免类的重复加载
- 次要原因可以避免资源的浪费,在双亲委派的过程中,如果发现已经加载过了就不会再次进行加载。
2.2.7.1.4、沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
2.2.7.1.5、如何打破双亲委派机制?
- 重写loadClass()
- 何时打破过?
- JDK1.2.之前,自定义ClassLoader都必须重写loadClass()
- ThreadContextClassLoader可以实现基本类调用实现类代码,通过thread.setContextClassLoader指定
- 热启动、热部署
- osgi tomcat都有自己的模块指定classloader(可以加载同一类库的不同版本)
2.2.7.2、编译器(扩展)
2.2.7.2.1、混合模式
解释器 —— bytecode intepreter
JIT ——Just In-Time compiler(把java代码编译成本地代码执行)
- 混合模式
- 混合使用解释器+热点代码编译
- 起始阶段采用解释执行
- 热点代码检测
- 多次被调用的方法(方法计数器:监测方法执行频率)
- 多次被调用的循环(循环计数器:检测循环执行频率)
- 进行编译
2.2.7.2.2、热点代码编译
当一段代码被大量的重复使用时,解释器不停地解释相同的代码,有损效率,为了提升效率,热点代码编译将这段代码编译为本地代码,当下次再需要这段代码时,就不用再执行解释器解释了,直接执行本地代码,这样就提高了编译效率
2.2.7.2.2.1、检测热点代码
-XX:CompileThreshold = 10000
2.2.7.2.3、编译模式的选择
- -Xmixed默认为混合模式 开始解释执行,启动速度较快对热点代码实行检测和编译
- -Xint 使用解释模式,启动很快执行稍慢
- -Xcomp使用纯编译模式,执行很快,启动很慢(指需要大量类进行编译时)
2.2.7.3、懒加载(扩展)
什么时候需要什么时候加载
- 严格讲应该叫lazy lnitializing
- JVM规范并没有规定何时加载
- 但是严格规定了什么时候必须初始化
- new getstatic putstatic invokestatic指令,访问final变量除外
- java.lang.reflect对类进行反射调用时
- 初始化子类的时候,父类首先初始化
- 虚拟机启动时,被执行的主类必须初始化
- 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF getstatic REF putstatic REF invokestatic的方法句柄时,该类必须初始化访问内部类时,需要外部类/内部类的名字
2.2.8、类的主动使用和被动加载
2.2.8.1、判断两个class对象是否为同一个类的条件
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader (指ClassLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
2.2.8.2、对类加载器的引用——动态链接
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会**将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。**当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
2.2.8.3、主动使用/被动使用
- 主动使用,又分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如: Class . forName (“com. atguigu . Test”) )
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK 7开始提供的动态语言支持:
- java. lang. invoke . MethodHandle实例的解析结果REF getStatic、REF putStatic、 REF invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
3、字节码与类的加载器
3.1、Class文件结构
3.1.1、概述
3.1.1.1、字节码文件的跨平台性
-
java语言的跨平台性
- java源代码编译成字节码文件后,如果在不同的平台上运行时,不用再此编译。
- 这点不再那么吸引人了,大多数其他语言也可以做到
- 跨平台已经快成为一门语言必选的特性
-
java虚拟机,跨语言的平台
- java虚拟机不和包括java在内的任何语言绑定,他只和Class文件这种特定的二进制文件格式所关联
- 所有的字节码文件必须遵守java虚拟机规范
-
想要让规格程序正确地运行在JVM中,java虚拟机就必须要被编译为符合JVM规范的字节码
- 前端编译器的主要任务就是负责将符合java语法规范的java代码转换为符合JVM规范的字节码文件
- javac是一种能够将java原码编译为字节码的前端编译器
- Javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。
-
Oracle的JDK软件包括两部分内容:
- 一部分是将Java源代码编译成Java虚拟机的指令集的编译器;
- 另一部分是用于实现Java虚拟机的运行时环境。
3.1.1.2、java的前端编译器
Java源代码的编译结果是字节码,那么肯定需要有种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。
- javac是一种能够将Java源码编译为字节码的前端编译器。
- Eclipse中内置的ECJ(Eclipse Compiler for Java)编译器是一种增量式编译器
前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的JIT编译器负责。
3.1.1.3、透过字节码指令看代码细节
-
类文件结构有几个部分?
- 魔数
- 版本号
- 常量池
- 访问表示
- 类索引、父类索引、接口索引
- 字段表
- 方法表
- 属性表
-
知道字节码吗?字节码都有哪些?Integerx=5;inty=5;比较x==y都经过哪些步骤?
-
通过Jclasslib观察字节码文件
-
public class IntegerTest { public static void main(String[] args) { Integer x = 5; int y = 4; System.out.println(x == y); } }
-
3.1.1.3.1、实例
/*
成员变量(非静态的)的赋值过程:
① 默认初始化
② 显式初始化 /代码块中初始化
③ 构造器中初始化
④ 有了对象之后,可以“对象.属性”或"对象.方法"的方式对成员变量进行赋值。
*/
class Father {
int x = 10;
public Father() {
this.print();
x = 20;
}
public void print() {
System.out.println("Father.x = " + x);
}
}
class Son extends Father {
int x = 30;
// float x = 30.1F;
public Son() {
this.print();
x = 40;
}
public void print() {
System.out.println("Son.x = " + x);
}
}
public class SonTest {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.x);
}
}
运行结果
字节码解析
3.1.2、Class文件:虚拟机的基石
- 字节码文件
- 源代码经过编译器去编译后便会生成一个字节码文件,他的内容就是JVM指令,而不是想C/C++一样生成机器码。
- 字节码指令
- Java虚拟机的指令是由一个字节码长度、代表着某种特定操作含义的操作码以及跟随其后的零至多个代表此操作所需参数的操作数所构成。部分指令并不包含操作数,只有操作码
- 操作数+操作码
- 解读方式
- NotePade++和插件HEX-Editor/Binary Viewer
- javap指令
- JClasslib
3.1.2.1、Class文件本质
一组以8位字节为基础单位的二进制流
3.1.2.2、Class文件格式
- 字节码文件中的字节顺序、数量都是被严格限定的,不能随意修改
- 采用一种类似于C语言的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别表示一个字节、两个字节、四个字节和八个字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“info"结尾。表用于描述有层次关系的复合结构的数据,整个Class 文件本质上就是一张表。 由于表没有固定长度,所以通常会在其前面加上个数说明
3.1.2.3、Class文件的数据类型
数据类型 | 定义 | 说明 |
---|---|---|
无符号数 | 无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。 | 其中无符号数属于基本的数据类型。 以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节 |
表 | 表是由多个无符号数或其他表构成的复合数据结构。 | 所有的表都以“_info”结尾。 由于表没有固定长度,所以通常会在其前面加上个数说明。 |
3.1.3、Class文件结构
Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。
结构如下:
类型 | 名称 | 说明 | 长度 | 数量 |
---|---|---|---|---|
u4 | magic | 魔数,识别Class文件格式 | 4个字节 | 1 |
u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 |
u2 | major_version | 主版本号(大版本) | 2个字节 | 1 |
u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 |
cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 |
u2 | access_flags | 访问标识 | 2个字节 | 1 |
u2 | this_class | 类索引 | 2个字节 | 1 |
u2 | super_class | 父类索引 | 2个字节 | 1 |
u2 | interfaces_count | 接口计数器 | 2个字节 | 1 |
u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count |
u2 | fields_count | 字段计数器 | 2个字节 | 1 |
field_info | fields | 字段表 | n个字节 | fields_count |
u2 | methods_count | 方法计数器 | 2个字节 | 1 |
method_info | methods | 方法表 | n个字节 | methods_count |
u2 | attributes_count | 属性计数器 | 2个字节 | 1 |
attribute_info | attributes | 属性表 | n个字节 | attributes_count |
3.1.3.1、实例
public class Demo {
private int num = 1;
public int add(){
num = num + 2;
return num;
}
}