8. 方法区
8.1 栈,堆,方法区的交互关系
从线程共享与否的角度来看:
栈、堆、方法区的交互关系:
8.2 方法区的理解
概述:
· 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
· 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的
· 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
· 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多类,导致方法区溢出,虚拟机同样会抛出错误:
java.lang.OutOfMemoryError : PermGen space或者java.lang.OutOfMemoryError : Metaspace
· 关闭JVM就会释放这个区域的内存
· 加载大量的第三方的jar包;Tomcat部署给工程过多也会导致方法区OOM
HotSpot中方法区的演进:
· 在JDK及以前习惯上把方法区成为永久代,JDK8开始使用元空间取代永久代
· 本质上,方法区和永久代并不等价,仅是对于HotSpot而言的,对如何实现方法区,不做统一要求
HotSpot方法区演变图:
· 元空间的本质和永久代类似,都是JVM规范中方法区的实现,不过元空间与永久代的最大区别是:元空间不在虚拟机 设置的内存中,而是使用本地内存
· 永久代、元空间二者并不只是名字变了,内部结构也调整了
· 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM
8.3 设置方法区大小与OOM
设置方法区内存的大小:
· 方法区的大小不必是固定的,JVM可以根据应用的需要动态调整
· JDK7及以前:
>通过-XX :PermSize来设置永久代初始分配空间,默认值是20.75 M
>-XX : MaxPermSize来设定永久代最大可分配空间,32位机器默认是64 M ,64位机器默认是82 M
>当JVM加载的类信息容量超过了这个值,就会报异常OutOfMemoryError : PermGen space
· JDK8及以后:
>元数据区大小可以使用参数-XX :MetaspaceSize和-XX :MaxMetaspaceSize指定,替代 上述原有的两个参数
>默认值依赖于平台,Windows下,-XX :MetaspaceSize是21 M,-XX :MaxMetaspaceSize的值是-1,没有限制
>与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有可用系统内存,如果元数据区发生溢出,虚拟机
一样会抛出异常OutOfMemoryError : Metaspace
>-XX :MetaspaceSize,设置初始的元空间大小,对于一个64位的服务器端JVM来说,其默认的-XX :MetaspaceSize
值是21 M,这就是初始的高水位线,一旦触及这个水位线,Full GC就会被触发并卸载没用的类(即这些类对应的类加 载器不在存活),然后这个高水位线将会重置,新的高水位线值取决于GC后释放了多少元空间,如果释放的空间不足
那么在不超过MaxMetaspaceSize时,适当提高该值,如果释放的空间过多,则适当降低该值
>如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收日志可以观察到Full GC多次
调用,为了避免频繁的GC,建议将-XX :MetaspaceSize设置为一个相对较高的值
举例方法区溢出的例子:
public class OOMTest extends ClassLoader{
public static void main(String[] args){
int j = 0;
try{
OOMTest test = new OOMTest();
for(int i = 0;i<10000;i++){
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号1.8,权限修饰符,类名,包名,父类,接口
ClassWriter.visit(
Opcodes.v1_8,Opcodes.ACC_PUBLIC,"class"+i,null,"java/lang/Object",null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("class"+i,code,0,code.length);
j++
}
} finally {
System.out.println(j);
}
}
}
8.4 方法区的内部结构
方法区图示一:
方法区图示二:
(方法区用于存储已被虚拟机加载的类型信息<枚举,接口,类>,常量,静态变量,即时编译器编译后的代码缓存等)
类型信息:
· 对每个加载的类型(类class,接口interface,枚举enum,注解annotation),JVM必须在方法区中存储以下类型信息
>这个类型的完整有效名称(全名 = 包名.类名)
>这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
>这个类型的修饰符(public,abstract,final的某个子集)
>这个类型直接接口的一个有序列表
域(Field)信息:
· JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
· 域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
方法(Method)信息:
· 方法名称
· 方法的返回类型(或void)
· 方法参数的数量和类型(按顺序)
· 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
· 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
· 异常表(abstract和native方法除外),每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被
捕获的异常类的常量池索引
举例说明:
public class MethodInnerStrucTest extends Object implements Comparable<String>,Serializable{
//属性
public int num = 10;
private static String str = "测试方法的内部结构";
//构造器
//方法
public void test1(){
int count = 20;
System.out.println("count = " + count);
}
public static int test2(int cal){
int result = 0;
try{
int value = 30;
result = value/cal;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
@Override
public int compareTo(String o){ return 0; }
}
上述代码字节码在方法区的信息:
· 类型信息图:
(包含该类的权限,该类的包名,该类的父类及其父类包名,该类的实现接口及其接口包名)
· 域信息:
(包含该类的属性的修饰符,属性类型,按照属性顺序来声明)
· 方法(构造器)信息:
(包含方法的名称,返回值,形参,权限修饰符,操作数栈,局部变量表)
· 异常信息:
(包含异常表,存在于方法体中,可以利用程序计数器的偏移地址捕获异常)
non-final的类变量:
· 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
· 类变量被类的所有实例共享,即使没有类的实例也可以访问它
· 被声明为final的类变量的处理方法不同,每个全局常量在编译的时候就会被分配
代码举例:
public class MethodAreaTest{
public static void main(String[] args){
//即使没有实例化,输出照样为--->"hello!" & 1
Order order = null;
order.hello();
System.out.println(order.count);
}
}
class Order{
//static修饰的变量在类加载器的Linking(prepare阶段)默认初始化为0,在Initialization赋值为1
public static int count = 1;
//static final修饰的变量在编译的时候就被赋值为2了
public static final int number = 2;
public static void hello(){
System.out.println("hello!");
}
}
8.5 常量池
运行时常量池图:
简要概述:
· 方法区,内部包含了运行时常量池
· 字节码文件,内部包含了常量池
· 要弄清楚方法区,就需要理解清楚ClassFile,因为加载类的信息都在方法区
· 要弄清楚方法区的运行时常量池,就需要理解清楚ClassFile中的常量池
· 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息就是常量池表
(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
常量池中有什么? :
>几种在常量池内存储的数据类型包括:数量值、字符串值、类引用、字段引用、方法引用
>代码举例说明:
public class MethodAreaTest{
public static void main(String[] args){
Object obj = new Object();
}
}
Object obj = new Object();将会编译成如下字节码:
0: new #2 // Class java/lang/Object
1: dup
2: invokespecial #3 //Method java/lang/Object "<init>"() v
>总结:常量池可以看作为一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型
运行时常量池详细概述:
· 运行时常量池是方法区的一部分
· 常量池表是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区
的运行时常量池中
· JVM为每个已加载的类型(类或接口)都维护一个常量池,池中的数据项像数组项一样,是通过索引访问的
· 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法
或者字段引用,此时不再是常量池中的符号地址了,这里换位真实地址
>运行时常量池,相对于Class文件常量池的另一个重要特征是:具备动态性
· 运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却要比符号表更加丰富一些
· 当创建类或接口的运行时常量池时,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,则JVM
会抛出OutOfMemoryError异常
8.6 方法区的使用举例
代码:
public class MethodAreaDemo{
public static void main(String[] args){
int x = 500;
int y = 100;
int a = x/y;
int b = 50;
System.out.println(a + b);
}
}
上述代码字节码指令:
Code:
stack=3,locals=5,args_size=1
0: bipush 500
3: istore_1
4: bipush 100
6: istore_0
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 //Field java/lang/System.out:Ljava/io/PrintStream
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 //Method java/io/PrintStream.println:(Ⅰ)V
25: return
常量池:
图解:
8.7方法区的演进细节
概述:
· 首先明确,只有HotSpot才有永久代,BEA JRockit、IBM J9等来说,是不存在永久代的概念的,原则上如何实现方法区
属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一
· HotSpot中方法区的变化:
图示:
永久代为什么要被元空间替换:
· 因为永久代设置空间大小是很难确定的,在某些场景下,如果动态加载类过多,容易产生Perm和OOM
· 元空间和永久代的最大区别在于,元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受
本地内存限制
· 对永久代进行调优是很困难的
StringTable为什么要调整:
· JDK7中将StringTable放到了堆空间中,因为永久代的回收率很低,在full gc的时候才会触发,而full gc是老年代的空间
不足的时候才会触发,这就导致了StringTable回收率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致
永久代内存不足,放到堆里,能及时回收内存
静态变量放在哪里:
public class StaticObject{
static class Test{
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceof = new ObjectHolder();
void foo(){
ObjectHolder localObj = new ObjectHolder();
System.out.println("done");
}
}
private static class ObjectHolder{
}
public static void main(String[] args){
}
}
· new出来的对象全部放在对空间之中,而静态变量staticObj随着类的加载而加载存放在方法区,instanceof随着对象的实
例而存放在堆空间中。localObj则是存放在foo()方法栈帧的局部变量表中
8.8 方法区的垃圾回收
· 方法区的垃圾回收主要回收两个部分,常量池中废弃的常量和不再使用的类型
· 方法区内常量池中主要存放的两大类常量:字面量和符号引用,字面量比较接近Java语言层次的常量概念,如文本字符
串,被声明为final的常量值,而符号引用则属于编译原理方面的概念,包括下面三类常量
>1. 类和接口的全限定名
>2. 字段的名称和描述符
>3. 方法的名称和描述符
· HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收
· 回收废弃常量与回收Java堆中的对象非常类似
8.9 总结与常见面试题
总结图:
常见面试题:
· 百度:说一下JVM内存模型吧,有哪些区,分别是干什么的?
· 蚂蚁金服:Java8的内存分代改进/JVM内存分哪几个区,每个区的作用/Eden和Survivor的比例分配/栈和堆的区别
· 小米:JVM内存分区,为什么要有新生代和老年代
· 字节跳动:Java内存分区/讲讲JVM运行时数据区/什么时候进入老年代
· 京东:JVM内存结构,Eden和Survivor比例/JVM为什么要分新生代和老年代持久代,新生代为什么要分伊甸区和幸存区
· 天猫:JVM内存模型分区以及每个区的作用/JVM的内存模型中java8做了什么修改
· 拼多多:JVM内存分哪几个区,每个区的作用是什么
常量与回收Java堆中的对象非常类似