JVM简介及类加载机制(一)
JVM介绍:
目标:JVM运行字节码文件,根据JVM的日志调节程序,对于底层原理有一定的了解
1. 类加载
在JAVA代码中,类型的加载,连接与初始化都是在程序运行期间完成的,提供了灵活性增加了更多的可能性
2. 类加载器(加载类的工具)
虚拟机结束生命周期的情况
A) 执行System.exit(),finally中的代码都不能执行
B) 程序正常结束
C) 程序在执行过程当中碰到异常或异常终止
D) 操作系统出现错误
3.类的加载,连接,初始化
加载:查找并加载类的二进制(把类的二进制文件加载到内存里面)
连接:
A) 验证:确保加载类的正确性
B) 准备:为类的静态变量分配内存,并将其初始化为默认值
例子:
Class Test{
public static int a=1;
}
这个阶段将赋予a默认值0,赋值阶段在后面完成
C) 解析:把类中的符号引用转换为直接引用
符号引用:一个方法里面引用令一个方法
直接引用:指针直接指向方法的内存中的地址
初始化阶段:为类的静态变量分配正确的值,如上例中将1赋值给a
4.类的使用和卸载
类的使用分为主动使用和被动使用
所有的java虚拟机实现必须在每个类或接口被java”首次主动使用”(静态代码块只执行 一次)时才初始化他们
主动使用:
A) 创建类的实例(new 一个对象)
B) 访问某个类或接口的静态变量,或对该静态变量赋值
C) 调用类的静态方法
D) 反射(Class.forName(Class clazz))
E) 初始化一个类的子类(初始化子类时会先初始化父类)
F) Java虚拟机启动时被标明为启动类的类
G) 1.7以后提供了动态语言的支持
被动使用:
除了以上7种都是被动使用,不会导致类的初始化
5.类的加载
将.class的二进制数据读入内存当中,并将其放在运行时数据区的方法区内,然后在内存中创建java.lang.Class对象,用来封装类在方法区的数据结构
6.加载.class的方式
A)从本地系统直接加载
B)网络下载.class文件
C)压缩包里面下载
D)将java源文件动态编译成.class文件(动态代理)
下面来看个例子1:
public class Test1 { public static void main(String[] args) { System.out.println(MyChild.str); } } class MyParent{ public static String str="hello world"; static{ System.out.println("myparent block"); } } class MyChild extends MyParent{ static{ System.out.println("mychild block"); } }
console
myparent block
hello world
再看看下面这个
public class Test1 { public static void main(String[] args) { System.out.println(MyChild.str2); } } class MyParent{ public static String str="hello world"; static{ System.out.println("myparent block"); } } class MyChild extends MyParent{ public static String str2="hello world11"; static{ System.out.println("mychild block"); } }
console
myparent block
mychild block
hello world11
注:在子类在初始化是要先初始化父类
-XX:+TraceClassLoading 打印加载类的信息
+表示开关
例子2:
public class Test2 { public static void main(String[] args) { System.out.println(MyParent1.str); } } class MyParent1{ public static String str="hello"; static{ System.out.println("parent block"); } }
这个结果不必多说
parent block
hello
如果静态变量加了final呢?
public class Test2 { public static void main(String[] args) { System.out.println(MyParent1.str); } } class MyParent1{ public static final String str="hello"; static{ System.out.println("parent block"); } }
console
hello
那是因为什么呢?原因很简单
那就是常量在编译阶段就会存入到调用该常量的方法的类的常量池当中
下面说一些助记符的意义
ldc表示将int,float或是string类型的常量值从常量池中推送至栈顶(马上就要使用)
Blpush:表示字节(-128-127)的常量推送至栈顶
Sipush:将短整形常量值推送至栈顶
Iconst_1表示将int型的数字1推送至栈顶(Iconst_1-Iconst_5)
那再来看看下面这个例子3
public class Test3 { public static void main(String[] args) { System.out.println(MyParent2.str); } } class MyParent2{ public static String str=UUID.randomUUID().toString(); static{ System.out.println("parent block"); } }
console
parent block
6eb597fb-e999-40f8-8dc9-bf57c9124914
不知道结果与你的预期是否相同?
原因:当一个常量的值在编译期间不能确定,那么七值就不会放到调用类的常量池当中。这时当程序在运行时,会导致主动使用这个常量所在的类,就会导致该类初始化
下面再来看一个例子
public class Test4 { public static void main(String[] args) { //MyParent4 m4=new MyParent4(); MyParent4[] m4=new MyParent4[1]; } } class MyParent4{ static{ System.out.println("parent block"); } }
这代表了什么?
MyParent4[] m4=new MyParent4[1];这个并不会主动使用
对于数组来说,其类型时JVM再运行期间动态生成的