天天看点

JVM - 运行时数据区 - 方法区

1. 栈,堆,方法区的交互关系

JVM - 运行时数据区 - 方法区

2. 方法区的基本理解

方法区是线程共享的内存区域,在Java规范中把方法区作为堆的一个逻辑部分,方法区的大小决定了系统可以保存多少类,如果加载了大量类可能会导致

OOM

方法区用来存储已经被虚拟机加载的类信息,常量,静态变量,编译器编译后的代码缓存等数据

2.1 方法区会存在内存溢出的问题

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:

java.lang.OutOfMemoryError:PermGen space

或者

java.lang,OutOfMemoryError:Metaspace

,比如:

  • 加载大量的第三方jar包;
  • Tomcat部署的工程过多;
  • 大量动态生成反射类;

2.2 内存结构详解

方法区用来存储已经被虚拟机加载的类信息,常量,静态变量,编译器编译后的代码缓存等数据

类型信息

对每个加载的类型( 类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

① 这个类型的完整有效名称(全名=包名.类名)

② 这个类型直接父类的完整有效名(对于interface或是java. lang.Object,都没有父类)

③ 这个类型的修饰符(public, abstract, final的某个子集)

④ 这个类型直接接口的一个有序列表

域信息(成员变量)

  • JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
  • 域的相关信息包括:域名称、 域类型、域修饰符(public, private, protected, static, final, volatile, transient的某个子集)

方法信息

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

  • 方法名称。
  • 方法的返回类型(或void)。
  • 方法参数的数量和类型(按顺序)。
  • 方法的修饰符(public, private, protected, static, final, synchronized, native , abstract的一个子集)。
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native 方法除外)。
  • 异常表( abstract和native方法除外),每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

关于静态变量

none - final

的类变量

静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分;类变量被类的所有实例共享,即使没有类实例时也可以访问

public class MethodAreaTest{
	public static void main(String[] args){
		Order order = null;
		order.hello();//并不会有空指针的异常 可以访问到
		System.out.println(order.count);
	}	
}
class Order{
	public static int count = 1;
	public static void hello(){
		System.out.println("hello");
	}
}
           

final

全局常量

被声明为

fianl

的类变量的处理方法则不同,每个全局常量在编译的时候就被分配了,看下面的代码

public class TestClass3 {
    public static int count = 1;
    public static final int number = 2;
}

           

反编译的部分结果如下

JVM - 运行时数据区 - 方法区

可以看到常量在类被编译成字节码的时候就被赋值了,而非

final

的类变量没有,是在类加载的过程才进行初始化

运行时常量池

Runtime Constant Pool

运行时常量池是方法区的一部分,

Class

文件中除了包括版本,字段,方法,接口等字段以外,还有一项信息就是常量池(Constant Pool Table)用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载进入方法区的运行时常量池中存放(注意区分常量池和运行时常量池)

JVM为每个已加载的类型(类或接口)都维护一个常量池

  • 常量池:放在二进制字节码文件,就是一张表,之后类的方法定义中的虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等信息
  • 运行时常量池:当类被加载(加载到内存),他的常量池信息就会被放入运行是常量池,并把里面的符号地址变为真实地址

一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池;而这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池.

相对应

Class

文件的常量池来说,运行时常量池的一个重要特性就是动态性,Java语言并不要求常量一定是在编译期间产生的,也就是并非预先置入

Class

文件中的常量池的内容才能进入方法区运行时常量池,运行期间也能将新的常量放入池中,比如

String

intern

方法.

串池

StringTable

StringTable

是运行时常量池的一部分,是

String

的享元模式的实现方式

常量池中的信息,都会被加载到运行时常量池中,但还没有变为java对象,当使用到时,会生成对象(延迟实例化);对于字符串来说,我们要在一个java程序中要使用要大量的字符串对象,而很多字符串对象的值都一样我们也把他们当作常量来使用,如果每次都分别创建字符串对象那么完全没有必要,所以我们要利用之前已经创建的字符串对象

以字面量直接赋值的

String

对象会被放到串池,下次如果有相同的字符串直接去串池中拿就行了,而不用生成新的对象

2.2 内存结构演进

jdk1.6

的内存结构中,使用永久代来实现方法区(对于Hotspots),静态变量放在永久代上

本质上,方法区和永久代并不等价。仅是对hotSpot而言的。《java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit/IBM J9中不存在永久代的概念

在jdk1.7时还有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中;

在1.8方法区不再由

JVM

管理而是放在了元空间中,元空间放到本地内存中,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍留在堆空间

版本 改进
jdk1.6及以前 有永久代,静态变量存放在永久代上
jdk1.7 有永久代,但已经逐步去永久代,字符串常量池,静态变量被移除,保存在堆中
jdk1.8及以后 无永久代,类型信息,字段,方法信息,常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆中
JVM - 运行时数据区 - 方法区
JVM - 运行时数据区 - 方法区
JVM - 运行时数据区 - 方法区

做这样元空间替换永久代的动机有两点:

元空间和永久代的区别

① 为永久代设置空间大小是很难确定的;

永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M。永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误

在某些场景下,如果动态加载类过多,会导致

Perm

区的

OOM

,而元空间和永久代的区别就在于:元空间并不限制在虚拟机,而是使用本地内存,因此元空间的大小仅受本地内存的限制

② 对永久代的调优是很困难的:垃圾回收的判断麻烦

类的元数据信息转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。

此外,在HotSpot中的每个垃圾收集器需要专门的代码来处理存储在PermGen中的类的元数据信息。从PermGen分离类的元数据信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空间,因此Metaspace和Java Heap可以无缝的管理,而且简化了FullGC的过程,以至将来可以并行的对元数据信息进行垃圾收集,而没有GC暂停

上面说到在jdk 1.6,串池

StringTable

被放到了永久代中,而在jdk1.8则是放到了堆中,为什么要做这个改变呢?

这个和

JVM

的垃圾回收机制有关,对于永久代来说,他的垃圾回收是发生在

full GC

,垃圾回收的几率很小,而堆的垃圾回收几率更多,为了尽快回收大量不用的

String

对象,才做出这个改变

3. 方法区的大小设置

jdk7及以前: 永久代
-XX:PermSize来设置永久代初始分配空间 默认值是20.75M
-XX:MaxPermSize来设置永久代的最大可分配空间 32位虚拟机默认是64M 64位虚拟机是82M

jdk8及以后: 元空间
-XX:MetaSpaceSize来设置永久代初始分配空间 默认值是20.75M
-XX:MaxMetaSpaceSize来设置永久代的最大可分配空间 32位虚拟机默认是64M 64位虚拟机是82M
           

继续阅读