JVM梳理
1、编译
class Person{
private String name;
private int age;
private static String address;
private final static String hobby="Programming";
public void say(){
System.out.println("person say..."); }
public int calc(int op1,int op2){
return op1+op2;
} }
cafe babe 0000 0034 0027 0a00 0600 1809
0019 001a 0800 1b0a 001c 001d 0700 1e07
001f 0100 046e 616d 6501 0012 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0361 6765 0100 0149 0100 0761 6464 7265
7373 0100 0568 6f62 6279 0100 0d43 6f6e
7374 616e 7456 616c 7565 0800 2001 0006
3c69 6e69 743e 0100 0328 2956 0100 0443
6f64 6501 000f 4c69 6e65 4e75 6d62 6572
5461 626c 6501 0003 7361 7901 0004 6361
6c63 0100 0528 4949 2949 0100 0a53 6f75
7263 6546 696c 6501 000b 5065 7273 6f6e
2e6a 6176 610c 000f 0010 0700 210c 0022
0023 0100 0d70 6572 736f 6e20 7361 792e
2e2e 0700 240c 0025 0026 0100 0650 6572
736f 6e01 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0100 0b50 726f 6772 616d
6d69 6e67 0100 106a 6176 612f 6c61 6e67
2f53 7973 7465 6d01 0003 6f75 7401 0015
4c6a 6176 612f 696f 2f50 7269 6e74 5374
7265 616d 3b01 0013 6a61 7661 2f69 6f2f
5072 696e 7453 7472 6561 6d01 0007 7072
696e 746c 6e01 0015 284c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b29 5600 2000
0500 0600 0000 0400 0200 0700 0800 0000
0200 0900 0a00 0000 0a00 0b00 0800 0000
1a00 0c00 0800 0100 0d00 0000 0200 0e00
0300 0000 0f00 1000 0100 1100 0000 1d00
0100 0100 0000 052a b700 01b1 0000 0001
0012 0000 0006 0001 0000 0001 0001 0013
0010 0001 0011 0000 0021 0002 0001 0000
0009 b200 0212 03b6 0004 b100 0000 0100
1200 0000 0600 0100 0000 0700 0100 1400
1500 0100 1100 0000 1c00 0200 0300 0000
041b 1c60 ac00 0000 0100 1200 0000 0600
0100 0000 0900 0100 1600 0000 0200 17
//U4对应的是16进制文件的前八位,U2对应前四位
ClassFile {
u4 magic; //魔数:用于确定这个文件是否能被虚拟机所接受的标志,对应 cafe babe
u2 minor_version;//次版本号 0000
u2 major_version;//主版本号 0034 对应10进制52 也就对应JDK1.8
u2 constant_pool_count;//常量池数量:0027 27个常量
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
2、类加载机制
将class文件交给JVM
(1)装载
-
找到类文件所在的位置 --> classloader -->不同的类装载器负责装载不同的类
几种类加载器?
1、Bootstrap ClassLoader:
启动类加载器,负责装载 JAVA_HOME/jre/lib/rt.jar 下面的所有的class,或者 Xbootclassoath 选项指定的jar包(C++实现,不是classloader的子类)
2、Extension ClassLoader:
扩展类加载器,负责装载 JAVA_HOME/jre/lib/ext 下面所有的jar包,或者 -Djava.ext.dirs 指定的目下的所有jar包
3、App ClassLoader:
系统类加载器,负责加载 应用程序classpath 下的所有的jar和class文件,或者 -Djava.class.path 指定目录下的所有的jar包
4、Custom ClassLoader:
自定义类加载器,通过java.lang.ClassLoader的子类自定义去加载class,属于应用程序根据自身需要去自定义的classloader,如tomcat、jboss都会根据j2ee规范自行实现classloader
装载的原则?
全路径一致视为同一个类,由双亲委派机制保证同一个类只能被加载一次。
装载过程中的双亲委派机制?
如果一个类加载器在接收到类加载请求时,它首先不会去加载这个类,而是把这加载请求委托给父类加载器去完成,一次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成加载任务时,才会自己去加载。
一定是先递交到最上层,然后由上而下依次尝试加载,如果父类之上都加载不到,才由自己加载
优势?
java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,java中的Object,它存放的位置在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派机制,那么由各个类加载器去自己加载的话,就会出现多种不同的Object类。
破坏双亲委派?
可以继承ClassLoader类,重写loadClass方法,不向父加载器递交加载请求。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 同步上锁 synchronized (getClassLoadingLock(name)) { // 先查看这个类是不是已经加载过 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 递归,双亲委派的实现,先获取父类加载器,不为空则交给父类加载器 if (parent != null) { c = parent.loadClass(name, false); // 前面提到,bootstrap classloader的类加载器为null,通过find方法来获得 } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { // 如果还是没有获得该类,调用findClass找到类 long t1 = System.nanoTime(); c = findClass(name); // jvm统计 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } // 连接类 if (resolve) { resolveClass(c); } return c; } }
-
类文件信息交给JVM -->类文件的字节码刘静态存储结构 --> 方法区(Method Area)
将这个字节流所代表的静态存储结构转化成方法区的运行时数据结构
-
将类文件对应的class对象交给JVM --> 堆(heap)
在Java堆中生成一个代表这个类的class对象,作为对方法区中这些数据的访问入口
(2)链接
-
验证:
保证被加载类的正确性
文件格式验证
元数据验证
字节码验证
符号引用验证
-
准备:
为类的静态变量分配内存,并将其初始化为默认值
static int i=10; 在此 i 初始化为0
如果是final修饰的静态变量在这个阶段就会被赋值而不是在初始化阶段才赋值(同理:如果调用一个类中的常量是不会触发类的初始化的)
-
解析:
把类的符号引用转换成直接引用
符号引用转直接引用?标志变为真实引用
符号引用:和内存不想关,class里面的一些名字,就是一个代称
直接引用:将名字对应到真正的内存地址指向
(3)初始化
为静态变量初始化真正的值 i = 10
什么时候才会触发类的初始化?
1、遇到new、getstatic、putstatic或者invokestatic这四条指令的时候如果类没有初始化,则需要触发其初始化,初始化就一定会执行静态代码块。这四条指令的场景:new创建一个对象实例、读取或设置一个类的静态字段(非final修饰)、调用一个类的静态方法
2、反射调用
3、初始化一个类的时候,如果父类没有被初始化,则优先初始化其父类
4、虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会初始化这个类
5、当使用JDK1.7的动态语言支持时,如果一个Java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_outStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
3、运行时数据区
(1)运行时常量池
方法区的一部分,存储常量信息,类加载后常量池中的数据会存储在这里(基本类型和包装类型)
(2)程序计数器
线程私有,记录的是正在执行的虚拟机字节码指令地址,为了让cpu回来执行时可以找到正确的执行位置
(3)虚拟机栈
线程独享:一个方法链中的方法的入栈出栈的过程
(4)本地方法栈
线程私有,本地方法:c++代码,同样是方法的入栈出栈的过程
(5)方法区
线程共享,虚拟机启动时创建,生命周期和JVM相同
存储的是已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据
逻辑上属于堆的一部分,但是为了区分开来,将其称为非堆
(6)堆
线程共享,java虚拟机管理内存中的最大的一快,在虚拟机启动时创建,存储对象实例
4、总结一下
(1)类加载过程
装载 – 》 链接 --》初始化
装载:通过类加载器找到类文件 --》 将类文件信息交给jvm方法区 --》将class对应的对象交给jvm堆(heap)
链接:验证-保证被加载类的正确性–》准备-为类的静态变量分配内存空间并设置默认值 --》解析-将符号引用转换成直接引用
初始化:为静态变量初始化真正的值
(2)运行时数据区
方法区:类信息,静态变量、常量、即时编译代码,线程共享
堆:存对象,线程共享
虚拟机栈:方法连的入栈出栈过程,线程私有
本地方法栈:c语言,线程私有
运行时常量池:方法区的一部分,类加载后常量池中的数据会存储在这里(基本类型和包装类型)
程序计数器:线程私有,为cpu调度提供,程序正在执行的字节码指令地址,