1.堆栈、堆栈、方法区域交互
查看是否共享线程。
互动
2.理解方法领域
2.1正式文件
the Java virtual machine has a method area that is shared among all Java virtual machine threads . the method area is analogous to the storage area ff commea
the method area is created on virtual machine start-up。although The method area is logically part of The heap,Simple implementations may choose not to either garbage collect or compact it . this specification
a Java virtual machine implementation may provide the programmer or the user control over the initial size of the method area,as well as,In the
the following exceptional condition is associated with the method area :
if memory in the method area cannot be made available to satisfy an allocation request,the Java virtual machine throws an out of memory error。
请注意
2.2方法区域在哪里
《Java虚拟机规范》表示:“所有方法区域在逻辑上都是堆的一部分,但在某些简单的实现中,可能不会选择垃圾收集或压缩。
“但对于HotSpot JVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开所以,方法区看做是一块独立于Java堆的内存空间
2.3 方法区的基本理解
- 方法区和Java堆一样,是各个线程共享的内存区域
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OurOfMemoryError: PermGen space或者java.lang.OurOfMemoryError: Metaspace
- 加载大量的第三方jar包
- Tomcat部署的工程过多(30-50)
- 大量动态生成反射类
代码测试:
public class MethodAreaTest {
public static void main(String[] args) {
Sy("testing");
try {
T(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 执行后使用jvisualvm查看类加载数量:
可以看到加载了1530个类而且数量还在动态变化中
- 关闭JVM就会释放这个区域的内存
2.4 HotSpot中方法区的演进
- 在JDK7及以前,习惯上把方法区称为永久代。JDK8开始,使用元空间取代了永久代
In JDK8, classes metadata is now stored in the native heap and this space is called Metaspace.
- 在本质上,方法区和永久代并不等价。仅是对HotSpot虚拟机而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit / IBM J9中不存在永久代的概念。现在看来,当年使用永久代,并不是好的选择。导致Java程序更容易OOM (超过-XX:MaxPermSize上限)
- 而到了JDK8, 终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间来代替
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存
- 永久代、元空间二者并不只是名字变了,内部结构也调整了
- 根据《Java虚拟机规范》规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常java.lang.OurOfMemoryError: Metaspace
3. 设置方法区大小与OOM
- 方法区的大小不必是固定的,JVM可以根据应用的需要动态调整
- JDK7及以前:
- 通过-XX:PermSize来设置永久代初始分配空间,默认值是20.75M;
- 通过-XX:MaxPermSize来设置永久代最大可分配空间,32位机器默认是64M,64位机器默认是82M。
- 当JVM加载的类信息容量超过了这个值,会报异常java.lang.OurOfMemoryError: PermGen space
- JDK8及以后:
- 元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数
-XX:MaxPermSize=size Sets the maximum permanent generation space size (in bytes). This option was deprecated in JDK 8, and superseded by the -XX:MaxMetaspaceSize o Sets the space (in bytes) allocated to the permanent generation that triggers a garbage collection if it is exceeded. This option was deprecated un JDK 8, and superseded by the -XX:MetaspaceSize option.
- 默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制
- 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常java.lang.OurOfMemoryError: Metaspace
- -XX:MetaspaceSize设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为21M,这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值
- 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值
JDK8 环境下代码测试
package com.na;
public class MethodAreaTest {
public static void main(String[] args) {
Sy("testing");
try {
T(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
设置运行参数-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m执行后,命令行查看如下:
$ jps
97123 Launcher
25059
97124 MethodAreaTest
97126 Jps
$ jinfo -flag MetaspaceSize 97124
-XX:MetaspaceSize=104857600
$ jinfo -flag MaxMetaspaceSize 97124
-XX:MaxMetaspaceSize=104857600
104857600/1024/1024 = 100m即为我们设置的运行参数。 而如果在JDK8 环境下设置运行参数-XX:PermSize=100m -XX:MaxPermSize=100m,则会输出如下提示信息:
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=100m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=100m; support was removed in 8.0
方法区OOM代码测试
package com.na;
import jdk.in;
import jdk.in;
public class MethodAreaOOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
MethodAreaOOMTest test = new MethodAreaOOMTest();
for (int i = 0; i < 10000; i++) {
// 创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
// 指明版本号,修饰符,类名,包名,父类,接口
cla, O, "Class" + i, null, "java/lang/Object", null);
// 返回byte[]
byte[] code = cla();
// 类的加载
("Class" + i, code, 0, code.length); // Class对象
j++;
}
} finally {
Sy(j);
}
}
}
运行参数:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m, 执行后输出如下:
8531
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.Cla(Native Method)
at java.lang.Cla(ClassLoader.java:763)
at java.lang.Cla(ClassLoader.java:642)
at com.na.MethodAreaOOMTest.main(MethodAreaOOMTest.java:20)
如何解决OOM ?
要解决OOM异常或者heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
- 如果是内存泄漏,可以进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置
- 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗
方法区的内部结构
4.1 方法区中存储什么
《深入理解Java虚拟机》一书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
- 类型信息。对每个加载的类型(类class,接口interface,枚举enum,注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或者是java.lang.Object,都没有父类)
- 这个类型的修饰符(public, abstract, final的某个子集)
- 这个类型直接接口的一个有序列表
- 域(Field)信息
- JVM必须在方法区中保存类型的所有域相关信息以及域的声明顺序
- 域的相关信息包括:域名称、域类型、域修饰符(public, private, protected, static, final, volatile, transient的某个子集)
- 方法(Method)信息。JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
代码测试 1
package com.na;
import java.io.Serializable;
public class MethodInnerStructTest extends Object implements Comparable<String>, Serializable {
public int num = 10;
private static String str = "nasuf";
public void test() {
int count = 20;
Sy("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;
}
}
使用javap -v -p MethodInnerStructTest反编译后输出如下:
Classfile /Users/nasuf/Project/JvmNotesCode/out/production/JvmNotesCode/com/nasuf/jvm
Last modified 2021-5-27; size 1600 bytes
MD5 checksum 884f65d3fee6a4e142249081ec516ffc
Compiled from "Me;
// 笔者注:类型信息
public class com.na.MethodInnerStructTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #18.#52 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#53 // com/nasuf/jvm
#3 = Fieldref #54.#55 // java/lang;
#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:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #4.#59 // java/lang:(I)Ljava/lang/StringBuilder;
#9 = Methodref #4.#60 // java/lang:()Ljava/lang/String;
#10 = Methodref #61.#62 // java/io:(Ljava/lang/String;)V
#11 = Class #63 // java/lang/Exception
#12 = Methodref #11.#64 // java/lang:()V
#13 = Class #65 // java/lang/String
#14 = Methodref #17.#66 // com/nasuf/jvm:(Ljava/lang/String;)I
#15 = String #67 // nasuf
#16 = Fieldref #17.#68 // com/nasuf/jvm;
#17 = Class #69 // com/nasuf/jvm/MethodInnerStructTest
#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 Lcom/nasuf/jvm/MethodInnerStructTest;
#32 = Utf8 test
#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 Me
#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 nasuf
#68 = NameAndType #23:#24 // str:Ljava/lang/String;
#69 = Utf8 com/nasuf/jvm/MethodInnerStructTest
#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
{
// 笔者注:域信息
public int num;
descriptor: I
flags: ACC_PUBLIC
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
// 笔者注:方法信息
// 默认提供无参构造器,即使类中没有写出
// 构造器也划归为方法
public com.na.MethodInnerStructTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 5: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/nasuf/jvm/MethodInnerStructTest;
public void test();
descriptor: ()V // 笔者注:V表示void返回值
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1 // 笔者注:操作数栈深度3,局部变量表长度2,参数大小1(this引用)
0: bipush 20
2: istore_1
3: getstatic #3 // Field java/lang;
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: ldc #6 // String count =
15: invokevirtual #7 // Method java/lang:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: iload_1
19: invokevirtual #8 // Method java/lang:(I)Ljava/lang/StringBuilder;
22: invokevirtual #9 // Method java/lang:()Ljava/lang/String;
25: invokevirtual #10 // Method java/io:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/nasuf/jvm/MethodInnerStructTest;
3 26 1 count I
public static int test2(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1 // 笔者注:相对于实例方法,静态方法没有this引用,此处参数大小1表示形参
0: iconst_0
1: istore_1
2: bipush 30
4: istore_2
5: iload_2
6: iload_0
7: idiv
8: istore_1
9: goto 17
12: astore_2
13: aload_2
14: invokevirtual #12 // Method java/lang:()V
17: iload_1
18: ireturn
Exception table: // 笔者注:异常表,字节码区域从2到9,对应代码区域从line 17 到 line 21
from to target type
2 9 12 Class java/lang/Exception
LineNumberTable:
line 15: 0
line 17: 2
line 18: 5
line 21: 9
line 19: 12
line 20: 13
line 22: 17
LocalVariableTable:
Start Length Slot Name Signature
5 4 2 value I
13 4 2 e Ljava/lang/Exception;
0 19 0 cal I
2 17 1 result I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ int, int ]
stack = [ class java/lang/Exception ]
frame_type = 4 /* same */
public int compareTo);
descriptor: (Ljava/lang/String;)I
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iconst_0
1: ireturn
LineNumberTable:
line 27: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this Lcom/nasuf/jvm/MethodInnerStructTest;
0 2 1 o Ljava/lang/String;
public int compareTo);
descriptor: (Ljava/lang/Object;)I
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #13 // class java/lang/String
5: invokevirtual #14 // Method compareTo:(Ljava/lang/String;)I
8: ireturn
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/nasuf/jvm/MethodInnerStructTest;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #15 // String nasuf
2: putstatic #16 // Field str:Ljava/lang/String;
5: return
LineNumberTable:
line 7: 0
}
Signature: #49 // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
SourceFile: "Me;
non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
- 类变量被类的所有实例共享,即使没有类实例时,也可以访问它
- 全局常量: static final
- 被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了
代码测试 2
class Order {
public int i = 10;
public static int count = 1;
public static final int number = 2;
public static void hello() {
Sy("hello");
}
}
同样使用javap -v -p Order.class反编译后查看部分字节码输入如下:
public int i;
descriptor: I
flags: ACC_PUBLIC
public static int count;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static final int number;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 2 // 笔者注:可以看到final的常量在编译器就被赋值
4.2 运行时常量池 vs 常量池
- 方法区,内部包含了运行时常量池
- 字节码文件,内部包含了常量池
- 要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区
- 要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池。字节码文件中的常量池,通过类加载器加载到方法区后,成为运行时常量池。
参考官网链接
4.2.1 常量池
字节码文件结构:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
一个有效的字节码文件中,除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),常量池内存储的数据类型包括:
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
为什么需要常量池
一个Java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池。这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。例如4.1节中的代码测试1,使用jclasslib查看其常量池信息(反编译字节码也可以看到常量池信息)
而方法的字节码指令中,#3,#4表示的就是常量池的索引
常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
4.2.2 运行时常量池
- 运行时常量池(Runtime Constant Pool)是方法区的一部分
- 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池
- JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址
- 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性
- 运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富一些
- 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemoryError异常
5. 方法区使用举例
代码测试
package com.na;
public class MethodAreaDemo {
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
Sy(a + b);
}
}
反编译字节码如下:
Classfile /Users/nasuf/Project/JvmNotesCode/out/production/JvmNotesCode/com/nasuf/jvm
Last modified 2021-5-28; size 632 bytes
MD5 checksum 098f118c12068e432489305b444363e3
Compiled from "Me;
public class com.na.MethodAreaDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #25.#26 // java/lang;
#3 = Methodref #27.#28 // java/io:(I)V
#4 = Class #29 // com/nasuf/jvm/MethodAreaDemo
#5 = Class #30 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/nasuf/jvm/MethodAreaDemo;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 x
#18 = Utf8 I
#19 = Utf8 y
#20 = Utf8 a
#21 = Utf8 b
#22 = Utf8 SourceFile
#23 = Utf8 Me
#24 = NameAndType #6:#7 // "<init>":()V
#25 = Class #31 // java/lang/System
#26 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#27 = Class #34 // java/io/PrintStream
#28 = NameAndType #35:#36 // println:(I)V
#29 = Utf8 com/nasuf/jvm/MethodAreaDemo
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (I)V
{
public com.na.MethodAreaDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/nasuf/jvm/MethodAreaDemo;
public static void main[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io:(I)V
25: return
LineNumberTable:
line 5: 0
line 6: 4
line 7: 7
line 8: 11
line 9: 15
line 10: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 args [Ljava/lang/String;
4 22 1 x I
7 19 2 y I
11 15 3 a I
15 11 4 b I
}
SourceFile: "Me;
字节码解析
#2 = Fieldref #25.#26 // java/lang;
...
#25 = Class #31 // java/lang/System
#26 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
...
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
···
#3 = Methodref #27.#28 // java/io:(I)V
...
#27 = Class #34 // java/io/PrintStream
#28 = NameAndType #35:#36 // println:(I)V
...
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (I)V
6. 方法区的演进细节
- 首先明确,只有HotSpot虚拟机才有永久代。BEA JRockit、IBM J9等,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》
- HotSpot中方法区的变化
永久代为什么要被元空间替换?
参考
- 随着Java 8的到来,HotSpot VM中再也见不到永久代了,但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域。这个区域叫做元空间
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统的可用内存空间
- 这项改动是很有必要的,因为:
- 为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
- 对永久代进行调优是很困难的
StringTable为什么要调整
- JDK7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会触发。而Full GC是老年代的空间不足,永久代不足时才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存
代码测试1: JDK7
package com.na7;
public class StaticFieldTest {
private static byte[] arr = new byte[1024 * 1024 * 100]; // 100m
public static void main(String[] args) {
Sy);
}
}
在JDK1.7下,运行参数为-Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails,输出如下:
[B@451dfada
Heap
PSYoungGen total 60416K, used 3133K [0x00000007fbd00000, 0x0000000800000000, 0x0000000800000000)
eden space 52224K, 6% used [0x00000007fbd00000,0x00000007fc00f718,0x00000007ff000000)
from space 8192K, 0% used [0x00000007ff800000,0x00000007ff800000,0x0000000800000000)
to space 8192K, 0% used [0x00000007ff000000,0x00000007ff000000,0x00000007ff800000)
ParOldGen total 136704K, used 102400K [0x00000007f3780000, 0x00000007fbd00000, 0x00000007fbd00000)
object space 136704K, 74% used [0x00000007f3780000,0x00000007f9b80010,0x00000007fbd00000)
PSPermGen total 307200K, used 2628K [0x00000007e0b80000, 0x00000007f3780000, 0x00000007f3780000)
object space 307200K, 0% used [0x00000007e0b80000,0x00000007e0e11050,0x00000007f3780000)
可以看到ParOldGen total 136704K, used 102400K arr数组放在了老年代中
同样代码运行在JDK1.8下面,运行参数为-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspace=300m -XX:+PrintGCDetails,输出如下:
[B@2503dbd3
Heap
PSYoungGen total 59904K, used 3103K [0x00000007bbd80000, 0x00000007c0000000, 0x00000007c0000000)
eden space 51712K, 6% used [0x00000007bbd80000,0x00000007bc087c70,0x00000007bf000000)
from space 8192K, 0% used [0x00000007bf800000,0x00000007bf800000,0x00000007c0000000)
to space 8192K, 0% used [0x00000007bf000000,0x00000007bf000000,0x00000007bf800000)
ParOldGen total 136704K, used 102400K [0x00000007b3800000, 0x00000007bbd80000, 0x00000007bbd80000)
object space 136704K, 74% used [0x00000007b3800000,0x00000007b9c00010,0x00000007bbd80000)
Metaspace used 2658K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 287K, capacity 386K, committed 512K, reserved 1048576K
可以看到byte数组依然放在老年代ParOldGen total 136704K, used 102400K。
如果测试JDK1.6版本下,依然可以看到byte数组放在了老年代。应注意,我们提到JDK1.7以后静态变量才存放在堆空间,指的是引用变量,而静态引用对应的对象实体始终都存在堆空间。即只要是对象实例必然会在Java堆中分配
7. 方法区的垃圾回收
有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过尅不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期ZGC收集器就不支持类卸载)
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的bug就是由于低版本的HotSpot虚拟机对此区域未完全收集而导致的内存泄漏
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
- 先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,也包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收
- 回收废弃常量与回收Java堆中的对象非常类似
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻。需要同时满足下面三个条件: - 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGI,JSP的重加载等,否则通常是很难达成的
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
- Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而不是和对象一样,没有引用了就必然会被回收。关于是否要对类型进行回收,Hotspot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息
- 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力
1.《00000007b怎么解决看这里!JVM系列(七)运行时数据区(方法区)》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《00000007b怎么解决看这里!JVM系列(七)运行时数据区(方法区)》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/gl/2206026.html