一、引言
首先看一张图
Java 是编译型语言还是解释型语言?
有人说Java是编译型的。因为所有的Java代码都是要编译的,.java不经过编译就无法执行。
也有人说Java是解释型的。因为java代码编译后不能直接运行,它是解释运行在JVM上的,所以它是解释型的。
可以说Java是兼具编译型语言与解释型语言的特点的。
另外,一些常用的代码,其实会被JIT即时编译器进行编译
Java对于多种不同的操作系统有不同的JVM,所以实现了真正意义上的跨平台。
并且现在能在JVM上跑的语言已经有一百多种了
JVM与class文件格式
也就是说任何语言,只要最终是class文件,符合JVM class文件的规范,就可以跑在JVM上
JVM跟Java无关
因此,jvm是一种规范 ,在这里可以看到各个版本的JVM的规范是什么,比如Jvms13
jvm是虚拟出来的一台计算机,他有自己的CPU,字节码指令集(汇编语言),有自己的内存等
常见的JVM实现
-
hotspot
: oracle官方,我们现在用的就是这个1
2
3
4
5Steven -Pro ~ % java -version
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)
Steven@StevendeMacBook-Pro ~ % -
Jrockit
-
J9 :IBM
-
Microsoft VM
-
TaobaoVM :hotspot深度定制版
-
LiquidVm:直接针对硬件
-
azul zing:最新垃圾回收的业界标杆
二、class文件
class文件还是有些复杂的,如何学习呢,先知道她的主要结构,然后一般使用IDEA插件
jclasslib
,然后从一个空的类,加属性,加变量,加方法,等等,分别通过插件查看生成的结构来学习
class文件的格式是一个二进制的字节流,这种文件当我们使用16进制的编译器(如subline)打开,看到的就是这样的
当然也可以使用8进制、10进制、2进制打开
而这些二进制的字节流就是由Java虚拟机来解释的,那是如何解释的呢,可以通过看class文件的组成理解
具体可以参考这篇文章 Class文件结构
三、class文件结构
1、魔数:
每个class文件的头4个字节称为魔数(Magic Number),唯一作用是用于确定这个文件是不是一个Class文件(一个能被虚拟机接受的class文件)。Class文件魔数的值为0xCAFEBABE(16进制的CAFEBABE)。如果一个文件不是以0xCAFEBABE开头,那就肯定不是Class文件
2、class文件的版本号
次版本号(minor_version): 前2字节用于表示次版本号
主版本号(major_version): 后2字节用于表示主版本号。
这个的版本号是随着jdk版本的不同而表示不同的版本范围的。Java的版本号是从45开始的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。
0X0034(对应十进制的50):JDK1.8
0X0033(对应十进制的50):JDK1.7
0X0032(对应十进制的50):JDK1.6
0X0031(对应十进制的49):JDK1.5
0X0030(对应十进制的48):JDK1.4
0X002F(对应十进制的47):JDK1.3
0X002E(对应十进制的46):JDK1.2
ps:0X表示16进制
3、常量池:
长度为constant_pool_count-1的表
可以查看这个结构的方法主要有
- Java自带的
javap
,比如
- IDEA插件
jclasslib
- JBE:可以直接修改
常量池是比较复杂的,具体可以参考这篇文章 Class文件结构
3、访问标志(2字节)(access_flags)
这个标志主要用于识别一些类或接口层次的访问信息
4、类索引、父类索引和接口索引集合
这三项数据主要用于确定这个类的继承关系
5、字段表集合(fields)
6、方法表集合(method)
7、属性表集合(attribute)
四、详解Class加载过程
class文件是怎么从硬盘上到内存中,并且开始准备执行的
三步:
- Loading
讲class文件load
到内存 - Linking:
- Verfication: 校验,校验是否符合class文件的标准
- Preparation:将class文件中的静态变量赋默认值
- Resolution:把class文件中常亮池用到的符号引用。转化成内存地址,可以直接使用
- Initlalizing:静态变量赋值为初始值
首先如何加载到内存中呢?
JVM有一个类加载器的层次,这个类加载器本身就是一个普通的class,类加载器的层次加载不同的class,jvm中所有的class都是被类加载器加载到内存中的,这个类加载器就叫做ClassLoader
一个class从硬盘中被加载到内存中之后,实际上创建了两块内容,第一块是把class文件放到内存中,另一块内容是生成了一个class类的对象,这个对象指向了第一块内容
- 第一个类加载器层次
BootStrap:加载Lib里最核心的内容,比如rt.jar,charset.jar等核心,所以说当我调用getClassLoader()
得到的加载器的结果是一个空值的时候,代表这个加载器已经到达了最顶层的类加载器 - 第二个类加载器的层次
Extension:加载扩展包中的文件,这些扩展包在jdk安装目录的ext
下 - 第三个类加载器的层次
APP:这个就是平时用的类加载器,用于加载classpath指定的内容 - 第四个类加载器的层次
自定义加载器,加载自定义的加载器
CustomerClassLoader的父类加载器是>Application,他的父类加载器是> Extension,他的父类加载器是BootStrap
1 | package com.shifpeng.scaffold.service.jvm; |
⚠️注意:父加载器不是“类加载器的加载器”!!!也不是“类加载器的父类加载器”,而上面的例子是指如果找不到对应的类加载器,委托给父加载器找
五、双亲委派
任何一个class,假如自定义了classloader
,这个时候会尝试在CustomerClassLoader
中找(类加载器中维护者缓存),如果已经加载进来了,那直接返回结果,如果没有,并不是立马把这个类加载进来 ,而是委托父级ApplicationCLassloader
,看看它有没有加载这个类,如果有直接返回,如果没有,再委托给Ext,Ext有就返回,没有就再委托Bootstrap,有就返回
Bootstrap如果没有,就**返回 ** 委托Ext加载器加载这个类,如果这个加载器可以加载这个类,那么就直接加载并返回(按照上面的四层加载器分别可以加载的类别),如果不能加载该类,再向下委托给App,如果还是不能加载,继续向下委托给自定义类加载器CustomerClassLoader
,如果加载成功,即返回,如果不成功,则会抛出ClassNotFound
的异常
这个过程就叫做** 双亲委派
**
为什么要搞双亲委派机制 ?
主要是为了安全(防止篡改),次要的就是为了防止重复加载,资源浪费;
如果任何的class都可以把class加载进内容的话,假如我把Oracle写的内部核心的
java.lang.String
,我就可以自己写并且覆盖他,交给自定义的ClassLoader加载,把这个String
load进内存,打包给客户,然后把密码存储成string类型的对象,我就可以偷偷的把密码发给我自己,那就不安全了而双亲委派就会产生警觉,先去看看父级有没有加载过,有加载过就直接返回,不会重复加载
双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程
如果从源码的角度理解
父加载器
,就是有一个class的对象,里面有一个成员变量叫parent,这个parent是ClassLoader
类型的,这个parent指定为哪个类加载器就是哪个加载器,我们所说的父加载器就是这个parent对象
例子:
1 | package com.shifpeng.scaffold.service.jvm; |
六、类加载器的范围
我们在打印类加载器的时候会显示
1 | sun.misc.Launcher$AppClassLoader@7f31245a |
Launcher
是ClassLoader的一个包装类启动类
而Bootstrap
、Extension
、App
这些的加载路径是如何指定的呢?就是来自于Launcher
源码,在源码中可以看到这几个指定的路径,当然按照这些方式我们也可以看到:
1 | package com.shifpeng.scaffold.service.jvm; |
七、自定义类加载器(ClassLoader
源码)
想要自定义类加载器就需要稍微读一读ClassLoader
的源码
首先如果要把一个类加载到内存的时候,应该怎么做?
1 | package com.shifpeng.scaffold.service.jvm; |
这里有个loadClass
方法
通过源码我们看到,只需要调用
loadClass
方法,把这个类加载到内存中,然后返回一个Class类的对象也就是前面说的,从硬盘上找到这个类的源码,然后加载到内存中,与此同时生成一个Class的对象,返回这个对象
1 | public Class<?> loadClass(String name) throws ClassNotFoundException { |
热部署就是需要一个
ClassLoader
将类手动load到内存中
ClassLoader
就是反射的基石
我们再看下方法中的loadClass()
方法:
1 | // The parent class loader for delegation |
在向上找是否有类加载器已经加载了该类,如果没找到,就开始向下找符合加载该类的加载器,这里调用了一个findClass()
方法,这个方法是protected
的,也就是说只能在子类中访问;但是我们发现这个方法的实现直接抛出了一个异常ClassNotFoundException
,因此只能自定义ClassLoader重写这个findClass
,因此其实每个父加载器都有重写这个findClass
方法
因此,如果我们要自定义ClassLoader
,我们只需要做一件事就可以,就是定义自己的findClass()
,
这里用到的是设计模式中的模板方法
1 | package com.shifpeng.scaffold.service.jvm; |
其中,class文件如下
1 | Steven@StevendeMacBook-Pro dao % ls |
说明类已经加载进来了
loadclass过程:findCCache
->parent.loadClass
->findClass
八、加密
通过自定义ClassLoader,可以给自己的代码加密,下面就是一种简单的加密方式
a异或两次就是它自己.
b^seed^seed=b
1 | package com.shifpeng.scaffold.service.jvm; |
九、JVM之混合模式
刚才一直说的是加载,这里要说一下编译
Java是解释执行的,一个class文件load
到内存中,通过Java的解释器(interpreter)解释执行。
JIT
Java有一个叫做JIT(Just in-Time compiler)的东西,有些代码需要编译成为本地代码,相当于win中的exe可执行文件
默认模式
-
混合使用解释器+热点代码编译
热点代码编译:一段代码里面的一个循环或者一个方法在执行的时候刚开始是用解释器执行,结果发现在整个执行过程中,这段代码执行的频率非常高,我就会把这段代码编译成本地代码(如win的exe,linux的elf),将来再执行的时候执行我本地的这段代码,效率提升
那么为什么不直接都编译成本地代码?那岂不是效率更高了
1、因为java的解释器现在的效率其实已经很高了,在一些简单的代码的执行上不输于编译器2、如果有一段代码执行文件特别多,各种各样的类库有好几十个class很正常,如果上来直接用编译器编译,编译过程会非常长,所以默认模式是混合模式
3、完全可以使用参数来制定到底用什么模式
1
2
3-Xmixed 混合模式,开始解释执行,启动速度较快。对热点代码实行检测和编译
-Xint 使用解释模式,启动很快,执行很慢
-Xcomp 使用纯编译模式,执行很快,启动很慢(很多类的时候) -
起始阶段采用解释执行
-
热点代码检测
- 多次被调用的方法
方法计数器:检测方法执行的频率 - 多次被调用的循环
循环计数器:检测循环被执行的频率 - 进行编译
1
2//检测热点代码:
-XX:CompileThreshold=10000 - 多次被调用的方法
我们进行个小测试
1 | package com.shifpeng.scaffold.service.jvm; |
在Jvm的参数配置这里,先不填任何,即混合模式试试
执行后结果为0.86s
,我们设置为解释模式试试
执行后时间为21.57s
纯编译执行时间为0.7s
(如果类特别多的话,启动就会非常慢)
因此m()
方法被检测到大量重复执行,所以会对这个方法进行编译,而其他的一些代码就是解释执行
十、 懒加载(lazyLoading)
严格的讲应该叫做lazyInitialzing
懒初始化
-
JVM规范并没有规定何时加载,什么时候需要这个类再去加载
-
但是严格规定了什么时候必须初始化(共五种情况,了解即可 )
Java虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1.遇到new,getstatic,putstatic和invokestatic这4条指令时,如果类没有初始化时,则需要先触发其初始化。生成这4条指令的最常见Java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法。
-
使用java.lang.reflect包的方法对类进行反射调用时候。
-
当初始化一个类时,发现其父类没有进行初始化,则需先触发其父类的初始化。
-
当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),当前类需要先初始化
-
当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_putStatic、REF_getStatic、REF_invokeStatic的方法句柄,并且此方法句柄所对应的类没有进行初始化时,需要初始化该类。
-
而上面说到class文件从硬盘加载到内存中,主要过程是这样的Loading->Linking->Initlalizing
- Loading
讲class文件load
到内存- Linking:
- Verfication: 校验,校验是否符合class文件的标准
- Preparation:将class文件中的静态变量赋默认值
- Resolution:把class文件中常亮池用到的符号引用。转化成内存地址,可以直接使用
- Initlalizing:静态变量赋值为初始值
我们说需要这个类的时候才会被加载,所以如何判断是否加载了呢,看下这段代码
1 | package com.shifpeng.scaffold.service.jvm; |
而Initlalizing
会执行上面``System.out.println(“P”);的静态语句块,而
P`一旦被打印,说明这个类被load进来了
下节说加载过程之Linking、加载过程之intializing
扩展:自定义ClassLoader的parent是怎么指定的?
任何一个classLoader,都有一个parent,,而我们自定义的ClassLoader
并没有指定parent,那就需要去看源码
我们在new
我们自定义的ClassLoader
的时候,会调用父类的无参构造方法
1 | protected ClassLoader() { |
这里的 getSystemClassLoader()
,系统的ClassLoader
是AppClassLoader
,这个可以自己验证下就可以
方法里面又调用了自己的另外一个构造方法
1 | protected ClassLoader(ClassLoader parent) { |
这个方法中已经指定了parent,即上面传入的AppClassLoader
我们也是可以自己指定指定父亲的ClassLoader
1 | package com.shifpeng.scaffold.service.jvm; |
用自己订单的ClassLoader继承ClassLoader
,构造方法里面调用super
指定parent
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !