1. 栈,堆,方法区的交互关系
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;
}
反编译的部分结果如下
可以看到常量在类被编译成字节码的时候就被赋值了,而非
final
的类变量没有,是在类加载的过程才进行初始化
运行时常量池 Runtime Constant Pool
Runtime Constant Pool
运行时常量池是方法区的一部分,
Class
文件中除了包括版本,字段,方法,接口等字段以外,还有一项信息就是常量池(Constant Pool Table)用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载进入方法区的运行时常量池中存放(注意区分常量池和运行时常量池)
JVM为每个已加载的类型(类或接口)都维护一个常量池
- 常量池:放在二进制字节码文件,就是一张表,之后类的方法定义中的虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等信息
- 运行时常量池:当类被加载(加载到内存),他的常量池信息就会被放入运行是常量池,并把里面的符号地址变为真实地址
一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池;而这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池.
相对应
Class
文件的常量池来说,运行时常量池的一个重要特性就是动态性,Java语言并不要求常量一定是在编译期间产生的,也就是并非预先置入
Class
文件中的常量池的内容才能进入方法区运行时常量池,运行期间也能将新的常量放入池中,比如
String
的
intern
方法.
串池 StringTable
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启动之前通过在命令行设置参数-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