jvm是什么
java virtual Machine(java 虚拟机)
Person.java->词法分析器->tokens流->语法分析器->语法树/抽象语法树->语法分析器->注解抽象树->字节码生成器->Person.class文件
反编译验证编译指令 javap -v -p Person.class
进行反编译之后,查看字节码信息和指令等信息
##类加载机制
类加载机制是指我们将类的字节吗文件所包含数据读入内存,同时我们会生成数据的访问入口的一种特殊机制。那么我们可以得知,类加载的最终产品是数据访问入口
这个时候,看到这张图,我们应该有一个问题,那就是我们的字节码加载的方式,也就是我们的字节码文件可以用什么方式进行加载呢?
加载.class文件的方式
1从本地系统中直接加载
典型场景:。。。
2通过网络下载.class文件
典型场景:web Applet,也就是我们的小程序应用
3从zip,jar等归档文件中加载.class文件
典型场景:jsp应用从专有数据库中提取.class文件,较为少见
4从专有数据库中提取.class文件
典型场景:jsp应用从专有数据库中提取.class文件,较为少见
将java源文件动态编译为.class文件,也就是运行时计算而成
典型场景:动态代理技术
从加密文件获取
典型场景:典型的防Class文件被反编译的保护措施
问题接锺而至,我们的类加载的方式已经了解了,那么加载的流程到底时怎样的呢?
查找和导入class文件
1,通过一个类的全限定名获取定义此类的二进制字节码流
思考:需要什么工具寻找获取我们的二进制流
而我们的java中恰好有这么一段代码模块,可以实现通过类全名来获取此类的二进制字节流这个动作,并且将这个动作放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类,实现这个动作的代码模块成为类加载器。
2将这个字节码流所代表的静态存储结构转换为方法区的运行时数据结构
3在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
在我们的装载阶段完成之后,这时候我们的内存当中,我们的运行时数据区的方法区及堆就已经有数据了。
方法区:类信息,静态变量,常量
堆:代表被加载类的java.lang.Class对象
即时编译之后的热点代码并不在这个阶段进入方法区
验证只是为了确保Class文件中的字节流包含的信息完全符合当符合当前虚拟机的要求,并且还要要求我们的信息不会危害虚拟机自身的安全,导致虚拟机的崩溃。
文件格式验证
验证字节流是否符合Class文件格式的规范,并且都能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储与方法区之内。这阶段的验证是基于二进制字节流进行的,只是经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行的
是否以16进制cafebaby开头
版本号是否正确
元数据验证
对类的元数据信息进行语义校验,保证不存在不符合java语法
规范的元数据信息
举例:
是否有父类
是否继承了final类
因为我们的final类是不能被继承的,继承了就会出现问题。
一个非抽象类是否实现了所有的抽象方法
如果没有实现,那么这个类也是无效的。
对类的元数据信息进行语义校验,保证不存在不符合java语法规范的元数据信息。
字节码验证
进行数据流和控制流分析,确定程序语义是合法的,符合逻辑的。对类的方法体进行校验分析,保证被校验的类的方法运行时不会做出危害虚拟机安全的行为。
把类中的符号引用转换为直接引用
符号引用就是一组符合来描述目标,可以是任何字面量。引用的目标并不一定已经加载到内存中。直接引用就是直接指向目标的指针,相对偏移量或一个间接定位的目标的句柄。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用限定符合7类符号引用进行。
直接引用时虚拟机内存布局实现相关,同一个符合引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中
同一符号引用进行多次解析请求是很常见的,除了invokedynamic指令以外,虚拟机实现可以对第一次解析结果进行缓存,来避免解析动作重复进行,无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个引用符号之前已经被成功解析过,那么后续的引用解析请求就应当一直成功,同样,如果第一次解析失败,那么其他指令对这个符号的解析请求也应该收到相同的异常。
invokedynamic是java7引入的一条新的虚拟机指令,这是自1.0以来第一次引入新的虚拟机指令。到了java8这条指令才第一次在java应用,用在lamdba表达式中。invokedynamic与其他invoke指令不同的是它允许由应用级的代码来决定方法解析。
初始化阶段是执行类构造器()方法的过程。
在java中对类变量进行初始值设定有两种方式:
1.声明类变量是指定初始值
2使用静态代码块为类变量指定初始值
按照程序员的逻辑,你必须把静态变量定义在静态代码块的前面。因为两个的执行是会根据代码编写的顺序来决定的,顺序搞错了可能会影响你的业务代码。
jvm初始化步骤
假如这个类还没有被加载和连接,则程序先加载并连接该类
假如该类的直接父类还没有被初始化,则先初始化其直接父类
假如类中有初始化语句,则系统依次执行这些初始化语句
我们的初始化过程什么时候会被触发执行?或者换句话说类初始化时机是什么呢?
主动引用
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种
1创建类的实例,也就是new的方式
2,访问某个类或接口的静态变量,或者对该静态变量赋值
3,调用类的静态方法
4反射(如Class.forName(com.carl.test))
5初始化某个类的子类,则其父类也会被初始化
6java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类
被动引用
1引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
2定义类数组,不会引起类的初始化
3引用类的static final常量,不会引起类的初始化(如果只是static修饰,还是会引起该类初始化的)。
在类使用完之后,如果满足下面的情况,类就会被卸载
1该类所有实例都已经被回收,也就是java堆中不存在该类的任何实例。
2加载该类的ClassLoader已经被回收。
3该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了,但是一般情况下启动类加载的类不会被卸载,而我们的其他两种基础类型的类加载器只有在极少情况才会被卸载。
什么是类加载器
负责读取java字节码,并转换成java.lang.Class类的一个实例的代码模块
类加载器除了用来加载类外,还可用于确定类在java虚拟机中的唯一性
一个类在同一个类加载器中具有唯一,而不同类加载器中是允许同名类存在的,
这里的同名是指全限定名相同。但是在整个jvm里,纵然全限定名相同,若类加载器不同,则乃然不算作是同一个类,无法通过instanceOf,equals等方式的校验。
1)Bootstrap ClassLoader
负责加载
J
A
V
A
H
O
M
E
中
j
r
e
/
l
i
b
/
r
t
.
j
a
r
里
所
有
的
c
l
a
s
s
或
X
b
o
o
t
c
l
a
s
s
o
a
t
h
选
项
指
定
的
j
a
r
包
。
是
由
c
+
+
实
现
,
不
是
C
l
a
s
s
L
o
a
d
e
r
的
子
类
。
2
)
E
x
t
e
n
s
i
o
n
C
l
a
s
s
L
o
a
d
e
r
负
责
加
载
j
a
v
a
平
台
中
扩
展
功
能
的
一
些
j
a
r
包
,
包
括
JAVA_HOME中 jre/lib/rt.jar里所有的class或Xbootclassoath选项指定的jar包。是由c++实现,不是ClassLoader的子类。 2)Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括
JAVAHOME中jre/lib/rt.jar里所有的class或Xbootclassoath选项指定的jar包。是由c++实现,不是ClassLoader的子类。2)ExtensionClassLoader负责加载java平台中扩展功能的一些jar包,包括JAVA_HOME中的jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。
3)App ClassLoader
负责加载Classpath 中指定的jar包及Djava.class.path所指定目录下的类和jar包。
4)Custom ClassLoader
通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat,jboss都会根据j2ee规范自行实现ClassLoader。
为什么我们的类加载器要分层
1.2版本的jvm中,只有一个类加载器,就是现在的Bootsrap类加载器,也就是根类加载器。但是这样会出现一个问题。假如用户调用它编写的java.lang.String类。理论上该类可以访问和改变java.lang包下其他类的默认访问修饰符的属性和方法的能力。也就是说,我们其他的类使用String时也会调用这个类,因为只有一个类加载器,我无法判定到底加载哪个个。因为java语言本身并没有阻止这种行为,所以会出现问题。
控制台输出
全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
例如,系统类加载器AppClassLoader加载入口类时,会把main方法所依赖的类及引用的类也载入,依此类推。"全盘负责"机制也可称为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。
以上步骤只是调用了ClassLoader.loadClass(name)方法,并没有真正意义类。真正加载class字节码文件生成Class对象由双亲委派机制完成
父类委托,双亲委派是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只是在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。
双亲委派机制加载Class的具体过程是:
1.ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。
2.父类加载器判断是否加载过该类,如果已加载,则返回Class对象;如果没有怎委托给祖父类加载器。
3依此类推,直到始祖加载器。
4始祖类加载器判断是否加载过该类Class,如果已加载,则返回Class对象;如果没有则尝试从其他对应的类路径下寻找Class字节码文件并载入,如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。
5始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功则返回Class对象,如果载入失败,则委托给始祖加载器的孙类加载器。
6依此类推,直到ClassLoader.
7源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会在委托其子类加载,而是抛出异常
双亲委派机制只是java推荐的机制,并不是强制的机制。
我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。
缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启jvm,程序的修改才会生效,对于一个类加载器实例来说,相同全名的类只加载一次,即loadClass方法不会被重复调用。
打破双亲委派
双亲委派这个模型并不是强制模型,而且会带一些问题,就比如java.sql.Driver这个东西。jdk只是能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放jdk目录里吧。
所以java想到了好几种方法可以用来打破我们的双亲委派。
SPI:比如java从1.6搞出了SpI就是为了优雅的解决这类问题–jdk提供接口,供应商提供服务。编程人员编码时面向接口编程,然后jdk能够自动找到适合的实现,此不是很爽。
java在核心类库中定义了许多接口,并且还给出针对这些接口的调用逻辑,然而并未给出实现。开发者要做的就是定制一个实现类。在META-INF/services中注册实现类信息,以供核心类库使用。比如JDBC中的DriverManager
OSGI:比如我们的java程序更加追求程序的动态性代码热部署,代码热替换。也就是机器不用重启,只要部署上就用。OSGI实现模块化热部署的关键则它自定义的类加载器机制的实现。每一个程序模块都有一个自己的类加载器,当需要更换一个程序模块时,就把程序模块连同类加载器一起换掉以实现代码的热替换。
自定义加载器
自定义加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。



