Hotpot Java虚拟机Class对象是在方法区仍是堆中

Class对象是存放在堆区的,不是方法区,这点不少人容易犯错。类的元数据(元数据并非类的Class对象。Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的。
方法区 
在一个JVM实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。
JVM实现的设计者决定了类型信息的内部表现形式。如,多字节变量在类文件是以big-endian存储的,但在加载到方法区后,其存放形式由jvm根据不一样的平台来具体定义。
JVM在运行应用时要大量使用存储在方法区中的类型信息。在类型信息的表示上,设计者除了要尽量提升应用的运行效率外,还要考虑空间问题。根据不一样的需求,JVM的实现者能够在时间和空间上追求一种平衡。
由于方法区是被全部线程共享的,因此必须考虑数据的线程安全。假如两个线程都在试图找lava的类,在lava类尚未被加载的状况下,只应该有一个线程去加载,而另外一个线程等待。
方法区的大小没必要是固定的,jvm能够根据应用的须要动态调整。一样方法区也没必要是连续的。方法区能够在堆(甚至是虚拟机本身的堆)中分配。jvm能够容许用户和程序指定方法区的初始大小,最小和最大尺寸。
方法区一样存在垃圾收集,由于经过用户定义的类加载器能够动态扩展java程序,一些类也会成为垃圾。jvm能够回收一个未被引用类所占的空间,以使方法区的空间最小。java

类型信息 
对每一个加载的类型,jvm必须在方法区中存储如下类型信息: 
一 这个类型的完整有效名 
二 这个类型直接父类的完整有效名(除非这个类型是interface或是 
java.lang.Object,两种状况下都没有父类) 
三 这个类型的修饰符(public,abstract, final的某个子集) 
四 这个类型直接接口的一个有序列表算法

类型名称在java类文件和jvm中都以完整有效名出现。在java源代码中,完整有效名由类的所属包名称加一个”.”,再加上类名 
组成。例如,类Object的所属包为java.lang,那它的完整名称为java.lang.Object,但在类文件里,全部的”.”都被 
斜杠“/”代替,就成为java/lang/Object。完整有效名在方法区中的表示根据不一样的实现而不一样。数组

除了以上的基本信息外,jvm还要为每一个类型保存如下信息: 
类型的常量池( constant pool) 
域(Field)信息 
方法(Method)信息 
除了常量外的全部静态(static)变量安全

常量池 
jvm为每一个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string, 
integer, 和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项同样,是经过索引访问的。 
由于常量池存储了一个类型所使用到的全部类型,域和方法的符号引用,因此它在java程序的动态连接中起了核心的做用。网络

域信息 
jvm必须在方法区中保存类型的全部域的相关信息以及域的声明顺序, 
域的相关信息包括: 
域名 
域类型 
域修饰符(public, private, protected,static,final volatile, transient的某个子集)
方法信息 
jvm必须保存全部方法的如下信息,一样域信息同样包括声明顺序 
方法名 
方法的返回类型(或 void) 
方法参数的数量和类型(有序的) 
方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)除了abstract和native方法外,其余方法还有保存方法的字节码(bytecodes)操做数栈和方法栈帧的局部变量区的大小 
异常表
类变量( Class Variables 译者:就是类的静态变量,它只与类相关,因此称为类变量 ) 
类变量被类的全部实例共享,即便没有类实例时你也能够访问它。这些变量只与类相关,因此在方法区中,它们成为类数据在逻辑上的一部分。在jvm使用一个类以前,它必须在方法区中为每一个non-final类变量分配空间。
常量(被声明为final的类变量)的处理方法则不一样,每一个常量都会在常量池中有一个拷贝。non-final类变量被存储在声明它的 
类信息内,而final类被存储在全部使用它的类信息内。
对类加载器的引用 
jvm必须知道一个类型是由启动加载器加载的仍是由用户类加载器加载的。若是一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用做为类型信息的一部分保存在方法区中。
jvm在动态连接的时候须要这个信息。当解析一个类型到另外一个类型的引用的时候,jvm须要保证这两个类型的类加载器是相同的。这对jvm区分名字空间的方式是相当重要的。
对Class类的引用 
jvm为每一个加载的类型(译者:包括类和接口)都建立一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。
你能够经过Class类的一个静态方法获得这个实例的引用// A method declared in class java.lang.Class: 
public static Class forName(String className);
假如你调用forName(“java.lang.Object”),你会获得与java.lang.Object对应的类对象。你甚至能够经过这个函数 获得任何包中的任何已加载的类引用,只要这个类可以被加载到当前的名字空间。若是jvm不能把类加载到当前名字空间,forName就会抛出ClassNotFoundException。 
(译者:熟悉COM的朋友必定会想到,在COM中也有一个称为 类对象(Class Object)的东东,这个类对象主要 是实现一种工厂模式,而java因为有了jvm这个中间 层,类对象能够很方便的提供更多的信息。这两种类对象 都是Singleton的)
也能够经过任一对象的getClass()函数获得类对象的引用,getClass被声明在Object类中: 
// A method declared in class java.lang.Object: 
public final Class getClass(); 
例如,假如你有一个java.lang.Integer的对象引用,能够激活getClass()获得对应的类引用。
经过类对象的引用,你能够在运行中得到相应类存储在方法区中的类型信息,下面是一些Class类提供的方法: 
// Some of the methods declared in class java.lang.Class: 
public String getName(); 
public Class getSuperClass(); 
public boolean isInterface(); 
public Class[] getInterfaces(); 
public ClassLoader getClassLoader();数据结构

