一.类文件结构
1.1 基础
Java规范分为 Java语言规范 与 Java虚拟机规范
Java 虚拟机 不和 包括 Java在内的任何语言绑定,它只与calss文件
这种特定的二进制文件格式关联,虚拟机不关心Class的来源是何种语言 如图:
1.2 Class类文件的结构
- 描述:Class文件是一组以8位字节为基础单位二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符。
- Class文件格式采用一种类似C语言结构体的伪结构来存储数据,包含两种数据类型:无符号数和表
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8分别表示1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
- 表:由多个无符号数和其他表作为数据项构成的复合类型的数据类型,所有的表都习惯地以"_info"结尾,表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。,它由下表所示的数据项构成:
- 魔数和Class文件的版本
- 每个Class文件的头四个字节称为魔数,它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准都是使用魔数而不是扩展名来进行识别,主要是基于安全方面考虑。紧接着魔数的四个字节存储的是Class文件的版本号:第五和第六是此版本号,第七和第八是主版本号.
-
JDK1.1 版本号为 45~45.65535 JDK1.2支持 45~46.65535 最新版JDK1.8 支持 52 看下 1.8之下的编译结果:
虚拟机属性 ()
参考 【深入Java虚拟机】之二:Class类文件结构
二、类加载的过程
- class装载验证流程
- 加载
- 链接
- 验证
- 准备
- 解析
- 初始化
- 对于初始化阶段,虚拟机严格规定有且只有5种情况必须立即对类进行初始化
- 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
eg:使用new关键字实例化对象的时候;读取或者设置一个类的静态字段;调用一个类的静态方法时等 - 使用java.lang.reflect包对类进行反射调用的时候
- 当初始化一个类的时候,如果发现其父类还没有初始化,则要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类时
- 当使用JDK 1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结构REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
- 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
这5种场景中的行为称为对一个类进行主动引用
下面看被动 引用
/**
* 被动使用类字段演示一:
* 通过子类引用父类的静态字段,不会导致子类初始化
**/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
out:
SuperClass init!
123
- 对于静态字段,只有直接定义这个字段的类才会被初始化,因为通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。(HotSpot 虚拟机 可以设置 -XX:+TraceClassLoading 使子类 加载)
被动引用例子
通过子类引用父类的静态字段,不会导致子类初始化
通过数组定义来引用类,不会触发此类的初始化
常量在编译阶段会存入调用类的常量池中,本质中并没有直接引用定义常量的类,因此不会触发定义常量的类的初始化。
加载
虚拟机需要完成三件事情
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(方法区里用来存储虚拟机加载的类信息)
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象将作为程序访问方法区中的这些类型数据的外部接口
连接-> 验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成下面4个阶段的检验动作
一.文件格式验证:是否以魔数0xCAFEBABE开头,主次版本是否在处理机处理范围内等
二.元数据验证:对字节码描述的信息进行语义分析,以保证描述的信息符合JAVA语言规范的要求,比如是否有父类等
三.字节码验证:这个阶段是最复杂的一个阶段,主要 目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。在第二个阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
四.符号引用验证:最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转换动作将在连接第三阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外的信息进行匹配性校验,比如校验 符号引用中通过字符串描述的全限定名是否能找到对应的类;符号引用中的类,字段,方法的访问性(public ,private..)是否可以被当前访问等。符号引用验证的目的是确保解析动作能正常执行
连接->准备
- 准备阶段是正式为类变量 分配内存 并且设置 类变量初始值 的阶段,这些变量所使用的内存都将在方法区中进行分配。
这里分配内存仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
其次这里所说的初始值是通常情况下的数据类型零值//假设一个类变量定位为 public static int value = 123;
那么变量valuew在准备阶段过后初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器< clinit >()方法中所以把value赋值为123是在初始化阶段才会执行。
- public static final int v=1; final;类型 在准备就会赋值 final 为常量
连接->解析
- 符号引用替换为直接引用
初始化
- static变量 赋值语句
- static{}语句
- 子类的
调用前保证父类的 被调用
是线程安全的
例子:
public class Test {
static {
i = 0; // 给变量复制可以正常编译通过
// System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
static class DeadLoopClass {
static {
// 如果不加上这个if语句,编译器将提示“Initializer does not complete normally”并拒绝编译
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
out:
Thread[Thread-0,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass
Thread[Thread-1,5,main]start
如果一个类的
类装载器ClassLoader
- ClassLoader是一个抽象类
- ClassLoader的实例将读入Java字节码将类装载到JVM中
- ClassLoader可以定制,满足不同的字节码流获取方式
- ClassLoader负责类装载过程中的加载阶段
ClassLoader的重要方法
- public Class loadClass(String name) throws ClassNotFoundException
载入并返回一个Class - protected final Class defineClass(byte[] b, int off, int len)
定义一个类,不公开调用 - protected Class findClass(String name) throws ClassNotFoundException
loadClass回调该方法,自定义ClassLoader的推荐做法 - protected final Class findLoadedClass(String name)
寻找已经加载的类
比较两个类是否相等,只有在两个类是同一个类加载前提下才有意义,否则即使两个类来自同一个Class文件被同一虚拟机加载,只要加载的类加载其不同两个类就不相等。(这里的相等包括 equals isInstance() instanceof )
例子:
package com.gx.jvm.jvmbook;
/**
* Created by gx
* Date: 2017-09-09
* Time: 16:50
*/
import java.io.IOException;
import java.io.InputStream;
/**
* 类加载器与instanceof关键字演示
*
* @author zzm
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.gx.jvm.jvmbook.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.gx.jvm.jvmbook.ClassLoaderTest);
}
}
out:
class com.gx.jvm.jvmbook.ClassLoaderTest
false
上面就是破坏了双亲委托机制
我们不能重写loadclass 要覆盖 findclass
-
JDK中ClassLoader默认设计模式 – 分类
- BootStrap ClassLoader (启动ClassLoader)
- Extension ClassLoader (扩展ClassLoader)
- App ClassLoader (应用ClassLoader/系统ClassLoader)
- Custom ClassLoader(自定义ClassLoader)
每个ClassLoader都有一个Parent作为父亲(BootStrap 没有 parent)
破坏双亲委派模型
上文中提到过双亲委派模型并不是一个强制性的约束模型,而是Java设计者们推荐给开发者们的类加载器的实现方式。到目前为止,主要主要出现过三次较大规模的“破坏”情况。
-
双亲委派模型之前
第一次破坏发生在双亲委派模型之前,为了兼容以前的代码在这之后的ClassLoader增加了一个新方法findClass(),在此之前用户只通过loadClass()实现自定义类加载器。在JDK 1.2之后,已经不再提倡采用覆盖loadClass(),而应当把自己的类加载逻辑写到findClass()方法完成加载,这样可以保证新写出来的类加载器是符合双亲委派模型的。
线程上下文加载器
双亲委派模型本身是存在着缺陷的,无法解决基础类调用回用户代码的情况。很典型的例子就是JNDI服务,它的代码由引导类加载器去加载,但JNDI的目的就是对资源进行管理和查找,它需要调用由独立厂商实现并部署在应用程序CLASSPATH下的JNDI接口提供者(SPI)的代码。
在Java中采用线程上下文加载器解决这一问题,如果不进行额外的设置,那么线程上下文加载器就是系统上下文加载器。在SPI接口是使用线程上下文加载器,就可以成功加载到SPI实现的类。
当然,使用线程上下文加载类,也需要注意保证多个需要通信的线程间类加载器应该是同一个,防止因为类加载器示例不同而导致类型不同。
- 代码热替换、热部署
简单实现 代码热部署demo
自定义类加载器
package com.gx.jvm.hotdeployment;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashSet;
/**
* Created by gx
* Date: 2017-09-09
* Time: 17:54
*/
public class HotSwapClassLoader extends ClassLoader {
private String basedir; // 需要该类加载器直接加载的类文件的基目录
private HashSet dynaclazns; // 需要由该类加载器直接加载的类名
public HotSwapClassLoader(String basedir, String[] clazns) throws Exception {
super(null); // 指定父类加载器为 null
this.basedir = basedir;
dynaclazns = new HashSet();
loadClassByMe(clazns);
}
private void loadClassByMe(String[] clazns) throws Exception {
for (int i = 0; i < clazns.length; i++) {
loadDirectly(clazns[i]);
dynaclazns.add(clazns[i]);
}
}
private Class loadDirectly(String name) throws Exception {
Class cls = null;
StringBuffer sb = new StringBuffer(basedir);
String classname = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator + classname);
File classF = new File(sb.toString());
cls = instantiateClass(name, new FileInputStream(classF),
classF.length());
return cls;
}
private Class instantiateClass(String name, InputStream fin, long len) throws Exception {
byte[] raw = new byte[(int) len];
fin.read(raw);
fin.close();
return defineClass(name, raw, 0, raw.length);
}
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if (!this.dynaclazns.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if (cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
}
测试热部署代码
package com.gx.jvm.hotdeployment;
public class Holder {
public void sayHello() {
System.out.println("hello world! (version three)");
}
}
测试代码
package com.gx.jvm.hotdeployment;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* Created by gx
* Date: 2017-09-09
* Time: 17:53
*/
public class TestSwap {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
while (true) {
try {
HotSwapClassLoader classLoader =
new HotSwapClassLoader("E:\\works\\javastudy\\target\\classes",
new String[]{"com.gx.jvm.hotdeployment.Holder"});
Class clazz = classLoader.loadClass("com.gx.jvm.hotdeployment.Holder");
Object holder = clazz.newInstance();
Method m = holder.getClass().getMethod("sayHello", new Class[]{});
m.invoke(holder, new Object[]{});
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
}
}
参考
- 1.JVM类加载机制
- 2.《深入理解Java虚拟机》