JVM虚拟机笔记(2)

Java类结构与类加载

1 Class文件结构

  Java的规范分为Java语言规范和Java虚拟机规范,在设计之初就考虑了其他语言运行在Java虚拟机之上的可能性。

 

这块内容,看起来比较生涩乏味,也不能结合具体的Class文件来看(反编译出来的内容不能完全体现出这个Class结构),具体可以参看下面的文章,本文就简单说明一下

  http://wiki.jikexueyuan.com/project/java-vm/class.html 

  https://zhuanlan.zhihu.com/p/25823310

魔数与版本

  每个Class文件头的4个字节称为魔术,是Class文件的身份标识。第5,6两个字节是次版本号(Minor Version),第7、8两个字节是主版本号(Major Version)

常量池

  存放文本字符串,声明为final的常量值、类和接口的全限定名、字段名称和描述符、方法名称和描述符等

访问标志

  标识这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型等等

字段表集合

  字段表用于描述接口或者类中声明的变量。字段包含类级变量以及实例级变量(不包括局部变量),可以包括的信息有:字段的作用域(public、private、protected)、是实例变量还是类变量(static修饰)、可变性(final)、并发可见性(volatile修饰)

可否被序列化(transient修饰)、字段数据类型、字段名称等

 

方法表集合

类似于字段集合表,方法表集合可以包括的信息有:访问标志、名称索引、描述符索引、属性表集合

 

属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,已用于描述某些场景专有的信息。java程序方法体重的代码经过javac编译器处理之后,最终变为字节码指令存储在Code属性内。

 

2 类加载

 当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader。

2.1 类的生命周期

 

2.2 类加载过程图

 

2.2 类加载机制--双亲委派(叫父委派更贴切)

类加载机制描述的选择哪个类加载器来加载类的过程。系统中默认的加载方式由下图所示。

基本过程:当调用一个类加载器的加载方法时,类加载器会先尝试让父类加载器来加载,如果父加载器不能加载,则自己尝试加载。

 

引申:注意这里的关系不是类的继承上的父子类关系,而是逻辑上的父子类关联关系+委派逻辑来实现(委派逻辑可以看ClassLoader类的loadClass方法)

类uml图

 

委派逻辑主要代码,有ClassLoader提供。AppClassLoader、ExtClassLoader 是该类的子类,所以也继承了该方法。 

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

 

 

我们业务代码默认都是AppClassLoader来加载,下面是该加载器加载路径的一个截图:主要包括jre\lib目录。target\classes目录,本地maven仓库。(当然这是没有打成jar包的时候,在idea运行时会去加载的目录,如果打成jar包后运行程序, 那么加载路径就是该jar包)

 

在使用某一个类时,比如 Object obj = new Object();jvm会使用默认的类加载器(AppClassLoader)去加载Object类,加载器会先查看自己是否加载过该类,没有就让父类来加载,父类重复该动作,直到父类为null时,使用boot类加载加载

 

 

 

2.3  为啥要使用双亲委派机制

1 为啥要多个类加载器

一个类加载器加载目录是需要指定的,所以一个类加载器不可能将所有目录都覆盖到。每一个类加载器都是为了去在不同的情景下去加载类。比如,你可以从联网服务器上加载一个class文件,也可以从远程web服务器下载二进制类。

 

2 双亲委派优势

保证了java基础的类如 Object都是由Boot类加载器加载,并且不会多次加载。保证了所有使用到Object类的场景都是用的同一个类信息,其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。简单来说,加载类的方法有无数种,我们需要一个灵活的加载器系统去在特定的情况下按照我们的想法来加载类。

 

3 破坏双亲加载机制

这个机制很好用,对于我们业务开发来说,已经足够了,甚至都不需要接触类加载器是什么东西。但是在一些特殊场景下,需要破坏该机制。下面以tomcat类加载机制来说明

 

 

2.4 tom类加载机制

我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
  3. web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。

 

 

 

1 应用加载器加载class文件时,先查看系统类加载器(system,bootstarp)是否有加载过,避免重复加载,一般来说系统类加载器能加载的类在启动时,都已经加载过。没有加载的话先尝试自己加载,不能加载则交给父加载器Common来加载。每个应用都有自己的类加载器,所以应用之间的类是相互隔离的。而Common加载器是共用一个,所以Common下的类也是共用的。

2 jsp文件修改时,会重新编译重class文件,并创建一个新的加载器来加载这个新class文件,旧的加载器和类信息会卸载。实现热部署

 

 

 

