1、方法区、永久代和元空间(Metaspace)到底要怎么理解
-
可以将方法区理解为接口,永久代和元空间是方法区的不同的实现方式。
-
元空间与永久代的最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存,因此它可以有更大的空间,防止内存溢出。
-
方法区是一块独立于java堆的内存空间
2、堆、栈、方法区的交互关系
3、设置方法区大小的参数
- 元数据区大小可以使用参数
-XX : MetaspaceSize
和-XX : MaxMetaspaceSize
指定。这个依赖于平台,因为是本地内存。 - 默认值依赖于平台。windows下,
-XX :MetaspaceSize
是21M,-XX:MaxMetaspaceSize
的值是-1,即没有限制。 - 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError: Metaspace
-XX :MetaspaceSize:
设置初始的元空间大小。对于一个64位的服务器端JVM来说, 其默认的
-XX:MetaspaceSize
值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活) ,然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize
时,适当提高该值。如果释放空间过多,则适当降低该值。- 如果初始化的高水位线设置过低,上述高水位线调 整情况会发生很多次。通过垃圾回 收器的日志可以观察到FullGC多次调用。为了避免频繁地GC,建议将
- XX:MetaspaceSize
设置为一个相对较高的值。
eg: -XX : MetaspaceSize=100M
注意有等号
4、方法区的内部结构
《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:
它用于存储已被虚拟机加载的 类型信息(类、枚举、接口、注解等信息)、常量、静态变量、即时编译器编译后的代码缓存等。
4.1方法区final变量的说明
non-final的类变量:就是static的成员变量。
final的类变量:就是static final的成员变量。
static final与non-final区别:初始化的时间不同,non-final的类变量在类加载的第二个阶段(链接阶段)的准备阶段被赋默认的初始值,然后再类加载的第三个阶段(初始化阶段)被显示初始化(也就是赋值为代码中写的值)。
比如: 定义一个成员变量a, public static int a = 7;
a在链接阶段的准备阶段被赋默认值0;然后再初始化阶段被显示初始化为7。
final的类变量是在编译阶段就被显示初始化了。
比如:定义一个成员变量, public static final int a = 7; ,a在代码被编译成字节码文件的时候就被赋值为7了。
eg:
代码:
public class MethodArea {
public static int count = 1;
public static final int number = 2;
public MethodArea() {
}
public static void main(String[] args) {
System.out.println("hello MethodArea");
}
}
编译完,执行指令:javap -v -p MethodArea.class > test1.txt
,打开test1查看内容:
4.2运行时常量池
字节码(.class文件 )的常量池加载到方法区就是运行时常量池,运行时常量池是方法区的一部分,在理解运行时常量池之前我们先理解下常量池。
4.2.1常量池
常量池在字节码文件中,如下图:
解释字面量:
1.文本字符串
2.八种基本类型的值
3.被声明为final的常量等;
Public static String str= “测试方法的内部结构”;
Public int num=10;
其中的“测试方法的内部结构”和10就是字面量
解释符号引用:
1.类和方法的全限定名
2.字段的名称和描述符
3.方法的名称和描述符
4.2.2为什么需要提供一个常量池呢
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式可以存到常量池,这个字节码包含了指向常量池的引用(“#” 就是那个引用)。在动态链接的时候会用到运行时常量池。
eg:上面的话如果不理解且看下面的分析:
(1)代码
public class MethodArea {
public static void main(String[] args) {
System.out.println("hello MethodArea");
System.out.println("hello MethodArea");
}
}
(2)执行指令javap -v -p MethodArea.class > test1.txt
,然后我们看下里面的内容:
常量池:
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello MethodArea
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/jvm/methodArea/MethodArea
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/jvm/methodArea/MethodArea;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 MethodArea.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello MethodArea
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/jvm/methodArea/MethodArea
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
main方法:
常量池中#23存入了字符串"hello MethodArea", 每一个#代表常量池的引用,我们分析字节码3: ldc #3
,引用#3,于是到常量池中找到 #3 = String #23
,然后#23找到#23 = Utf8 hello MethodArea
,说明常量池中存入了大量的数据(这里的hello MethodArea就是上面提出来的字面量)。
通过下面的字节码我们可以看出:他们对常量池里的数据进行了引用,且看3和11都引用了常量池中的#3,说明常量池中存入的字面量“hello Method”被两个地方引用了,如果不放在常量池,而是将数据放在字节码后边,我们发现每个指令后边都加载了字面量“hello Method”,这样就会占用了大量的空间
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello MethodArea
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #3 // String hello MethodArea
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
通过上面的分析我们能理解了常量池设计的妙处了吧,就好比常量池是茶米油盐酱醋、各种原食材,每一个方法就好比是做好的菜,做菜的时候需要去用到这些调料。
4.2.3 常量池中有什么
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
常量池,可以看成一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
4.2.4 运行时常量池说明
-
运行时常量池( Runtime Constant Poo1)是方法区的一部分。
-
常量池表( Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
-
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
-
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
-
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
➢运行时常量池,相对于class文件常量池的另一重要特征是:具备动态性。
eg: String.intern()如果string在常量池没有就又放进去。 -
运行时常量池类似于传统编程语言中的符号表(symbol table) ,但是它所包含的数,据却比符号表要更加丰富一些。
-
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常。
5、如何解决OOM
- 要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具.(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(MemoryOverflow )
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
- 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms) ,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。