Java Class文件的结构

在*.class文件中,以Byte流的形式进行Class的存储,通过一系列Load,Parse后,Java代码实际上可以映射为下图的C结构体,这里可以用javap -v -p命令或者IDE插件进行查看。

typedef struct {
    u4             magic;/*0xCAFEBABE*/
    u2             minor_version; /*网上有表可查*/
    u2             major_version; /*网上有表可查*/
    u2             constant_pool_count;
    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];
}ClassBlock;
  • 常量池(constant pool):类似于C中的DATA段与BSS段,提供常量、字符串、方法名等值或者符号(可以看作偏移定值的指针)的存放

  • access_flags: 对Class的flag修饰

      typedef enum {
      	ACC_PUBLIC = 0x0001,
      	ACC_FINAL = 0x0010,
      	ACC_SUPER = 0x0020,
      	ACC_INTERFACE = 0x0200,
      	ACC_ACSTRACT = 0x0400
      }AccessFlag
    
  • this class/super class/interface: 一个长度为u2的指针,指向常量池中真正的地址,将在Link阶段进行符号解引。

  • filed: 字段信息,结构体如下

    typedef struct fieldblock {
       char *name;
       char *type;
       char *signature;
       u2 access_flags;
       u2 constant;
       union {
           union {
               char data[8];
               uintptr_t u;
               long long l;
               void *p;
               int i;
           } static_value; 
           u4 offset;
       } u;
    } FieldBlock;
    
  • method: 提供descriptor, access_flags, Code等索引,并指向常量池:

    它的结构体如下,详细在这里

      method_info {
          u2             access_flags;
          u2             name_index;
          //the parameters that the method takes and the 
          //value that it return
          u2             descriptor_index;
          u2             attributes_count;
          attribute_info attributes[attributes_count];
      }
    

以上具体内容可以参考

  1. JVM文档
  2. 周志明的《深入理解Java虚拟机》,少见的国内精品书籍
  3. 一些国外教程的解析

Class与Object

typedef struct object Class;

typedef struct object {
   uintptr_t lock;
   Class *class;
} Object;

本文指的Class

前提: 已经获取到Class结构体对应的指针

下面是经过删减与注释的代码(删去了状态判断、Lock与异常处理),并替换宏变量为字符串

// class.c
Class *initClass(Class *class) {
   ClassBlock *cb = CLASS_CB(class);
   ConstantPool *cp = &cb->constant_pool;
   FieldBlock *fb = cb->fields;
   MethodBlock *mb;
   Object *excep;
   int state, i;

   linkClass(class);

   cb->state = CLASS_INITING;
   cb->initing_tid = threadSelf()->id;

   if(!(cb->access_flags & ACC_INTERFACE) && cb->super
              && (CLASS_CB(cb->super)->state != CLASS_INITED)) {
      initClass(cb->super);
   }

   /* Never used to bother with this as only static finals use it and
      the constant value's copied at compile time.  However, separate
      compilation can result in a getstatic to a (now) constant field,
      and the VM didn't initialise it... */

   for(i = 0; i < cb->fields_count; i++,fb++)
      if((fb->access_flags & ACC_STATIC) && fb->constant) {
         if((*fb->type == 'J') || (*fb->type == 'D'))
            fb->u.static_value.l = *(u8*)&(CP_INFO(cp, fb->constant));
         else
            fb->u.static_value.u = resolveSingleConstant(class, fb->constant);
      }

   if((mb = findMethod(class, "<clinit>", "()V")) != NULL)
      executeStaticMethod(class, mb);
 
   return class;
}

贴出上面主要是为了让你明白

  • 调用linkClass进行连接
  • 调用super的class中的initClass
  • 调用<clinit> 方法,也就是static代码段

上面部分实际上是对Class进行模式匹配(pattmatch)的遍历,伪代码如下

(define initClass
  (lambda (exp)
    (linkClass exp)
    (match [(?isSuperInited superClass) (initClass superClass)])
  	(clinit exp)))

在平时开发中,只需要背住就可以了。

Class与依赖注入

很多人认为学习JVM是“高手”才去做的,平时写业务时没用,下面举一个Spring断点调试技巧。在分析Spring的依赖注入时,很多人看到复杂源码就无法接着分析了。其实可以这样,首先在Bean中加入static代码段,并打上断点,然后启动程序。

@Bean(name="SSR")
public class DemoBean{
    static{
      	// 此处打上断点
        System.out.println("class loaded");
    }
  	....
}

等断点跳到这里时,可以发现是Class.newInstance()方法被调用,进而调用<clinit>,此时再向上分析Spring的代码堆栈,阅读源码与流程就轻而易举了。

附录

下面2个是常见面试题

求打印顺序

实例化AChildChild后,求输出顺序

public class AParent {
    static {println("AParent clinit");}//1
    public AParent() {System.out.println("AParent init");}//4
}
public class AChild extends AParent {
    static {println("AChild clinit");}//2
    public AChild() {println("AChild init");}// 5
}
public class AChildChild extends AChild {
    static {println("AChildChild clinit");}//3
    public AChildChild() {println("AChildChild init");}//6
}
AChildChild acc = new AChildChild();

其中1,2,3的顺序本文已经可以解释,4,5,6下次讲解init时进行分析

JDBC的加载

下图是加载mysql的例子,当程序员调用Class.forName时,static代码段就会执行

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    public Driver() throws SQLException {}
}