理解:
类文件由单个ClassFile结构组成。
任何一个Class文件都对应着一个类或接口的定义信息,但反过来,类或接口并不一定都得定义在Class文件里(譬如类或接口可以通过类加载器直接生成)。
Class文件应当是一串二进制的字节流,无论以何种形式。Class文件是一组以8位字符为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中。
Class文件中存储数据的类型:无符号和复合数据类型。无符号:u1、u2、u4、u8分别表示1/2/4/8字节的无符号数。
类文件结构的组成部分:
ClassFile {
u4 magic; //Class文件的标志
u2 minor_version; //Class的小版本号
u2 major_version; //Class的大版本号
u2 constant_pool_count; //常量池的数量
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //Class的访问标记
u2 this_class; //当前类
u2 super_class; //父类
u2 interfaces_count; //接口
u2 interfaces[interfaces_count]; //一个类可以实现多个接口
u2 fields_count; //Class文件的字段属性
field_info fields[fields_count]; //一个类会可以有个字段
u2 methods_count; //Class文件的方法数量
method_info methods[methods_count]; //一个类可以有个多个方法
u2 attributes_count; //此类的属性表中的属性数
attribute_info attributes[attributes_count]; //属性表集合
}
具体介绍:
| 参数 | 说明 |
|---|---|
| u4 magic | 魔数,确定这个文件是否为一个能被虚拟机接收的Class文件 |
| u2 minor_version u2 major_version | Class文件的版本号 前者次版本号,后者主版本号 |
| cp_info constant_pool[constant_pool_count-1] | 常量池。减1因为空出索引0表示“不引用任何一个常量池项” 存放的内容是字面量和符号引用。字面量 = java层面的常量概念(字符串、常量(final修饰的成员变量,局部变量不行)、非final修饰的基本数据类型); 符号引用 = 编译原理方面概念,包括(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符) |
| u2 access_flags | 访问标志,识别一些类或接口层次的访问信息。 这个Class是接口还是类、是否定义为public、abstract类型等 |
| u2 this_class / super_class / interfaces[interface_count] | 当前类索引、父类索引和接口索引集合 一个类可以实现多个接口 |
| field_info fields[fields_count] | 字段结构,用于描述接口或类中声明的变量,包括类级变量或实例级变量,不包括方法内声明的变量 |
| method_info methods[methods_count] | 方法结构,用于描述接口或类中声明的方法 方法里的java代码,经过编译器编译成字节码指令后,存放在方法属性集合中一个名为Code的属性里 如果父类方法没有在子类中被覆盖,方法集合中就不会出现来自父类的方法信息 |
| attribute_info attributes[attributes_count] | 属性集合:不要求各个属性具有严格的顺序,并且只要不与已有属性名重复,可以自定义属性信息 Code属性 = java程序方法体中的代码经过javac编译后,生成的字节码指令便会存储在Code属性中 ,并非所有方法的方法体都会存储在Code属性中,接口或抽象类中方法的方法体就不会被存储在Code属性中 |
理解:
解释执行:将编译好的字节码一行一行的翻译为机器码后执行;
编译执行:以方法为单位,将字节码一次性翻译为机器码后执行。
- 概念:
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、准备、解析和初始化,最终形成可以被虚拟机直接使用的java类型。
具体过程包括7个:加载、验证、准备、解析、初始化、使用、卸载。 - 图示:
- 解释:
| 名称 | 说明 |
|---|---|
| 加载 | 读取一个class文件,将其转化为某种静态数据结构而存储在方法区中,并在堆中生成一个便于用户调用的java对象 |
| 验证 | 通过类加载不一定就说明JVM认可这个类,还要进行词义、语法、语义校验 |
| 准备 | 为静态变量赋初值,是JVM默认的初始值,是固定的,不是写代码中的初始值 |
| 解析 | 将符号引用替换为直接引用 符号引用 = 假设A引用B,加载阶段就是静态解析,此时B还没有在JVM内存中,这时A的引用只是代表B的符号 直接引用 = 类A在解析阶段发现引用了B,但是B还没有被加载,则直接触发B加载,之后A对B的引用就从引用B的符号变成引用B的实际地址 动态解析:本类的生命周期部分引出了后期绑定这个概念,也就是代码使用了多态; B是一个抽象类或接口,A不知道具体用哪个来替代,只能等到实际发生调用时再进行实际地址的替换 |
| 初始化 | 类加载最后阶段,若该类具有超类,则对其超类进行初始化,执行静态初始化成员方法/成员变量 |
扩展
- IR = 中间表达形式(Intermediate Representation)
在编译原理中,通常把编译器分为前端和后端,前端编译经过词法分析、语法分析、语义分析生成中间表达形式,后端会对IR进行优化,生成目标代码。
现代编译器一般采用图结构的IR,静态单赋值(Static Single Assignment,SSA)IR是目前比较常用的一种,特点是每个变量只能被赋值一次,而且只有当前变量被赋值之后才能使用。
加载
-
概念:
读取一个class文件,将其转化为某种静态数据结构而存储在方法区中,并在堆中生成一个便于用户调用的java对象。 -
工作:
①通过全限定类型获取定义此类的二进制字节流;
②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
③内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问接口。
验证 -
作用:
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。 -
验证大致分为四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
| 验证类型 | 说明 |
|---|---|
| 文件格式验证 | 验证字节流是否符合class文件的规范,并且能被当前版本的虚拟机处理 后续阶段都是基于方法区的存储结构进行的,不会再直接操作字节流 |
| 元数据验证 | 对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求 |
| 字节码验证 | 进行数据流和控制流分析,保证被验证类的方法在运行时不会做出危害虚拟机安全的行为 |
| 符号引用验证 | 发生在虚拟机将符号引用转化为直接引用的时候,目的在于验证解析动作能正常执行 |
准备
- 作用:
为类变量分配内存并设置类变量(被static修饰)初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。 - 解释:
类加载 和 实例化区别
| 区别 | 类加载 | 实例化 |
|---|---|---|
| 变量内存分配 | 类变量分配(被static修饰) 分配在方法区中 | 实例变量 随对象分配在java堆中 |
| 执行先后顺序 | 类加载在实例化之前 | 类加载在实例化之前 |
| 执行次数 | 执行一次 | 可执行多次 |
解析
- 概念:
虚拟机将常量池内的符号引用替换成直接引用的过程。
初始化
- 概念:
准备阶段,变量已经赋值过一次系统要求的初始值了;
初始化阶段,根据程序员指定的主观计划去初始化类变量和其他资源 = 执行类构造器方法的过程。
类加载器ClassLoader
将.class文件转化为Class对象。
类Class对象只有被加载到JVM中才能运行,JVM会编译生成字节码.class文件加载到内存中,组织成一个完整的java应用程序。
三个类加载器
| 类加载器 | 作用 |
|---|---|
| 引导类加载器 BootStrap | 加载java核心库,用原生代码实现,不继承自java.lang.ClassLoader |
| 扩展类加载器 ExtClassLoader | 加载java的扩展库 |
| 应用类加载器 AppClassLoader | 根据java应用的类路径(CLASSPATH)来加载java类 java应用的类都是由它来完成加载的 获取 = 类名.class.getClassLoader() |
类加载器的机制是:全盘负责委托机制。
5 类与类加载器的关系?类加载器负责加载所有的类,并为所有被载入内存中的类生成一个java.lang.Class实例对象
解释:
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。
每个类加载器,都拥有一个独立的类名称空间。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。即使两个类来自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这个两个类必定不相等。
java中,一个类用其全限定类名作为标识;在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
概述:
-
概念:
双亲委派模型 = parent-delegation model ,该模型用于组织类和类加载器之间的关系,因为对于任何一个类而言,都需要这个类本身和加载它的类加载器一同来确定其在JVM中的唯一性。 -
作用:
类伴随它的类加载器一起具备了一种带有优先级的层次关系,确保了在各种环境的加载顺序;
保证了运行的安全性,防止不可信类扮演可信的类。 -
理解:
类java.lang.object由BootstrapClassLoader加载,双亲委派模型确保任何类加载器收到的都是对java.lang.object的加载请求,最终通过双亲委派模型把它委派给BootstrapClassLoader进行加载,保证了Object类在程序的各个加载器环境中都是同一个类。
双亲委派机制下,一个全限定的类只会被一个类加载器加载并解析使用。这样可以保证唯一性且不会产生歧义。 -
注意:
类在JVM中加载的规则,任何一个类只需要在JVM中加载一次,不需要重复加载。对象有n个,但是对象的模板只需要有一个,这样节省了内存空间。 -
弊端:
不能向下委派;
不能不委派; -
图示:
- 依次从CustomClassLoader(用户自定义类加载器)、AppClassLoader(系统类加载器)、ExtClassLoader(标准扩展类加载器)、BootstrapClassLoader(启动类加载器)查看是否加载过该类
加载一个类的时候,首先从自定义类加载器开始查找,看它有没有加载过这个类,加载过直接返回,没有加载过就看AppClassLoader;加载过就直接返回,没有则继续委派给ExtClassLoader;加载过就直接返回,没有就委派给BootstrapClassLoader;加载过就直接返回,没有就会逆向加载。 - 如果都没有则逆向加载
从BootstrapClassLoader、ExtClassLoader、AppClassLoader、自定义ClassLoader 依次加载该类,如果都没有加载到,则抛出ClassNotFoundException。
假设有人想替换系统级别的String类,编写恶意代码危害系统安全。由于要想加载修改的String类就必须使用自定义的类加载器去加载。
在双亲委派机制下,如果该类没有被加载到JVM中,会首先通过BootsClassLoader(顶级类加载器)中加载;BootsClassLoader将String类加载之后,自定义的String类就不会被再加载,因为其他类加载器没有机会去加载String类。这样在一定程度上防止了危险代码的植入。
实现双亲委派机制的代码都集中在java.lang.ClassLoader的loadClass()方法中。
方法的实现逻辑:
先检查是否已经被加载过;
若没有则调用父类加载器的loadClass()方法;
若父类加载器为空则默认使用启动类加载器(BootsClassLoader)作为父类加载器;
如果父类加载失败,则抛出ClassNotFoundException异常后,然后在调用自己的findClass()方法进行加载。
源码:
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//1 首先检查类是否被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2 没有则调用父类加载器的loadClass()方法;
c = parent.loadClass(name, false);
} else {
//3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
//4 若父类加载失败,抛出ClassNotFoundException 异常后
c = findClass(name);
}
}
if (resolve) {
//5 再调用自己的findClass() 方法。
resolveClass(c);
}
return c;
}
7 怎么实现一个自定义的类加载器?需要注意什么?
自定义类加载器 所需步骤:
一、自定义类加载器。
1 创建类 继承自 ClassLoader
public class 类名 extends ClassLoader{}
2 重写findClass方法,在findClass里获取类的字节码,并调用ClassLoader中的defineClass方法来加载类,获取Class对象
public class 类名 extends ClassLoader{
@Override
protected Class> findClass(String name) throws ClassNotFoundException{
// 获取字节码文件
}
// 使用defineClass方法来加载类。
return this.defineClass(data,0,data.length);
}
二、测试类
1 创建类加载器对象
MyClassLoader myclassloader = new MyClassLoader();
2 调用loadClass方法实现类的加载
Class> clazz = myclassloader.loadclass(参数);
3 通过调用newInstance创建实例
Object obj = clazz.newInstance();
4 使用反射机制获得类的方法
Method method = clazz.getMethod("类的方法名字符串");
5 使用反射调用方法
method.invoke(obj);
案例:
文件介绍:3个文件
1 MyClassLoad 自定义的类加载器,继承自ClassLoader 2 Demo 自定义类,即MyClassLoader类加载器加载Demo类 3 测试类,进行类加载并且通过反射实现方法。
- 自定义类加载器
package com.javaface5.test518;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
// 自定义类加载器
public class MyClassLoader extends ClassLoader{
@Override
// 重写findClass对象,用于获取类的字节码文件 字节码文件 通过.java文件编译得到。
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] data=null;
try {
// loadByte自定义方法,加载类的字节码文件
data= loadByte(name);
} catch (IOException e) {
e.printStackTrace();
}
// defineClass加载类
return this.defineClass(data,0,data.length);
}
private byte[] loadByte(String name) throws IOException {
// 路径是 类的字节码文件存放的路径 \build\classes\production 路径中。
File file = new File("****省略\build\classes\production" +
"\idea_face\com\javaface5\test518\"+name);
// 将文件内容读入到字节流中
FileInputStream fi = new FileInputStream(file);
// 返回从此输入流可以读取的剩余字节数的估计值
int len = fi.available();
byte[] b = new byte[len];
// 从该输入流读取一个字节的数据
fi.read(b);
return b;
}
}
- 被加载的类
package com.javaface5.test518;
public class Demo {
public void say(){
System.out.println("hello");
}
}
- 测试类
package com.javaface5.test518;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class testMain {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException,
IllegalAccessException, ClassNotFoundException, InstantiationException {
// 创建自定义的类加载器对象
MyClassLoader classLoader = new MyClassLoader();
// 调用loadClass方法加载类
Class> clazz = classLoader.loadClass("Demo.class");
// 通过newInstance方法创建对象
Object o=clazz.newInstance();
// 通过getMethod方法获得对象的方法
Method method = clazz.getMethod("say");
// 通过invoke方法实现对象调用方法,反射实现
method.invoke(o);
}
}
8 怎么打破双亲委派模型?
自定义类加载器重写LoadClass方法;
主要重写LoadClass方法,因为双亲委派机制是通过该方法实现的,先判断该类是否被加载,没有则找父类类加载加载,如果父类无法加载则再由自己加载,源码里会直接找到根加载器也就是启动类加载器(BootsClassLoader)。
- 所需步骤:
1 自定义一个类加载器,需要继承自ClassLoader
public class 类加载器类 extends ClassLoader{}
2 重写loadClass()方法
@Override
public Class> loadClass() throws ClassNotFoundException{}
3 重写findClass()方法
@Overrride
protected Class> findClass() throws ClassNotFoundException{}
案例
代码报错。
com.javaface5.test520.test3;
扩展
- 方法介绍
| 所属类 | 方法名 | 说明 |
|---|---|---|
| FileInputStream | FileChannel getChannel() | 返回与此文件输入流相关联的唯一的FileChannel对象 |
| ByteArrayOutputStream | ByteArrayOutputStream() | 创建一个新的字节数组输出流 |
| ByteArrayOutputStream | byte[] toByteArray() | 创建一个新分配的字节数组 |
| Channels | static WritebleByteChannel newChannel(OutputStream out) | 构造一个向给定流写入字节的通道 |
| FileBuffer | static ByteBuffer allocate(int capacity) | 分配一个新的字节缓冲区 |
| FileBuffer | ByteBuffer flip() | 翻转这个缓冲区 |
| FileBuffer | ByteBuffer clear() | 清除此缓冲区 |
| WritableByteBuffer | int write(ByteBuffer src) | 从给定的缓冲区向该通道写入一个字节序列 |
| FileChannel | abstract int read(ByteBuffer[] dst) | 从该通道读取到给定缓冲区的字节序列 |
| System | static long nanoTime() | 以纳秒为单位返回正在运行的Java虚拟机的高分辨率时间源的当前值 |
- 类介绍
| 类名 | 说明 |
|---|---|
| ByteArrayOutputStream | 将数据写入字节数组的输出流 关闭ByteArrayOutputStream没有任何效果 |
| Channels | public final class Channels extends Object 通道和流的使用方法,该类定义了java.io包的流类与此包的通道类的互操作的静态方法 |
| ByteBuffer | public abstract class ByteBuffer extends Buffer implements comparable< ByteBuffer > 一个字节缓冲区 |
| WritableByteChannel | public interface WritableByteChannel extends Channel 一个可以写字节的通道 |
| FileChannel | public abstract class FileChannel extends AbstractInterruptedChannel implements… 用于读取、写入、映射和操作文件的通道 |
概述
SPI = Service Provider Interface 服务提供接口,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。
作用是:为这些被扩展的API寻找服务实现。
使用场景
API(Application Programming Interface),大多数情况下都是由实现方指定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现;从使用人员上来讲,API直接被应用开发人员使用。
SPI(Service Provider Interface),是调用方来指定接口规范,提供给外部来实现,调用方在调用时刻选择自己需要的外部实现;从使用人员来说,SPI被框架扩展人员使用。
案例
文件介绍:四个文件 在项目idea_code中
1 定义接口文件
package xxxinterface;
public interface 接口名{
// 抽象方法
public String method();
}
2 定义两个接口的实现类
package xxximpl;
public class 实现类名 implements 接口名{}
3 在resources目录下再创建META-INF/services目录,创建txt文件,名称是接口的全路径类名package xxinterface 中的xxinterface + 接口名
文件内容是:xxximpl.实现类名
4 测试类
ServiceLoader helloSpis = ServiceLoader.load(接口.class);
// ServiceLoader.load(Class service)使用当前线程的context class loader 为给定的服务器类型创建一个新的服务加载器。
接口
package test;
public interface HelloSpi {
public String getName();
}
实现类
package test;
public class HelloSpi0 implements HelloSpi {
@Override
public String getName() {
return "HelloSpiImpl1";
}
}
public class HelloSpiImpl2 implements HelloSpi {
@Override
public String getName() {
return "HelloSpiImpl2";
}
}
resource目录下内容
test.HelloSpi0 test.HelloSpiImpl2
测试类
package test;
import java.util.ServiceLoader;
public class SPITest {
public static void main(String[] args) {
ServiceLoader helloSpis = ServiceLoader.load(HelloSpi.class);
for(HelloSpi hello:helloSpis){
System.out.println(hello.getName());
}
}
}
重点
最核心的部分是:通过ServiceLoader这个类去加载接口的具体实现类。
而不是之前的通过创建具体实现类的对象,而去调用实现类的方法。
SPI可以让父类委托子类去加载不在自己目录下的类,这样打破了双亲委派机制。(不理解xhj)
2022/5/20 :ServiceLoader.load 为给定的服务类型创建了一个新的服务加载器,是ServiceLoader类的。
ServiceLoader
| 方法名 | 参数 |
|---|---|
| static < S > ServiceLoader < S > load(Class < S > service) | 使用当前线程的context class loader 为给定的服务类型创建一个新的服务加载器 |
- 概念:
Tomcat服务器是一个免费的开放源代码的Web应用的轻量级服务器。 - 打破双亲委派的介绍:
初学部署项目,会把war包放在tomcat的webapp下,意味着一个tomcat可以运行多个web应用程序
假设两个web应用程序,都有相同的类User,且类全限定名一样,为com.test.User,但具体实现不一样;
Tomcat可以保证两个类不冲突,通过给每个Web应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了LoadClass方法,优先加载当前应用程序目录下的类,如找不到才会一层层往上查找;
这就做到Web应用层级的隔离; - 总结:
| 问题 | 答案 |
|---|---|
| Tomcat打破双亲委派机制的原因 | 保证在一个tomcat上运行的多个web应用程序之间存在相同类(全限定类名一样)但具体操作不一样的类 不冲突; |
| 具体操作 | WebAppClassLoader,为每个Web应用程序创建类加载器实例,且重写LoadClass方法。 |
- 概述:
JNDI服务 = java Naming and Directory Interface,java命名和目录接口,是SUN公司提供的一种标准的Java命名系统接口。为开发人员提供了查找和访问各种命名和目录服务的通道、统一的接口,类似JDBC都是构建在抽象层上。 - 打破双亲委派机制的介绍:
JNDI是java的标准服务,它的代码由启动类加载器去加载。但是JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(API);
为了解决该问题,引入一个线程上下文类加载器,可以通过java.lang.Thread类的setContextClassLoader()方法设置;
如果创建线程的时候没有设置,线程上下文类加载器将会从父线程中继承;
如果应用程序在全局范围内都没有设置过,那么这个类加载器默认就是AppClassLoader。
有两个线程上下文类加载器,可以去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载动作,也就打破了双亲委派模型的层次结构来逆向使用类加载器的方式。
- 概念:
Spring框架是一个开放源代码的J2EE应用程序框架。
J2EE = java 2 Platform Enterprise Edition ,本质是一个分布式的服务器应用程序设计环境(java环境)。提供了基于组件的方式来设计、开发、组装和部署企业应用。 - 打破双亲委派机制介绍:
Spring对用户程序进行组织和管理,应用程序一般存放在WEB-INF目录下,由WebAppClassLoader类加载器加载;
而Spring由Common类加载器或Shared类加载器加载;Spring如何加载WEB-INF下的应用程序呢?
使用线程上下文类加载器。
Spring加载类所用的classLoader都是通过Thread.currenThread().getContextClassLoader()获取。
通过setContextClassLoader(AppClassLoader) 来加载应用程序。每个类创建时都会默认创建一个AppClassLoader类加载器。
前端编译器在编译期的优化过程占重要地位,java中的及时编译器在运行期间的优化过程占重要地位。
编译期优化 概述- java语言的编译期就是一段不确定的操作过程:
它可能指前端编译器把java文件转变成class字节码文件的过程,也可以指虚拟机后端运行期间编译器(JIT,Just In Time Compiler)把字节码编程机器码的过程。 - 它可以分为三类编译过程:
前端编译:把.java文件转变为.class文件
后端编译:把字节码(.class文件)转变为机器码
静态提前编译:直接把.java文件编译成本地机器代码
图示:
解析分为词法分析、语法分析两个步骤
- 词法分析:
将源代码的字符流转变为标记集合(token);
单个字符是程序编写过程的最小元素,标记则是编译过程的最小元素; - 语法分析:
根据token序列(标记集合)构造抽象语法树的过程。
抽象语法树(AST):是一种用来描述程序代码语法结构的树形表示方式,语法树中的每个节点都代表着程序代码中的语法结构。
填充符号表
enterTree()方法实现。
符号表:是由一组符号地址和符号信息构成的表格,符号表中登记的信息在编译的不同阶段都要用到。
语义分析中,符号表所登记的内容将用于语义检查和产生中间代码;
在目标代码产生阶段,当对符号进行地址分配时,符号表是地址分配的依据。
enterTree():
实现抽象语法树的填充,主要为节点填充symbol和type,生成env;主要针对根节点、class节点、class的成员节点。
填充过程两个阶段:
①classEnter(tree,null):遍历根节点与class节点,填充package和类自身,以及类的参数类型使用的范围;
②.1 clazz.complete() 进一步完成的类的env进入到umcompleted中;
②.2 finish(toFlnish),生成属性的varSymbol和typeSymbol放入当前类的env中。
env = 抽象语法树中当前节点的上下文环境信息。
插入式注解处理器可以看做是一组编译器的插件,可以读取、修改、添加抽象语法树中的任意元素;
如果这些插件对抽象语法树进行修改,那么编译器会回到解析与填充符号表的过程重新处理,直到插入式注解处理器没有再对抽象语法树进行修改。
语义分析
语法分析之后,编译器获得程序代码的抽象语法树表示,语法树表示结构正确的源程序的抽象,但无法保证源程序是否符合逻辑,语义分析主要用于对结构上正确的源程序进行上下文有关性质的检查。
语义分析由三个部分构成:
| 部分 | 操作/作用 |
|---|---|
| 标注检查 | 针对变量使用前是否已被声明、变量与赋值之前的数据类型是否能够匹配、常量折叠 |
| 数据及控制流分析 | 针对程序上下文逻辑更进一步的验证,可以检查出程序局部变量在使用前是否被赋值、方法的每条路径是否有返回值、是否所有的受检异常都被处理 |
| 解语法糖 | java的泛型、变长参数、自动拆箱与装箱、条件编译等都属于语法糖 。它们在编译阶段被还原成简单的语法结构(比如:List 和 List 在运行期间其实是同一个类) 语法糖指在计算机语言中添加某种语法,对语言的功能并没有影响,但更方便程序员使用; |
语法糖
- 泛型与类型擦除
java语言中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经被替换成原生类型,并且在相应的地方插入了强制转换。
程序源码
public static void main(String[] args){
Map map = new HashMap();
map.put("hello","你好");
System.out.println(map.get("hello"));
}
泛型擦除后的代码
public static void main(String[] args){
Map map = new HashMap(); // 泛型擦除,替换成原生类型。
map.put("hello","你好");
System.out.println(map.get("hello")); // 插入强制转换。
}
- 变长参数
会变成数组类型的参数。 - 自动拆箱与装箱
自动拆箱与装箱在编译之后会被转成对应的包装和还原方法,如 Integer.intValue() 与 Integer.valueOf(参数int) - 遍历循环
会变成迭代器的实现 - 条件编译
java语言也可以进行条件编译,方法就是使用if语句,它在编译阶段被“运行”;
java的语法糖只能是条件为常量的if语句,根据布尔值的真假,编译器会把分支中不成立的代码块消除掉。
public static void main(String[] args) {
if(true){
System.out.println("block 1");
}
else{
System.out.println("block 2");
}
}
假设结果为true,编译器会将假的值去掉,即分支中不成立的代码块去掉。
public static void main(String[] args) {
System.out.println("block 1");
}
字节码生成
javac编译的最后一个阶段,字节码生成阶段将之前各个步骤所生成的信息转化成字节码写到磁盘中,另外进行少量的代码添加和转换工作。
java最初是通过解释器(interpreter)进行解释执行的。当虚拟机发现某方法或代码块的运行很频繁时,会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,运行时,虚拟机会将这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler)
解释器与即时编译器 区别
| 区别 | 解释器 | 即时编译器 |
|---|---|---|
| 作用 | 将字节码解释为机器码,下次遇到相同的字节码,还会进行重复解释 | 字节码解释为机器码,存入Code Cache,下次遇到相同的字节码,不再编译直接编译 |
| 特点 | 将字节码解释为所有平台通用的机器码 | 根据平台类型,生成平台特定的机器码 |
xhj理解: 也就是说 运行期优化 说的是解释器,即及时编译期(JIT,Just In Time Compiler)。
热点代码探测 优化手段 逃逸分析代码:
package com.javaface5.test519.test2;
public class Test1 {
// 逃逸分析
public static void main(String[] args) {
// 循环200次
for (int i = 0; i < 200; i++) {
// 创建1000个对象
// 获取当前系统的时间,以纳秒单位
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object(); // 循环创建对象
}
long end = System.nanoTime();
System.out.printf("%dt%dn", i, (end - start));
}
}
}
结果分析:
0 66800 1 48600 2 53900 3 39900 ----------------------- 47 63900 48 67600 49 51400 50 51900 51 45300 52 54900 ------------------- 195 23300 196 24100 197 23500 198 29300 199 27800 // 相同的代码,后续执行时间越来越短。
解释:
由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间更长;
想要编译出优化程度更高的代码,解释器还可能替编译器收集性能监控信息,对解释器执行的速度也有影响。
这也就是为什么前期运行时间长的原因。
为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启动**分层编译(Tiered Compilation)**的策略。
JVM将执行状态分成5个层次:
| 层次 | 说明 |
|---|---|
| 0层 | 解释器执行(Interpreter),将字节码解释为机器码 |
| 1层 | 使用C1 即时编译器编译执行(不带profiling) |
| 2层 | 使用C1 即时编译器编译执行(带基本的profiling) |
| 3层 | 使用C1 即时编译器编译执行(带完全的profiling) |
| 4层 | 使用C2 即时编译器编译执行 |
profiling = 在运行过程中收集一些程序执行状态的数据,即信息统计工作(如:方法的调用次数、循环的回边次数等)。
执行效率上 解释器 < C1(提升5倍左右) < C2 (提升10-100倍)
概念:
发现方法是热点方法,且长度不长,会进行内联 = 把方法代码拷贝、粘贴到调用者的位置。
作用:
去除方法调用的成本,同时为其他优化建立了良好的基础;
各种编译器一般会把内联优化放在优化序列的最靠前位置;
代码:
package com.javaface5.test519.test2;
public class Test2 {
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining // 查看 JVM 对代码的内联情况
// -XX:CompileCommand=dontinline,*T02_RunTime_Inlining.square // 禁用内联
// -XX:+PrintCompilation
public static void main(String[] args) {
int x = 0;
for (int i = 0; i < 500; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
x = square(9);
}
long end = System.nanoTime();
System.out.printf("%dt%dt%dn", i, x, (end - start));
// 输出内容是:最外层遍历次数i 内存计算平方结果值x 最后是遍历一次的执行时间 time
}
}
private static int square(final int i) {
return i * i;
}
}
结果分析:
0 81 225900
1 81 52200
2 81 29600
-------------------
245 81 100
246 81 100
247 81 0
248 81 0
249 81 0
250 81 100
// 输出结果:System.out.printf("%dt%dt%dn", i, x, (end - start));
// 输出内容是:最外层遍历次数i 内存计算平方结果值x 最后是遍历一次的执行时间 time
常量折叠的优化:
即编译期就把一些表达式计算好,不需要在运行时进行计算。
也就说:编译期常量加减乘除的运行过程会被折叠
1 System.out.println(square(9)); 2 System.out.println(81); // 就是编译期常量加减乘除的运行过程会被折叠 String s1 = "1" + "2" ; // 字符串字面量相加 编译器会给你优化成 String s1 = "12"; String a = "a"; String bc = "bc"; String s1 = "a" + "bc" ; ⇒ String s1 = "abc"; 编译器优化 的常量折叠 String s2 = a + bc; ⇒ 两个非final的变来个相加,不会进行常量折叠。 被final修饰的字符串也是编译期的常量。反射优化
案例:
package com.javaface5.test520;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test2 {
public static void foo(){
System.out.println("foo....");
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException,
IllegalAccessException, IOException {
Method method = test2.class.getMethod("foo");
// 前16次调用效率较低,第17次调用效率较高
// 因为从第17次开始,已经不是反射来调用,而是采用 类名.静态方法名 来调用了。
// JVM虚拟机已经将我们的反射方法调用转换成 静态方法调用。
for(int i = 0;i < 16 ; i++){
// 按照指定格式字符串和参数将格式化的字符串写入此输出流
System.out.printf("%dt",i);
method.invoke(null);
}
System.in.read();
}
}
图示:
PrintStream 和 InputStream
PrintStream 标准字节输出流 System.out属性 返回PrintStream对象
InputStream 字节输入流 System.in属性 返回InputStream对象
方法介绍:
| 所属类 | 方法名 | 说明 |
|---|---|---|
| PrintStream | printf(String format,Object…args) | 使用指定的格式字符串和参数将格式化的字符串写入此输出流 |
| InputStream | abstract int read() | 从输入流读取数据的下一个字节 |
参考
11 为何HotSpot虚拟机要使用解释器与编译器并存的框架?HotSpot采用解释器与编译器并存的框架,是因为两者皆有优势。
- 解释器:
逐条转换,保留源代码;
程序员可以快速启动和执行,消耗内存小;(成本低,后期效率低) - 编译器:
一次性转换,不保留源代码;
随着代码频繁执行会将代码编译成本地机器码;(成本高、后期效率高)
解释器与编译器配合使用:
当程序需要迅速启动和执行的时候,解释器可以率先发挥作用,省去编译时间,立即执行;
程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可以提高执行效率;
当程序运行环境中内存资源限制较大,可以使用解释器执行节约内存,反之可以使用编译执行提升效率。
HotSpot
概述:它是一款及时编译器执行引擎,是目前使用范围最广的java虚拟机;
关于主内存与工作内存(线程的本地内存)之间的具体交互协议,即一个变量如何从主内存拷贝到本地内存、如何从本地内存同步回主内存的实施细节。
java内存模型定义了8种操作,每一种都是原子的,不可再分的。
| 操作名 | 作用于 | 说明 |
|---|---|---|
| lock(锁定) | 主内存变量 | 把一个变量标识为一条线程独占的状态 |
| unlock(解锁) | 主内存变量 | 把一个处于锁定状态的变量释放,释放后的变量才能被其他线程所访问 |
| read(读取) | 主内存变量 | 把一个变量的值从主内存传输到本地内存中,以便load操作使用 |
| load(载入) | 本地内存变量 | 把read操作从主内存中得到的变量值放入本地内存的变量副本中 |
| use(使用) | 本地内存变量 | 把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时,就执行执行这个操作 |
| assgin(赋值) | 本地内存变量 | 把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |
| store(存储) | 本地内存变量 | 把工作内存中的一个变量的值传送到主内存中,以便随后的write操作使用 |
| write(写入) | 主内存变量 | 把store操作从工作内存中得到的变量的值放入主内存的变量中 |
需要满足的规则
- 不允许read和load、store和write操作之一单独出现,且必须保证操作执行顺序;
且变量从主内存到工作内存 = read 和 load、变量从工作内存到主内存 = store 和 write
表示不允许一个变量从主内存读取但是工作内存不接受,从工作内存发起写会但是主内存不接受 - 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变之后必须把该变化同步回主内存中;
- 不允许一个线程无原因的(没有发生任何assign操作)把数据从线程的工作内存同步回主内存中;
即 数据从工作内存 - 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量 = 在对一个变量实施use和store操作之前,必须执行assign和load操作;(xhj:本地内存的值是别人给的,不是自己的)
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行后,只有执行相同次数的unlock,变量才会被解锁;
- 如果一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新load(主内存给本地)或assign(执行引擎给本地)操作初始化变量的值;
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量;
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作);



