Java Class解析

198 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 31 天,点击查看活动详情

一、Class 类的结构

1.前四个字节:

魔数:确定这个文件是不是被虚拟机接收的class文件,是固定不变的CA FE BA BE

2.第 5-6 个字节:代表次版本号

3.第 7-8:代表主版本号

jdk1.7 0030
jdk1.8 0034

4.第 9-10 代表常量池的数量,常量池中每一个常量都是一个表,一共有 14 个,每个表的数据类型都不同,唯一同的是他们的第一位都是标志位。 标志位图片

TIM截图20180729234733.png 4.1 用 javap 命令可以查看常量池,例:

 javap -verbose TestClass

5.之后就是访问标志,将符合标志的值进行亦或计算, 标志图片

TIM截图20180730233149.png 6.数据类型图

TIM截图20180730234544.png

二、类加载机制

微信截图_20200115204759.png

2.1 以下情况对类进行初始化
  1. 使用 new 关键字实例化对象,读取或设置一个静态变量(被 final 修饰,或编译器把结果放入常量池的除外),以及调用一个类的静态方法时

  2. 使用 recleft 包的方法对类进行反射调用

  3. 当初始化一个类时,当他的父类还没有初始化时,先初始化他的父类

  4. 当虚拟机启动时,必须指定一个主类,这个主类会先被初始化

  5. 当使用 jdk1.7 的语言动态支持时,如果 MethodHandler 实例最后的解析结果为 REF_getStatic,REF_putStatic,REF_invokeStatic 的方法句柄,如果这个方法对应的类没有初始化,那要先初始化

除此之外所有引用的类的方式不会被初始化,称为被动引用

  1. 对于静态字段只有直接定义的类会被初始化,如父类中定义了一个静态字段,如果通过其子类来引用这个字段,那么只会初始化父类
  2. 数组中使用类,如Super[] s=new Super[5],不会初始化Super类
  3. 调用一个类中的常量池,不会初始化这个类
2.2.类加载的过程
  1. 加载
    • 通过全限定名获取二进制字节流
    • 将这个字节流代表的静态存储结构转为方法区的运行时数据结构
    • 生成一个Class对象,作为方法区这个类的各种数据的入口
  2. 验证
  3. 准备
    • 初始化静态变量初始值
  4. 解析
    • 在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用
  5. 初始化
    • 静态变量赋值,静态代码块执行
    • 初始化阶段是执行类构造器方法的过程,clinit方法时编译器自动收集属性类变量的赋值和静态代码块生成的。
    • clinit方法执行之前必须保证其父类的这个方法执行完毕
    • 如果多个线程去执行一个类的clinit方法,那么只有一个线程去执行这个类,其他线程阻塞
2.3 类加载器

微信截图_20200115205000.png

双亲委派机制的作用:

  • 沙箱安全机制,防止核心 API 库被篡改
  • 避免类的重复加载,当父加载器加载了,子加载器就不用加载了
  1. 不同的类加载器加载出来同一个类不"相等",equers,isInstance,instanceof 方法判断都会不同
  2. JVM 对 Class 文件是按需加载(运行期间动态加载)

三、堆栈

  1. 局部变量表:用于存放方法参数和方法内部定义的变量
    • 局部变量表的容量以变量槽为最小单位(Variable Table),32 位的数据占用一个 slot
  2. 操作数栈
    • 是一个后入先出的栈,32 位的占一个容量。
    • 例:如两个 int 类型的数相加,最接近栈顶的两个元素存入了两个 int 类型的值,然后执行 iadd 指令时,这两个数出栈并相加,然后结果入栈。
  3. 静态分派
    • 演示代码,输出都为 Human invoke
    • 原理:编译器在重载时通过参数的静态类型而不是实际类型
public class TestStaticDispatch {
    static  abstract class Human{}

    static class Man extends Human{

    }

    static class WoMan extends Human{

    }

    public void sayHello(Human human){
        System.out.println("Human invoke");
    }

    public void sayHello(Man man){
        System.out.println("Man invoke");
    }

    public void sayHello(WoMan woman){
        System.out.println("WoMan invoke");
    }

    public static void main(String[] args) {
        Human hu=new Man();
        Human hu2=new WoMan();

        TestStaticDispatch ts=new TestStaticDispatch();
        ts.sayHello(hu);
        ts.sayHello(hu2);
    }
}

4.动态分派

在运行期间根据实际类型来确定方法的执行分派叫做动态分派

四、类加载实例

  1. tomcat 目录结构
  • /common 目录下,类库会被 tomcat 和所有应用程序使用
  • /server 下,类库对 tomcat 可用,所有的应用程序不可见
  • /shared 下,类库对所有的应用程序可见,对 tomcat 不可见
  • /WEB-INF 下,只对当前应用程序可见,对 tomcat 和其他应用不可见

注:一般情况下只能向上加载,Shared 可以使用被 Common 类加载器加载的类

为了实现这样的目录权限,tomcat 自定义了几个类加载器,如图:

TIM截图20180802195704.png

五、示例

字节码层的反射写法:

public class MethodHandleTest {

    static class ClassA{
        public void println(String s){
            System.out.println(s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj=System.currentTimeMillis()%2==0?System.out:new ClassA();
        getPringlnMH(obj).invokeExact("哈哈哈");
    }

    private static MethodHandle getPringlnMH(Object reveiver) throws NoSuchMethodException, IllegalAccessException {
        MethodType mt=MethodType.methodType(void.class,String.class);
        return lookup().findVirtual(reveiver.getClass(),"println",mt).bindTo(reveiver);
    }
}

六、对象结构

在 JVM 中,对象在内存中的布局为对象头,实例变量,对齐填充。

TIM截图20190826212328.png 实例变量:存放类的属性信息,包括父类的属性信息,如果是数组则还包括数组的长度。这部分内存按 4 字节对齐。

对象头主要结构是由 Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

虚拟机位数头对象结构说明
32/64bitMark Word存储对象的 hashCode、锁信息或分代年龄或 GC 标志等信息
32/64bitClass Metadata Address类型指针指向对象的类元数据,JVM 通过这个指针确定该对象是哪个类的实例

Monitor 介绍:

TIM截图20190826213025.png 每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

在 Java 虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的。ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程同时 monitor 中的计数器 count 加 1,若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 WaitSe t 集合中等待被唤醒。