java虚拟机:方法区

一、方法区简介

方法区,Method Area, 对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。

主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(比如spring使用IOC或者AOP创建bean时,或者使用cglib,反射的形式动态生成class信息等)。

注意:JDK 6 时,String等字符串常量的信息是置于方法区中的,但是到了JDK 7 时,已经移动到了Java堆。所以,方法区也好,Java堆也罢,到底详细的保存了什么,其实没有具体定论,要结合不同的JVM版本来分析。

二、方法区的几个特点

  1. 方法区在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。
  2. jvm实现的设计者决定了类型信息的内部表现形式。如,多字节变量在类文件是以big-endian存储的,但在加载到方法区后,其存放形式由jvm根据不同的平台来具体定义。
  3. jvm在运行应用时要大量使用存储在方法区中的类型信息。在类型信息的表示上,设计者除了要尽可能提高应用的运行效率外,还要考虑空间问题。根据不同的需求,jvm的实现者可以在时间和空间上追求一种平衡。
  4. 因为方法区是被所有线程共享的,所以必须考虑数据的线程安全。假如两个线程都在试图找lava的类,在lava类还没有被加载的情况下,只应该有一个线程去加载,而另一个线程等待。
  5. 方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。同样方法区也不必是连续的。方法区可以在堆(甚至是虚拟机自己的堆)中分配。jvm可以允许用户和程序指定方法区的初始大小,最小和最大尺寸。
  6. 方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展java程序,一些类也会成为垃圾。jvm可以回收一个未被引用类所占的空间,以使方法区的空间最小。

三、方法区的结构信息

1)  类型信息

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

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

2) 常量池

jvm为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,integer, 和floating point常量)和对类型,域和方法的符号引用。池中的数据项像数组项一样,是通过索引访问的。

3)域信息

jvm必须在方法区中保存类型的所有域的相关信息以及域的声明顺序,域的相关信息包括:

  • 域名
  • 域类型
  • 域修饰符(public, private, protected,static,final,volatile, transient的某个子集)

4)方法信息

jvm必须保存所有方法的以下信息,同样域信息一样包括声明顺序

  • 方法名
  • 方法的返回类型(或 void)
  • 方法参数的数量和类型(有序的)
  • 方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)除了abstract和native方法外,其他方法还有保存方法的字节码(bytecodes)操作数栈和方法栈帧(堆栈以帧为单位保存线程的状态)的局部变量区的大小
  • 异常表

5)类变量与类常量

类变量(Class Variables:就是类的静态变量,它只与类相关,所以称为类变量)

类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在jvm使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。

常量(被声明为final的类变量)的处理方法则不同,每个常量都会在常量池中有一个拷贝。non-final类变量被存储在声明它的类信息内,而final类被存储在所有使用它的类信息内。

6)对类加载器的引用

jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。jvm在动态链接的时候需要这个信息。当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的。这对jvm区分名字空间的方式是至关重要的。

7)对Class类的引用

jvm为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。

你可以通过Class类的一个静态方法得到这个实例的引用:public static Class forName(String className);

假如你调用forName("java.lang.Object"),你会得到与java.lang.Object对应的类对象。你甚至可以通过这个函数得到任何包中的任何已加载的类引用,只要这个类能够被加载到当前的名字空间。如果jvm不能把类加载到当前名字空间,forName就会抛出ClassNotFoundException。

也可以通过任一对象的getClass()函数得到类对象的引用,getClass被声明在Object类中:public final Class getClass();

例如,假如你有一个java.lang.Integer的对象引用,可以激活getClass()得到对应的类引用。

通过类对象的引用,你可以在运行中获得相应类存储在方法区中的类型信息,下面是一些Class类提供的方法:

  • public String getName();
  • public Class getSuperClass();
  • public boolean isInterface();
  • public Class[] getInterfaces();
  • public ClassLoader getClassLoader();

这些方法仅能返回已加载类的信息。getName()返回类的完整名,getSuperClass()返回父类的类对象,isInterface ()判断是否是接口。getInterfaces()返回一组类对象,每个类对象对应一个直接父接口。如果没有,则返回一个长度为零的数组。

getClassLoader()返回类加载器的引用,如果是由启动类加载器加载的则返回null。所有的这些信息都直接从方法区中获得。

8)方法表

为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,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变量设为初始值,另外一条指令会用这个引用激活Lava对象的flow()方法。

解释:符号引用与直接引用

1、符号引用(Symbolic References):

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

2、直接引用:

(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)

