类的加载方式分为隐式加载与显式加载两种。隐式加载指的是程序在使用new等方法创建对象时,会隐式地调用类的加载器把对应的类加载到JVM中。显式加载指的是通过直接调用class.forName()方法来把所需要的类加载到JVM中。
任何一个工程项目都是由许多个类组带颂液成的,当程序启动时,只把需要加载的类加载到JVM中,其他类只有被使用到的时候才会被加载,采用这种方法,一方面可以加快加载速度,另外一方面可以节约程序运行过程中对内存的开销。此外,在Java语言中,每个类或接口都对应一个.class文件,这些蠢物文件可以被看成一个个可以被动态加载的单樱滚元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。
我们知道 javac 命令可以将 .java 文件编译成 .class 文件,而这个 Class 文件 中包含了 Java虚拟机 指令集、符号表以及若干其他辅助信息;最终将在 Java虚拟机 运行。
本文是以 JVM8 为例的。
每一个 Class文件 都有如下的 ClassFile 文件结构:
先简单介绍一下 ClassFile 文件结构各部分含义:
描述符是表示字段或方法类型的字符串。
字段描述符表示类、实例或局部变量的类型。
从上面文法可以看出,字段描述符中一共有三个类型:
方法描述符包含 0 个或者多个参数描述符以及一个返回值描述符。
看了描述符,可能大家有点疑惑,泛型信息怎么表示啊?
常量池的通用格式如下:
目前 JVM8 中一共用 14 种常量类型,分别如下:
我们知道要使用一个字段或者调用一个方法,就必须知道字段或者方法所属类符号引用,和字段的名字和类型,方法的名字和方法参数类型以粗液及方法返回值类型。
但是我们知道类是能继承的,那么子类调用父类的方法或者字段,这里的所属类符号引用,到底是子类本身还是父类的呢?
我们知道类,方法,字段都有不同的访问标志,在 Class 文件 中使用一个 u2 类型数据项来存储,也就是最多可以有 16 个不同标志位。
在类,方法,字段中有相同的标志,也有不同的标志,总体规划,我们可以借助 Modifier 类的源码来了解:
在 Modifier 类中,类的访问标志:
我们知道在 java 中类可以用的修饰符有: public , protected , private , abstract , static , final , strictfp 。
但是我们再看 Class 文件 中类的访问标志:
仔细看,你会发现有些不同点:
在 Modifier 类中,字段的访问标志:
我们知道在 java 中字段可以用的修饰符有: public , protected , private , static , final , transient 和 volatile 。
但是我们再看 Class 文件 中字段的访问标志:
Class 文件 中字段的访问标志和 java 中字段的修饰符差不多,只是多了 ACC_SYNTHETIC 和 ACC_ENUM 两个标志。
在 Modifier 类中,方法的访问信滚标志:
我们知道在 java 中方法可以用的修饰符有:
public , protected , private , abstract , static , final , synchronized , synchronized 和 strictfp 。
但是我们再看 Class 文件 中方法的访问标志:
字段详情 field_info 的格式如下:
方法详情 method_info 的格式如下:
关于 Class 文件 中属性相关信息,我们再后面章节介绍。
我们可以通过 javap 的命令来阅读 Class 文件 中相关信息。
这个是最简单的一个类,没有任何字段和方法,只继承 Object 类,我们来看看它编译后的字节码信息,通过 javap -p -v T.class 的命令:
我们重点关注常量池相关信息,会发现虽然 T.class 很干净,但是也有 15 个常量,来我们依次分析:
与之前的例子相比较,多了一个字段和方法,那么得到的字节码信息如下:
但是你会发现常量池中怎么没有这个字岩坦物段 name 的 CONSTANT_Fieldref_info 类型的常量呢?
那是因为我们没有使用这个字段。
多写了一个方法 test1 来调用 name 字段和 test 方法,那么得到的字节码信息如下:
这里定义一个父类 TParent ,有一个公共字段 name 和方法 say 。子类
JDK6 HotSpot VM用instanceKlass来记录类的元数据,每个Java类有一个对应的instanceKlass。每个instanceKlass上引用着一个constantPoolOopDesc对象,然后间接引用着一个constantPoolCacheOopDesc对象。前者跟Class文件里记录的常量池的结构类似,而后者是为了让解释器运行得更高效的一个缓存。
举例的话,用VisualVM里的 SA Plugin 来演示,java.lang.String的状况。
这里我用JDK 7的一个预览版,build 96来运行VisualVM 1.3和一个groovysh,并且用VisualVM里的SA Plugin来观察groovysh的运行状态:
图1:java.lang.String对应的一个instanceKlass
留意到instanceKlass里有个_constants字段,引用着一个constantPoolOopDesc对象(后面简称constantPool对象)。
图2:观察constantPool对象的内容:
留意到它是一个类似数组的对象,里面有_length字段描述常量池内容的个数,后面就是常量池项了。
各个类型的常量是混在一起放在常量池里的,跟Class文件里的基本上一样。
最不同的是在这个运行时常量池里,symbol是在类之间共享的;而在Class文件的常量池里每个Class文件都有自皮做己的一份symbol内容,没共享。
图3:观察constantPool里其中一个Utf8常量的内容:
这张图的关注点是位于0x180188a8的一个symbol对象(内容是"intern"),它的结构跟数组类似,有_length来记录长度,后面是UTF-8编码的字节。
这些Utf8常量在HotSpot VM里以symbolOopDesc对象(下面简称symbol对象)来表现;它们可以通过一个全局的SymbolTable对象找到。注意:constantPool对象并不“包含”这些symbol对象,而只是引用着它们而已;或者说,constantPool对象只存了对symbol对象的引用,而没有存它们的内容。
让我们来看看原本的Class文件里内容是怎样的:
再对比图2看看,是不是正好对应上的?
图2里constantPool的第一个常量池项的内容是:
这个26738818数字是怎么来的呢?
实际上是:26738818 = 408 <<16 | 130
而原本Class文件里常量池的第一项内容正是#130.#408,也就是由一个Class_index和一个NameAndType_index组成的Methodref。
图2里还有个细节,可以看到原本Class文件里常量池第7项是一个Class,但在图2里显示的是一个“UnresolvedClass”。这正是动态类加载/链接的一个表现。这个项所指向的Class还没被String里的方法使用过,所以还没跟String链接起祥握漏来,所以这里看到是unresolved。
我们可以故意在那个groovysh里执行一句:
这样会引发String.charAt()方法执行的过程中抛出一个java.lang.StringIndexOutOfBoundsException异常,那么就必须要完成链接的步骤。
然后再去看看String的常量池的样子:
就可以看到常量池的第7项已经解析(resolve)好了,从原本的符号引用变成了一个直接引用。
在JDK7以后的更新版中,HotSpot VM会逐渐去除谨烂PermGen,原本一些放在GC堆里的元数据会搬到GC管理之外的堆空间里。所以上面描述的实现会有些变化。具体会变成怎样还没真相。
至于其它JVM,其实运行时常量池想怎么组织都可以的,反正Java层面上看不出来JVM内部组织这些元数据的方式的差异。
原文地址: https://hllvm-group.iteye.com/group/topic/26412#post-187861
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)