方法区
栈、堆、方法区的交互关系
- Person 类的 .class 信息存放在方法区中
- person 变量存放在 Java 栈的局部变量表中
- 真正的 person 对象存放在 Java 堆中
方法区的理解
方法区的位置
《java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。对于HotSpotJVM而言,方法区还有一个名称,叫做no-Heap (非堆) ,目的就是和堆分开,所以方法区可以看做是一个独立于java堆的内存空间
理解
方法区主要存放的是Class,而堆中存放的是实例化对象。可以理解为方法区中存放的是各种类的模板,当我们实例化对象的时候,就是从方法区中取出模板,然后复制一份到堆空间中,变量名则存放在栈的指向堆内存的地址。
-
方法区与堆区一样,是各个线程共享的区域
-
多个线程同时加载同一个类时,只能有一个线程加载该类,其他线程只能等待该线程加载完毕,然后直接使用。(类只能加载一次)
-
方法区在JVM启动的时候就被创建,和堆一样,他的内存空间也是可以在物理上不连续逻辑上连续
-
方法区的大小是可以改变的
-
方法区的大小决定了系统可以保存多少个类,如果系统定义的类太多了(加载大量的第三方的jar包,大量动态的生成反射类,Tomcat部署的工程过多),导致方法区溢出,JVM同样会抛出内存溢出异常
- java.lang.OutofMemoryError:PermGen space(JDK7之前)
- java.lang.OutOfMemoryError:Metaspace(JDK8之后)
HotSpot虚拟机演进过程
- 在JDK7以前习惯上把方法区叫做永久代,JDK8开始使用
元空间
替代了永久代。JDK8之后,元空间存放在堆外内存 - 永久代或元空间是方法区的具体实现
- 元空间与永久代不只是名称变了,内部结构也改变了:元空间不再虚拟机设置的内存中,而是使用本地内存
- 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常
设置方法区大小与OOM
JDK7 永久代
JVM参数-XX:Permsize=100m --XX:MaxPermsize=100m
- 通过-XX:Permsize来设置永久代初始分配空间。默认值是20.75M
- -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
- 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。
JDK8 元空间
JVM参数-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
- 使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定
- Windows下,-XX:MetaspaceSize 默认为21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。
- 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
- -XX:MetaspaceSize的默认值21M就是初始的高水位线,达到这个值,将会触发FullGC,卸载没用的类,然后重置高水位线,新的高水位线取决于GC后释放的空间,空间不足则提高,空间过多则下降
- 为了避免频繁的FullGC建议将-XX:MetaspaceSize设置一个较高的值
方法区内部结构
方法区存储数据
深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
查看方法 javap -v -p *.class
- 类型信息(类class、接口interface、枚举enum、注解annotation),包括 完整的完全限定名,父类,修饰符
- 方法信息,包括方法名,参数(数量,类型),返回类型,修饰符,方法的字节码,操作数栈,局部变量表,异常
- 域信息
- 静态变量
- 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
- 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
- 全局常量就是使用 static final 进行修饰
- 被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
public class Goods {
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 final int number = 2;
public static void hello() {
System.out.println("hello!");
}
}
// 打印结果
hello!
1
- 运行时常量池 ,方法区内部包含了运行时常量池
常量池
Constant pool:
#1 = Methodref #18.#52 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#53 // cn/sxt/java/MethodInnerStrucTest.num:I
#3 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #56 // java/lang/StringBuilder
#5 = Methodref #4.#52 // java/lang/StringBuilder."<init>":()V
#6 = String #57 // count =
#7 = Methodref #4.#58 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #4.#59 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#9 = Methodref #4.#60 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Methodref #61.#62 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #63 // java/lang/Exception
#12 = Methodref #11.#64 // java/lang/Exception.printStackTrace:()V
#13 = Class #65 // java/lang/String
#14 = Methodref #17.#66 // cn/sxt/java/MethodInnerStrucTest.compareTo:(Ljava/lang/String;)I
#15 = String #67 // 测试方法的内部结构
#16 = Fieldref #17.#68 // cn/sxt/java/MethodInnerStrucTest.str:Ljava/lang/String;
#17 = Class #69 // cn/sxt/java/MethodInnerStrucTest
#18 = Class #70 // java/lang/Object
#19 = Class #71 // java/lang/Comparable
#20 = Class #72 // java/io/Serializable
#21 = Utf8 num
#22 = Utf8 I
#23 = Utf8 str
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 <init>
#26 = Utf8 ()V
#27 = Utf8 Code
#28 = Utf8 LineNumberTable
#29 = Utf8 LocalVariableTable
#30 = Utf8 this
#31 = Utf8 Lcn/sxt/java/MethodInnerStrucTest;
#32 = Utf8 test1
#33 = Utf8 count
#34 = Utf8 test2
#35 = Utf8 (I)I
#36 = Utf8 value
#37 = Utf8 e
#38 = Utf8 Ljava/lang/Exception;
#39 = Utf8 cal
#40 = Utf8 result
#41 = Utf8 StackMapTable
#42 = Class #63 // java/lang/Exception
#43 = Utf8 compareTo
#44 = Utf8 (Ljava/lang/String;)I
#45 = Utf8 o
#46 = Utf8 (Ljava/lang/Object;)I
#47 = Utf8 <clinit>
#48 = Utf8 Signature
#49 = Utf8 Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
#50 = Utf8 SourceFile
#51 = Utf8 MethodInnerStrucTest.java
#52 = NameAndType #25:#26 // "<init>":()V
#53 = NameAndType #21:#22 // num:I
#54 = Class #73 // java/lang/System
#55 = NameAndType #74:#75 // out:Ljava/io/PrintStream;
#56 = Utf8 java/lang/StringBuilder
#57 = Utf8 count =
#58 = NameAndType #76:#77 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#59 = NameAndType #76:#78 // append:(I)Ljava/lang/StringBuilder;
#60 = NameAndType #79:#80 // toString:()Ljava/lang/String;
#61 = Class #81 // java/io/PrintStream
#62 = NameAndType #82:#83 // println:(Ljava/lang/String;)V
#63 = Utf8 java/lang/Exception
#64 = NameAndType #84:#26 // printStackTrace:()V
#65 = Utf8 java/lang/String
#66 = NameAndType #43:#44 // compareTo:(Ljava/lang/String;)I
#67 = Utf8 测试方法的内部结构
#68 = NameAndType #23:#24 // str:Ljava/lang/String;
#69 = Utf8 cn/sxt/java/MethodInnerStrucTest
#70 = Utf8 java/lang/Object
#71 = Utf8 java/lang/Comparable
#72 = Utf8 java/io/Serializable
#73 = Utf8 java/lang/System
#74 = Utf8 out
#75 = Utf8 Ljava/io/PrintStream;
#76 = Utf8 append
#77 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#78 = Utf8 (I)Ljava/lang/StringBuilder;
#79 = Utf8 toString
#80 = Utf8 ()Ljava/lang/String;
#81 = Utf8 java/io/PrintStream
#82 = Utf8 println
#83 = Utf8 (Ljava/lang/String;)V
#84 = Utf8 printStackTrace
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
方法区演变过程
只有Hotspot才有永久代
JDK 版本 | 演变细节 |
---|---|
JDK1.6及以前 | 有永久代(permanent generation),静态变量存储在永久代上 |
JDK1.7 | 有永久代,但已经逐步 “去永久代”,字符串常量池、静态变量从永久代中移除,保存在堆中 |
JDK1.8 | 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。 |
JDK6 方法区由永久代实现,使用的是JVM虚拟机的内存
JDK7 方法区由永久代实现,使用的是JVM虚拟机的内存,但是 字符串常量池、静态变量从永久代中移除,保存在堆中
JDK8及以后 方法区由元空间实现,使用物理机本地内存
为什么要用元空间
由于类的元数据分配在本地内存中,所以元空间的最大可分配空间就是系统可用内存空间。
- 为永久代设置空间大小是很难确定
- 对永久代进行调优很困难
字符串常量池
字符串常量池 StringTable 为什么要调整位置?
- JDK7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。
- 这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
方法区垃圾回收
方法区的垃圾回收主要是回收常量池中废弃的常量和不再使用的类型
-
只要常量池中的常量没有任何地方引用,就可以被回收
-
回收废弃的常量与回收Java堆中的对象相似
-
判断一个类型是否属于不再使用就比较麻烦,必须保证:
- 该类型所有的实例都被回收,即在java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收
- 对应该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
满足以上三个条件的类才会允许被回收(允许不一定是,还有很多判断)