(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

(3)一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

时间: 2024-11-08 21:13:59

java虚拟机:方法区的相关文章

java虚拟机 jvm java堆 方法区 java栈

java堆是java应用程序最密切的内存空间. 差点儿全部的对象都存在堆中.java堆全然自己主动化管理,通过垃圾回收机制,垃圾对象会自己主动清理.不须要显式释放. 依据java垃圾回收机制的不同.java堆可能有不同的结构. 最常见的是将整个java堆分为新生代和老年代.跟人类几乎相同了.老龄化就有可能找上帝去了,新生代存放新对象或者年龄不大的对象,老年代存放老年对象.新生代有可能分为eden区.s0区和s1区,s0区和s1区也被称之为 from 到to区域.他们是两块大小相等能够互换的内存空

java虚拟机存储区

方法区和堆区是数据共享区. 栈区:数据不共享.方法参数.局部变量.参与运算的中间结果.返回值等等都在栈区中. 堆区:数据共享.存放对象. 方法区存放类型信息,类型信息包括:字段信息.方法信息.该类型的常量池.类变量.一个到类ClassLoader的引用,一个到Class类的引用.这部分数据是共享的,是一个Java虚拟机实例或者说一个Java程序共享的. 当一个类被多个不同的ClassLoader加载的时候,需要对ClassLoader标识.同时也要标识一个Class类,他的完全限定名. publ

java虚拟机内存区常用名词解释

虚拟机内存区常见名词: 栈帧(Frames): 栈帧是用来存储数据和部分结果,以及执行动态链接.方法返回值和异常信息的. 每次方法的调用都会生成一个栈帧,然后将该栈帧进行压栈.当方法结束后,该栈帧将会出栈.被销毁. 无论是正常结束还是异常结束. 每个栈帧都是自己的局部变量表.操作数栈.以及对当前方法类的运行时常量池的引用. 由当前线程创建的帧不能被其它线程引用. 局部变量(Local Variables): 每个栈帧包含一个叫局部变量的变量数组,它的长度在编译的时候确定. 单个局部变量可以保存类

深入Java虚拟机(程序运行)

引子:打开黑匣子 心中有数 老实说,对于C++的整个编译运行过程,我并没有全面的了解,好几次被问住了,看来是汇编没有学好,但是在看完<深入Java虚拟机>之后,对于Java代码到运行的每一个细节,有了更全面的认识. 描述一下整体的流程:程序员根据Java API编写Java程序,各种类文件,用一个Java编译器编译Java程序为class文件,class文件通过一定的分发方式被Java虚拟机装载.连接.初始化,变成Java虚拟机方法区的Class类,然后被实例化为堆区的对象,随着虚拟机的执行,

jvm java虚拟机 新生代的配置

版权声明:本文为博主原创文章,未经博主允许不得转载.不经过允许copy,讲追究法律责任,欢迎加入我们的学习提升群523988350,可以相互交流 目录(?)[+] 111 -Xmn参数 112 第一种情况-Xmx20m -Xms20m -Xmn1m -XXSurvivorRatio2 -XXPrintGCDetails 113 第二种情况-Xmx20m -Xms20m -Xmn7m -XXSurvivorRatio2 -XXPrintGCDetails 114 第三种情况-Xmx20m -Xms

对Java虚拟机理解

深入理解Java虚拟机 Java技术体系 Java体系分为四个平台 Java card 运行在小内存上的 Java ME 运行在手机上 Java SE 完整Java 核心api JavaEE 支持使用多层架构的企业 JVM自身的物理结构 Java 代码编译和执行的整个过程 Java 编译的过程 Java字节码执行的过程  Java 代码编译和执行有下面三个过程 Java 源码编译 类加载机制' 类执行机制 下面就分别对着三个过程进行详细的介绍 Java源码编译机制 分析输入符号表 注解处理 语义

java虚拟机学习-JVM内存管理:深入Java内存区域与OOM(3)

概述 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任. 对于Java程序员来说,不需要在为每一个new操作去写配对的delete/free,不容易出现内容泄漏和内存溢出错误,看起来由JVM管理内存一切都很美好.不过,也正是因为Java程序员把内存控制的

Java 虚拟机面试题全面解析(干货)

Java 虚拟机面试题全面解析(干货) 本文固定链接:https://www.zybuluo.com/Yano/note/321063 本文 PDF 下载:http://download.csdn.net/detail/yano_nankai/9469648 LeetCode题解:https://github.com/LjyYano/LeetCode 我的博客:http://blog.csdn.net/yano_nankai 周志明著的<深入理解 Java 虚拟机>的干货~如有错误,欢迎指出

JAVA虚拟机:垃圾回收策略及算法

java虚拟机中的程序计数器区.虚拟机栈区.本地方法栈区3个区域是随着线程的创建而创建,随着线程的结束而结束时,内存自然得到回收,所以这三个区域不需要过多考虑内存的回收问题. java虚拟机中的方法区和虚拟机堆区2个区是所有线程共享的区域,不同的接口或类需要的内存不同,且方法区和堆区往往是在程序运行期间进行内存动态分配或回收.GC回收器的使用范围就是对这两个区域的定义. 虚拟机堆区垃圾回收策略:GC回收器在回收内存之前,首先要知道哪些对象可以回收,即“死去”的对象是可以回收的:哪些对象不能回收,