1. 类文件结构
执行 javac -parameters -d . HellowWorld.java
编译为 HelloWorld.class 后是这个样子的:
[root\@localhost ~]# od -t xC HelloWorld.class
……
1.1 魔数
0~3 字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
1.2 版本
4~7 字节,表示类的版本 00 34(52) 表示是 Java 8
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
1.3 常量池
8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34 项,注意 #0 项不计入,也没有值
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#1 项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得 这个方法的【所属类】和【方法名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#2 项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项 来获得这个成员变量的【所属类】和【成员变量名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
1.4 访问标识与继承信息
1.5 Field 信息
1.6 Method 信息 表示方法数量
1.7 附加属性
00 01 表示附加属性数量
00 13 表示引用了常量池 #19 项,即【SourceFile】
00 00 00 02 表示此属性的长度
00 14 表示引用了常量池 #20 项,即【HelloWorld.java】
2. 字节码指令
3. 编译期处理
类加载机制
Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,静态代码块在初始化阶段完成,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的加载机制。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。其中验证、准备和解析三个部分统称为连接(Linking)
JVM 就是按照上面的顺序一步一步的将字节码文件加载到内存中并生成相应的对象的。首先将字节码加载到内存中,然后对字节码进行连接,连接阶段包括了验证准备解析这 3 个步骤,连接完毕之后再进行初始化工作。
4. 类加载阶段
类加载时机:
对于初始化阶段,虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
- 遇到 new、getstatic 和 putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。
- 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)
- 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类), 虚拟机会先初始化这个主类。
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
类加载的过程
java编译器将 .java 文件编译成扩展名为 .class 的文件。.class 文件中保存着 java 转换后,虚拟机将要执行的指令。当需要某个类的时候,java 虚拟机会加载 .class 文件,并创建对应的 class 对象,将 class 文件加载到虚拟机的内存,这个过程被称为类的加载。
(1)加载
类加载过程的一个阶段,ClassLoader 通过一个类的完全限定名查找此类字节码文件,并利用字节码文件,将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。
在加载阶段,虚拟机主要完成三件事情: ① 通过一个类的全限定名(比如 com.danny.framework.t)来获取定义该类的二进制流; ② 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构; ③ 在内存中生成一个代表这个类的 java.lang.Class 对象,作为程序访问方法区中这个类的外部接口。
(2)验证
目的在于确保 class 文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:
文件格式的验证,
元数据的验证,
字节码验证,
符号引用验证。
(3)准备
为类变量(static 修饰的字段变量)分配内存并且设置该类变量的初始值,(如 static int i = 5 这里只是将 i 赋值为 0,在初始化的阶段再把 i 赋值为 5),这里不包含 final 修饰的 static ,因为 final 在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到 Java 堆中。
(4)解析
这里主要的任务是把常量池中的符号引用替换成直接引用
(5)初始化
这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。(前面已经对 static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)
有以下几种情况进行初始化:
1.当创建某个类的新实例时(如通过 new 或者反射,克隆,反序列化等)。
2.当调用某个类的静态方法时。
3.当使用某个类或接口的静态字段时。
4.当调用 Java API 中的某些反射方法时,比如类 Class 中的方法,或者 java.lang.reflect 中的类的方法时。
5.当初始化某个子类时。
6.当虚拟机启动某个被标明为启动类的类(即包含 main 方法的那个类)。
静态代码块在初始化阶段完成。
初始化的顺序:
父类静态变量,父类静态代码块,多个按先后顺序执行;
子类静态变量,子类静态代码块,多个按先后顺序执行;
父类非静态代码
父类构造函数
子类非静态代码
子类构造函数
5. 类加载器
作用:加载 class,确定类的唯一性
把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。
将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。
目前类加载器却在类层次划分、OSGi、热部署、代码加密等领域非常重要,我们运行任何一个 Java 程序都会涉及到类加载器。
类的唯一性和类加载器:
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性。
即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。 这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。
分类及关系
类加载器分为如下几种:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)和自定义类加载器(User ClassLoader),其中启动类加载器属于 JVM 的一部分,其他类加载器都用 java 实现,并且最终都继承自 java.lang.ClassLoader。
① 启动类加载器(Bootstrap ClassLoader)是由 C/C++编译而来的,看不到源码,所以在 java.lang.ClassLoader 源码中看到的 Bootstrap ClassLoader 的定义是 native 的“private native Class findBootstrapClass(String name);”。启动类加载器主要负责加载 JAVA_HOMElib 目录或者被-Xbootclasspath 参数指定目录中的部分类,具体加载哪些类可以通过“System.getProperty(“sun.boot.class.path”)”来查看。
② 扩展类加载器(Extension ClassLoader)由 sun.misc.Launcher.ExtClassLoader 实现,负责加载 JAVA_HOMElibext 目录或者被 java.ext.dirs 系统变量指定的路径中的所有类库,可以用通过“System.getProperty(“java.ext.dirs”)”来查看具体都加载哪些类。
③ 应用程序类加载器(Application ClassLoader)由 sun.misc.Launcher.AppClassLoader 实现,负责加载用户类路径(我们通常指定的 classpath)上的类,如果程序中没有自定义类加载器,应用程序类加载器就是程序默认的类加载器。
④ 自定义类加载器(User ClassLoader),JVM 提供的类加载器只能加载指定目录的类(jar 和 class),如果我们想从其他地方甚至网络上获取 class 文件,就需要自定义类加载器来实现,自定义类加载器主要都是通过继承 ClassLoader 或者它的子类来实现,但无论是通过继承 ClassLoader 还是它的子类,最终自定义类加载器的父加载器都是应用程序类加载器,因为不管调用哪个父类加载器,创建的对象都必须最终调用 java.lang.ClassLoader.getSystemClassLoader()作为父加载器,getSystemClassLoader()方法的返回值是 sun.misc.Launcher.AppClassLoader 即应用程序类加载器。
双亲委派
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
优势:
采用双亲委派模式的好处就是 Java 类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器(ClassLoader)再加载一次。
其次是考虑到安全因素,Java 核心 API 中定义类型不会被随意替换,假设通过网路传递一个名为 java.lang.Integer 的类,通过双亲委派的的模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字类,发现该类已经被加载,并不会重新加载网络传递过来的 java.lang.Integer.而之际返回已经加载过的 Integer.class,这样便可以防止核心 API 库被随意篡改。
可能你会想,如果我们在 calsspath 路径下自定义一个名 java.lang.SingInteger?该类并不存在 java.lang 中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为 java.lang 是核心的 API 包,需要访问权限,强制加载将会报出如下异常。
java.lang.SecurityException:Prohibited package name: java.lang
破坏双亲委派模型
典型的两中方法:一种是自定义类加载器,重写 loadClass 方法,第二种是使用线程上下文类加载器
线程上下文类加载器
双亲委派模型主要出现过 3 较大规模的“被破坏”情况。
- 双亲委派模型在引入之前已经存在破坏它的代码存在了。 双亲委派模型在 JDK 1.2 之后才被引入,而类加载器和抽象类 java.lang.ClassLoader 则在 JDK 1.0 时代就已经存在,JDK 1.2 之后,其添加了一个新的 protected 方法 findClass(),在此之前,用户去继承 ClassLoader 类的唯一目的就是为了重写 loadClass() 方法,而双亲委派的具体逻辑就实现在这个方法之中,JDK 1.2 之后已不提倡用户再去覆盖 loadClass() 方法,而应当把自己的类加载逻辑写到 findClass() 方法中,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
- 基础类无法调用类加载器加载用户提供的代码。 双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),但如果基础类又要调用用户的代码,例如 JNDI 服务,JNDI 现在已经是 Java 的标准服务,它的代码由启动类加载器去加载(在 JDK 1.3 时放进去的 rt.jar ),但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface,例如 JDBC 驱动就是由 MySQL 等接口提供者提供的)的代码,但启动类加载器只能加载基础类,无法加载用户类。
为此 Java 引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread.setContextClassLoaser() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。 如此,JNDI 服务使用这个线程上下文类加载器去加载所需要的 SPI 代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
- 用户对程序动态性的追求。 代码热替换(HotSwap)、模块热部署(Hot Deployment)等,OSGi 实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。
在 OSGi 环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: 1)将以 java.* 开头的类委派给父类加载器加载。 2)否则,将委派列表名单内的类委派给父类加载器加载。 3)否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载。 4)否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载。 5)否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载。 6)否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载。 7)否则,类查找失败。 上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。OSGi 的 Bundle 类加载器之间只有规则,没有固定的委派关系。
全盘委托机制
当一个类运行时,可能有其他的类,这时由应用类加载器委托给扩展类加载器是否加载这些类,扩展类加载器再次向上委托引导类加载器是否加载这些类,引导类加载器判断后将有的类进行加载向内存中返回 class 对象后,再由扩展类加载器中有的类进行加载返回 class 对象,剩下全部有应用类加载器进行加载.
自定义类加载器
Java 默认 ClassLoader,只加载指定目录下的 class,如果需要动态加载类到内存,例如要从远程网络下来类的二进制,然后调用这个类中的方法实现我的业务逻辑,如此,就需要自定义 ClassLoader。
自定义类加载器分为两步:
- 继承 java.lang.ClassLoader
- 重写父类的 findClass() 方法
针对第 1 步,为什么要继承 ClassLoader 这个抽象类,而不继承 AppClassLoader 呢? 因为它和 ExtClassLoader 都是 Launcher 的静态内部类,其访问权限是缺省的包访问权限。 static class AppClassLoader extends URLClassLoader{…}
第 2 步,JDK 的 loadCalss() 方法在所有父类加载器无法加载的时候,会调用本身的 findClass() 方法来进行类加载,因此我们只需重写 findClass() 方法找到类的二进制数据即可。
线程上下文类加载器
如上所说,为解决基础类无法调用类加载器加载用户提供代码的问题,Java 引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器默认就是 Application 类加载器,并且可以通过 java.lang.Thread.setContextClassLoaser() 方法进行设置。
Copy// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
“Could not create application class loader” );
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
那么问题来了,我们使用 ClassLoader.getSystemClassLoader() 方法也可以获取到 Application 类加载器,使用它就可以加载用户类了呀,为什么还需要线程上下文类加载器? 其实直接使用 getSystemClassLoader() 方法获取 AppClassLoader 加载类也可以满足一些情况,但有时候我们需要使用自定义类加载器去加载某个位置的类时,例如 Tomcat 使用的线程上下文类加载器并非 AppClassLoader ,而是 Tomcat 自定义类加载器。
以 Tomcat 为例,其每个 Web 应用都有一个对应的类加载器实例,该类加载器使用代理模式,首先尝试去加载某个类,如果找不到再代理给父类加载器这与一般类加载器的顺序是相反的。 这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
更多关于 Tomcat 类加载器的知识,这里暂时先不讲了。
new 一个对象过程中发生了什么?
- 确认类元信息是否存在。当 JVM 接收到 new 指令时,首先在 metaspace 内检查需要创建的类元信息是否存在。 若不存在,那么在双亲委派模式下,使用当前类加载器以 ClassLoader + 包名+类名为 Key 进行查找对应的 class 文件。 如果没有找到文件,则抛出 ClassNotFoundException 异常 , 如果找到,则进行类加载(加载 - 验证 - 准备 - 解析 - 初始化),并生成对应的 Class 类对象。
- 分配对象内存。 首先计算对象占用空间大小,如果实例成员变量是引用变量,仅分配引用变量空间即可,即 4 个字节大小,接着在堆中划分—块内存给新对象。 在分配内存空间时,需要进行同步操作,比如采用 CAS (Compare And Swap) 失败重试、 区域加锁等方式保证分配操作的原子性。
- 设定默认值。 成员变量值都需要设定为默认值, 即各种不同形式的零值。
- 设置对象头。设置新对象的哈希码、 GC 信息、锁信息、对象所属的类元信息等。这个过程的具体设置方式取决于 JVM 实现。
- 执行 init 方法。 初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
首先尝试在栈上进行分配(效率高,对象少),如果对象很大,直接分配到老年代,否则,就先分配到 Eden 区,