这些方法仅能返回已加载类的信息。getName()返回类的完整名,getSuperClass()返回父类的类对象,isInterface()判断是不是接口。getInterfaces()返回一组类对象,每一个类对象对应一个直接父接口。若是没有,则返回一个长度为零的数组。 
getClassLoader()返回类加载器的引用,若是是由启动类加载器加载的则返回null。全部的这些信息都直接从方法区中得到。jvm

方法表 
为了提升访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还能够添加一些其余的数据结构,如方法表。jvm对每一个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法)。jvm能够经过方法表快速激活实例方法。(译者:这里的方法表与C++中的虚拟函数表同样,但java方法全都 是virtual的,天然也不用虚拟二字了。正像java宣称没有 指针了,其实java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的,我的认为java的设计者 始终是把安全放在效率之上的,全部java才更适合于网络开发)函数

举一个例子 
为了显示jvm如何使用方法区中的信息,咱们据一个例子,咱们 
看下面这个类: 
class Lava { 
private int speed = 5; // 5 kilometers per hour 
void flow() { 

}线程

class Volcano { 
public static void main(String[] args) { 
Lava lava = new Lava(); 
lava.flow(); 


下面咱们描述一下main()方法的第一条指令的字节码是如何被执行的。不一样的jvm实现的差异很大,这里只是其中之一。设计

为了运行这个程序,你以某种方式把“Volcano”传给了jvm。有了这个名字,jvm找到了这个类文件(Volcano.class)并读入,它从类文件提取了类型信息并放在了方法区中,经过解析存在方法区中的字节码,jvm激活了main()方法,在执行时,jvm保持了一个指向当前类(Volcano)常量池的指针。 注意jvm在尚未加载Lava类的时候就已经开始执行了。正像大多数的jvm同样,不会等全部类都加载了之后才开始执行,它只会在须要的时候才加载。 main()的第一条指令告知jvm为列在常量池第一项的类分配足够的内存。jvm使用指向Volcano常量池的指针找到第一项,发现是一个对Lava类的符号引用,而后它就检查方法区看lava是否已经被加载了。 这个符号引用仅仅是类lava的完整有效名”lava“。这里咱们看到为了jvm能尽快从一个名称找到一个类,一个良好的数据结构是多么重要。这里jvm的实现者能够采用各类方法,如hash表,查找树等等。一样的算法能够用于Class类的forName()的实现。 当jvm发现尚未加载过一个称为”Lava”的类,它就开始查找并加载类文件”Lava.class”。它从类文件中抽取类型信息并放在了方法区中。 jvm因而以一个直接指向方法区lava类的指针替换了常量池第一项的符号引用。之后就能够用这个指针快速的找到lava类了。而这个替换过程称为常量池解析(constant pool resolution)。在这里咱们替换的是一个native指针。 jvm终于开始为新的lava对象分配空间了。此次,jvm仍然须要方法区中的信息。它使用指向lava数据的指针(刚才指向volcano常量池第一项的指针)找到一个lava对象究竟须要多少空间。 jvm总可以从存储在方法区中的类型信息知道某类型对象须要的空间。但一个对象在不一样的jvm中可能须要不一样的空间,并且它的空间分布也是不一样的。(译者:这与在C++中,不一样的编译器也有不一样的对象模型是一个道理) 一旦jvm知道了一个Lava对象所要的空间,它就在堆上分配这个空间并把这个实例的变量speed初始化为缺省值0。假如lava的父对象也有实例变量,则也会初始化。 当把新生成的lava对象的引用压到栈中,第一条指令也结束了。下面的指令利用这个引用激活java代码把speed变量设为初始值,5。另一条指令会用这个引用激活Lava对象的flow()方法。  

相关文章
相关标签/搜索