方法区
方法区是虚拟机规范中的抽象概念,原文如下:
什么是虚拟机规范,换句话说,无论你用的虚拟机是HotSpot还是JRockit等等,他们的具体实现中,必须要存在方法区这个结构,但具体的实现可以灵活发挥,打个比方,规范好比是造一座房子的图纸,其中规定了必须要有书房这个房间,建造商拿到图纸以后,需要在限定的条件下去设计这个书房,如何进行设计建造,这就是具体的实现方式。我们就将目光聚焦到最主流的HotSpot虚拟机上。在JDK8以前,HotSpot的开发者,将面向堆的分代设计复用在了方法区上。
他们使用“永久代”来作为HotSpot上的方法区的实现。但是后来发现这种设计并不好,所以从JDK8开始借鉴了一些JRockit的设计思路,使用了元空间来代替“永久代”作为新的实现方式。总结来说,“方法区”是抽象,“永久代”和“元空间”是实现。
那么为什么要用元空间这种本地内存的方式来代替“永久代”呢?
因为永久代由两个主要的缺点:
第一:就是可能引起内存溢出,“永久代”的大小设置为多少,可以通过启动数来指定,但是其中存储的数据大小是动态变化的,若阈值设置的太小则可能导致频繁的类卸载或者说内存溢出问题。设置的太大有可能会存在空间浪费,所以将会由此出现一些调优的问题。
第二:就是“永久代”本身设计复杂,“永久代”本身是面向堆来设计的,所以存储在“永久代”内的对象不是内存连续的,需要通过额外的存储信息以及实现额外的对象查找机制来定位对象,所以比较麻烦。虚拟机设计团队之所以一开始会使用永久代这种方式来实现方法区,是为了进行一定程度的代码复用。但是后来发现存在一些问题,以上两个缺点,对于方法区来说并不是不可避免的,所以目前使用基于直接内存的元空间来代替“永久代”就不会有这些问题。
理清了概念,下面看看方法区内到底存储了一些什么东西,在类加载部分有说到,类加载的第一个阶段叫做“加载”,在这个阶段内,虚拟机将会读取被编译的Class文件,生成Class对象,Class对象存储了一些类型信息。这些信息就是存储在方法区内的,这里所说的类型信息就是,诸如像“类的签名”、“属性”、“方法”既然类型信息是从class文件读取的,那么我们就写个demo编译成字节码以后,来看看其中具体由哪些类型信息。
源码:
public class Test extends Lock {
private int num;
private String str;
public Test() {
}
public Test(String str) {
this.str = str;
}
public String test(){
return this.str;
}
}
通过javap反编译class之后,可以得到可读性比较好的字节码,字节码中展示了当前类的全限定名以及父类或者接口的全限定名。下面两行minor version 和major version 代表当前JDK的主版本号和次版本号。像这里,52其实表示的就是1.8版本。ACC PUBLIC 代表当前类的访问权限类型是public,ACC SUPER其实不用理会,它是一个为了实现多态的补丁。
我们在源码中定义了两个属性,基本类型int的属性num和复杂类型是String的属性str。这部分 我们可以看到,在字节码中,几乎没有什么理解歧义,已经表达的很清楚了,源码中还存在三个方法,分别是无参构造方法,含参构造方法,以及一个自定义函数,这一块的字节码其实更多体现的是虚拟机栈的相关内容,因为方法调用直接和虚拟机栈有关。
这里有一个细节,我们看到bytecode index 为1的这行字节码,调用了invokespecial指令。可以看得出来,这里的意图是首先调用父类的构造方法。
这就验证了大家熟知的“子类在构造对象时,默认先调用父类无参构造函数”的这一概念。另外我们又发现了一个名为LineNumberTable的这个变量,它的数据格式非常统一,呈现出了 line xx:xx的形式,那这是用来干什么的呢?
这其实是表示源码行数和bytecodeindex之间的映射,这也是我们在debug的时候,为什么程序能够精准的停留在源码的断点处。
在字节码中,和类型相关的信息不难理解,此外我们注意到字节码中存在名为“Cosrant Pool”的内容占据了大量的篇幅,下面我们就来看看
这个常量池,究竟有什么作用?
首先我们先来思考一个问题,从上层来看,大部分类都不是孤岛,他们之间存在着相互调用的关系。比方说我这里的test类就继承了lock类,拥有了lock的能力,此外还存在着String类的属性,就可以调用String的相关方法。
那么这些调用是如何实现的呢?
最简单的方式。如果类A的源码中调用了类B源码中的逻辑,那么在编译期间把类B的源码直接引入到类A一起编译,这样的话,也能够达到最终目的,但显然是不合理的,因为这回造成代码大量膨胀,想想都会觉得恐怖。比较合理的方式,就是通过类似指针的方式在类A的字节码中留下一个指针,指向想要调用的类B的字节码,这里指针就起到了“链接”的作用。这些内容其实属于类加载的部分。
上面说到的符号引用,既然是从字节码中加载进来的,那么在字节码中怎么体现呢?
Constant Pool (常量池)内的数据就体现了符号引用与一些其他的静态引用,需要注意的是,这里的“常量”和我们平时写代码时所谓的“常量”意义上不太一样,这里的常量池不是说仅仅用来存储源码中定义的那一些常量和字面量的,这里的常量池更像是一张链表,我们可以看到第一行和第二行分别对应了方法和属性所需要使用到的外部链接,第三行和第四行对应了当前类信息需要使用的外部链接。
运行时常量池
简单来说,运行时常量池存储着两大类数据。
第一种,是编译期间产生的,主要是字节码中定义的静态信息,比如:
由字节码生成的class对象(上面所说的Constant Pool就包含在内)
由字节码生成的字面量
第二种,运行期间产生的,这部分比较灵活,虚拟机开发者可以将必要的数据都放进去,比如:
运行时会将一部分符号引用转换为直接引用,那么这些直接引用可以存储进来。
常见的字符串常量池。
哪些信息可能会被垃圾回收器回收呢?
比如说通过类加载进入方法区的类型信息,当内存紧张的时候可能会对小部分类进行卸载,被卸载的类需要再次使用的时候,就需要再次重新加载。
再比如说上面提到的运行时常量池的这个字符串常量池,当内存紧张的时候,也会对其进行部分回收。
堆
从jvm的规范上来说,没有进行严格意义上的分区,只是从不同的角度去看,可以进行不同的逻辑划分,最主流最常见的就是从垃圾回收的角度对堆内存进行划分。