2.5 类加载完成后的状态

 

 

问题:

类加载器完成之后的Class对象有什么作用?

类加载器是如何加载到jvm中,普通类为啥要有类加载器来加载?

 

参考:

 http://wiki.jikexueyuan.com/project/java-vm/class-loading-mechanism.html

 https://www.cnblogs.com/duanxz/p/3728737.html

3 方法调用

3.1 代码示例

 1 //main方法
 2 public class HelloWorldDemo2 {
 3 
 4 
 5     public static void main(String[] args) {
 6         Caller caller = new Caller();
 7         caller.callerMethod();
 8     }
 9 }
10 
11 
12 //主类
13 public class Caller {
14 
15     public static int a = 1;
16 
17     public static void staticMethod() {
18         System.out.println("caller static method");
19     }
20 
21         public void callerMethod() {
22         int b = a;
23         System.out.println(b+a);
24         staticMethod();
25         Father father = new Father();
26         Father son = new Son();
27         father.hardChoice(1);
28         father.hadChoice("father");
29         father.sayHello();
30         son.hardChoice(1);
31         son.hadChoice("son");
32         son.sayHello();
33     }
34 }
35 
36 
37 //调用类父类
38 public class Father {
39 
40     public void hardChoice(int a) {
41         System.out.println("father choose int: " + a);
42     }
43 
44     public void hadChoice(String b) {
45         System.out.println("father choose str: " + b);
46     }
47 
48     public void sayHello() {
49         System.out.println("father say hello");
50     }
51     
52 }
53 
54 //调用类子类
55 public class Son extends Father {
56 
57     @Override
58     public void hardChoice(int a) {
59         System.out.println("son choose int:" + a);
60     }
61 
62 }

 反编译后的代码

...
public
void callerMethod(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=4, args_size=1 0: getstatic #5 // Field a:I 3: istore_1 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7: iload_1 8: getstatic #5 // Field a:I 11: iadd 12: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 15: invokestatic #7 // Method staticMethod:()V 18: new #8 // class cn/timyag/jvmlearn/cp/rundemo/Father 21: dup 22: invokespecial #9 // Method cn/timyag/jvmlearn/cp/rundemo/Father."<init>":()V 25: astore_2 26: new #10 // class cn/timyag/jvmlearn/cp/rundemo/Son 29: dup 30: invokespecial #11 // Method cn/timyag/jvmlearn/cp/rundemo/Son."<init>":()V 33: astore_3 34: aload_2 35: iconst_1 36: invokevirtual #12 // Method cn/timyag/jvmlearn/cp/rundemo/Father.hardChoice:(I)V 39: aload_2 40: ldc #13 // String father 42: invokevirtual #14 // Method cn/timyag/jvmlearn/cp/rundemo/Father.hadChoice:(Ljava/lang/String;)V 45: aload_2 46: invokevirtual #15 // Method cn/timyag/jvmlearn/cp/rundemo/Father.sayHello:()V 49: aload_3 50: iconst_1 51: invokevirtual #12 // Method cn/timyag/jvmlearn/cp/rundemo/Father.hardChoice:(I)V 54: aload_3 55: ldc #16 // String son 57: invokevirtual #14 // Method cn/timyag/jvmlearn/cp/rundemo/Father.hadChoice:(Ljava/lang/String;)V 60: aload_3 61: invokevirtual #15 // Method cn/timyag/jvmlearn/cp/rundemo/Father.sayHello:()V 64: return
...

3.2 调用指令码说明

invokestatic: 调用静态方法
invokespecial: 调用实例构造器<init>方法、私有方法和父类方法
invokevirtual:调用所有的虚方法
invokeinterface: 调用接口方法,会在运行时再确定一个实现此接口的对象。
invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,之前的4条调用指令,分派逻辑是固话在java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

invokevirtual指令的解析过程:

1 找到操作数栈顶的第一个元素所指向的对象的实际类型

2 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,判断是否有访问权限,通过则返回这个方法的直接引用。

3 否则,按继承关系从下往上依次对父类进行上述步骤

4 最终没有找到适合方法,则抛错。

 

 

3.3 方法调用过程

 

 

 

3.4 总体加载图

 

 

 说明:

1 Class类中封装了类型的各种信息。在jvm中就是通过Class类的实例来获取每个Java类的所有信息的(类Class对象就是该类在内存中的代理)





 

 

 

 
posted @ 2019-03-25 13:14  timyag  阅读(152)  评论(0)    收藏  举报