金樽清酒斗十千,玉盘珍羞直万钱。
1、栈、堆、方法区的交互关系
从内存结构来看:
运行时数据区大致可分为:程序计数器 ,虚拟机栈 ,本地方法栈 ,堆 ,方法区 五部分。
从线程共享与否的角度来看:
方法区 和堆 是线程共享的;程序计数器 ,虚拟机栈 ,本地方法栈 是线程私有的。而元空间(jdk8及以上) /永久代(JDK7及以前) 是方法区的实现落地。
从一个常见的代码来看栈、堆、方法区的关系
Person person = new Person();
2、方法区的理解
《Java虚拟机规范》明确提出:尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择区进行垃圾收集 或者进行压缩 。但对HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开,为此方法区可以看成一块独立的内存空间
方法区的特点:
- 方法区和堆一样,是线程共享的内存区域
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
- 方法区的大小决定了系统可以保存多少个类信息,如果系统定义了太多类或加载了大量第三方jar文件,这些都可能导致方法区溢出,JVM同样会抛出OOM
一个很神奇的事情,一个普通的类运行起来,JVM到底加载了多少个类?
public class TestClassLoader {
public static void main(String[] args) {
System.out.println("start");
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
}
竟然有1633个类!!!这里面不但包括了自身的类信息,也包括类父类信息,类使用的其他类的信息等等等…
3、方法区在HotSpot VM中的演变
在JDK7及之前,方法区的实现称为永久代(PermGen Space),在JDK8到现在称为元空间(Meta Space),这二者是对方法区的不同实现
在JDK8中及以后,永久代的概念被废弃,改用了JRockit,J9一样在本地内存中实现的元空间来代替
元空间的本质和永久代相似,都是方法区的落地实现,不过元空间和永久代最大的区别在于:元空间不再像永久代一样使用JVM的内存,而使用本地物理内存 ,二者不但名字不同,使用的内存不同,内部结构也进行了调整
4、设置方法区大小
方法区的大小是不固定的,JVM可以根据应用的需要动态调整
在JDK7及以前:
- 通过
-XX:PermSize=x 来设置永久代初始分配空间,默认值是20.75M - 通过
-XX:MaxPermSize=x 来设置永久代的最大空间 32位机器最大64M 64位机器最大82M
在JDK8到现在:
- 通过
-XX:MetaspaceSize=x 来设置元空间初始分配空间,默认是21M - 通过
-XX:MaxMetaspaceSize=x 来设置永久代的最大空间,默认为-1 即没有限制,可用物理内存多大就能用多大 - 在开发中,方法区一般只设置初始分配空间,不设置最大空间。
- 对于
64位 的服务器端JVM来说,其默认的-XX:MetaspaceSize 为21M ,这是初始的高水位线。一旦方法区的大小触及到这个高水位线,会开启Full GC 对堆和方法区进行垃圾回收,将方法区中没有的类信息回收,然后这个高水位线将会重置。新的高水位线的值取决于Full GC后方法区释放了多少空间,如果释放的空间不足,在不超过MaxMetaspace时,适当提高该值,如果释放空间较多,则适当降低该值
可以看见元空间和永久代最大的差距是内存方面的实现 ,元空间使用本地内存 ,本地内存有多大,元空间就可以有多大,而永久代的内存空间由JVM管辖 ,受JVM的内存大小的限制。
5、OOM的排查
- 要解决方法区或堆出现的OOM,一般手段是通过内存映像分析工具(如Java VisualVM)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是说先要分析出是出现了“内存泄漏” 还是 “内存溢出”
- 如果是出现了内存泄漏,可以通过工具查看泄漏对象到GC Roots的引用链,于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的,掌握了泄漏对象的信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置
- 如果不存在内存泄漏,就是说内存中的对象确实还必须存活,那就应该检查虚拟机的堆参数,与机器物理内存,从代码上检查对象生命周期是否过长,持有时间是否过长,尝试减少程序运行期间的内存消耗
内存溢出和内存泄露
内存泄漏:已申请的内存空间无法释放
内存溢出:申请空间是,可用内存不够。
6、方法区的内部结构
方法区存储的信息包括:类型信息 ,常量 ,静态变量 ,即时编译器编译后的代码缓存 ,运行时常量池 。
6.1 类型信息
类型信息的详细内容(对每个加载的类型(class,interface,enum,annotation),JVM必须在方法区中存储以下类型信息):
- 这个类型的完整名称(包名.类名)
- 这个类型的直接父类的完整名称(interface和Object类没有直接父类)
- 这个类型的修饰符(public,abstract,final)的某个子集
- 这个类型直接接口的一个有序列表
字段(成员)信息:(JVM必须在方法区中保存类型所有成员的信息及声明顺序):
- 成员名称
- 成员类型
- 成员修饰符(public,protected,static,volatile,transient)的某个子集
方法信息:(JVM必须在方法区保存所有方法的信息及声明顺序)
- 方法名称
- 方法返回类型
- 方法的参数和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract)的某个子集
- 方法的字节码(bytecodes),操作数栈,局部变量表及大小(abstract和native方法除外)
- 方法的异常表(abstract和native方法除外)
对下面的程序进行分析,查看字节码文件
public class MethodInnerStructTest extends ArrayList implements Runnable, Serializable {
//属性
public int num = 10;
private static String str = "方法区内部结构";
//构造器
//方法
public void test1() {
int num = 20;
System.out.println("count= " + num);
}
public static int test2(int cal) {
int result = 0;
try {
int value = 30;
result = value / cal;
}catch (Exception e) {
}
return result;
}
//接口方法
@Override
public void run() {
}
}
对其进行反编译:
public class com.shang.jvm.runtimedata.MethodArea.MethodInnerStructTest extends java.util.ArrayList implements java.lang.Runnable,java.io.Serializable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #15.#41
#2 = Fieldref #14.#42
#3 = Fieldref #43.#44
#4 = Class #45
#5 = Methodref #4.#41
#6 = String #46
#7 = Methodref #4.#47
#8 = Methodref #4.#48
#9 = Methodref #4.#49
#10 = Methodref #50.#51
#11 = Class #52
#12 = String #53
#13 = Fieldref #14.#54
#14 = Class #55
#15 = Class #56
#16 = Class #57
#17 = Class #58
#18 = Utf8 num
#19 = Utf8 I
#20 = Utf8 str
#21 = Utf8 Ljava/lang/String;
#22 = Utf8 <init>
#23 = Utf8 ()V
#24 = Utf8 Code
#25 = Utf8 LineNumberTable
#26 = Utf8 LocalVariableTable
#27 = Utf8 this
#28 = Utf8 Lcom/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest;
#29 = Utf8 test1
#30 = Utf8 test2
#31 = Utf8 (I)I
#32 = Utf8 value
#33 = Utf8 cal
#34 = Utf8 result
#35 = Utf8 StackMapTable
#36 = Class #52
#37 = Utf8 run
#38 = Utf8 <clinit>
#39 = Utf8 SourceFile
#40 = Utf8 MethodInnerStructTest.java
#41 = NameAndType #22:#23
#42 = NameAndType #18:#19
#43 = Class #59
#44 = NameAndType #60:#61
#45 = Utf8 java/lang/StringBuilder
#46 = Utf8 count=
#47 = NameAndType #62:#63
#48 = NameAndType #62:#64
#49 = NameAndType #65:#66
#50 = Class #67
#51 = NameAndType #68:#69
#52 = Utf8 java/lang/Exception
#53 = Utf8 方法区内部结构
#54 = NameAndType #20:#21
#55 = Utf8 com/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest
#56 = Utf8 java/util/ArrayList
#57 = Utf8 java/lang/Runnable
#58 = Utf8 java/io/Serializable
#59 = Utf8 java/lang/System
#60 = Utf8 out
#61 = Utf8 Ljava/io/PrintStream;
#62 = Utf8 append
#63 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#64 = Utf8 (I)Ljava/lang/StringBuilder;
#65 = Utf8 toString
#66 = Utf8 ()Ljava/lang/String;
#67 = Utf8 java/io/PrintStream
#68 = Utf8 println
#69 = Utf8 (Ljava/lang/String;)V
{
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.shang.jvm.runtimedata.MethodArea.MethodInnerStructTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1
4: aload_0
5: bipush 10
7: putfield #2
10: return
LineNumberTable:
line 12: 0
line 14: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest;
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: bipush 20
2: istore_1
3: getstatic #3
6: new #4
9: dup
10: invokespecial #5
13: ldc #6
15: invokevirtual #7
18: iload_1
19: invokevirtual #8
22: invokevirtual #9
25: invokevirtual #10
28: return
LineNumberTable:
line 21: 0
line 22: 3
line 23: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest;
3 26 1 num I
public static int test2(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=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 13
12: astore_2
13: iload_1
14: ireturn
Exception table:
from to target type
2 9 12 Class java/lang/Exception
LineNumberTable:
line 26: 0
line 28: 2
line 29: 5
line 32: 9
line 30: 12
line 34: 13
LocalVariableTable:
Start Length Slot Name Signature
5 4 2 value I
0 15 0 cal I
2 13 1 result I
StackMapTable: number_of_entries = 2
frame_type = 255
offset_delta = 12
locals = [ int, int ]
stack = [ class java/lang/Exception ]
frame_type = 0
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 40: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #12
2: putstatic #13
5: return
LineNumberTable:
line 15: 0
}
SourceFile: "MethodInnerStructTest.java"
6.2 类变量 (non-final)
静态变量和类变量关联在一起,随着类加载而加载,它们成为类数据在逻辑上的一部分。
类变量被所有的实例共享,即使没有类使用也可以进行访问。
public class MethodAreaTest {
public static void main(String[] args) {
Order order = null;
System.out.println(order);
order.hello();
System.out.println(order.count);
}
}
class Order {
public static int count = 10;
public static void hello() {
System.out.println("hello");
}
}
null
hello
10
7、class文件中的常量池 Constant Pool
方法区中还有一个重要的结构运行时常量池,而在字节码文件中包含了常量池
字节码文件被类加载子系统加载到方法区后,常量池中的内容就被加载到了方法区对应的运行时常量池,在理解运行时常量池前,先分析字节码文件中的常量池
一个有效的字节码文件中除了包含类的版本信息,成员,方法以及接口等描述信息外,还有一项信息就是常量池,
常量池保存各种字面量(文本字符串,final修饰的常量,基本数据类型的值,变量名,方法名…),以及类型,成员和方法的符号引用
常量池中保存的字面量 (CONSTANT_Utf8_info都是字面量)
7.1 为什么需要常量池
一个Java源文件中的类,接口等信息编译后产生一个字节码文件。
字节码文件才是JVM需要的原材料,字节码文件也需要大量的数据支持,通常这些数据很大以至于不能直接存放到字节码文件中。如果将这些数据全直接放到字节码文件中,字节码文件会很庞大,因此采用常量池来“节省空间” 保存这些数据,以压缩整个字节码文件
一句简单的话理解:
多个类都使用到了输出流,那么这些类加载过程中都需要将输出流加载到方法区中吗?不一定把!其实只需要一份,这些类只要保存输出流的一个引用就可以了。刚好,常量池中保存的就是引用,这样不就节省了大量了资源了吗?
8、方法区的运行时常量池
常量池是字节码文件中的一部分,用于存放编译期间生成地各种字面量和符号引用。当字节码文件经过类加载子系统加载后,常量池中的内容被加载到方法区对应的运行时常量池
运行时常量池的特点:
-
当某个类型(类,接口,枚举,注解)被加载到JVM后,就会在方法区为它创建对应的运行时常量池,方法区中存储若干个运行时常量池,运行时常量池通过索引访问数据 -
当在方法区创建类型的运行时常量池时,如果创建运行时常量池所需的空间超过了方法区剩余的空间,就会抛出OOM -
运行时常量池中包括编译期已经明确的字面量,和运行期将符号引用解析后获得的类,方法,成员的直接引用(真实地址指针) -
运行时常量池,相对于class文件常量池的最大特征是 “具备动态性”,也就是运行期间也能将常量放入运行时常量池,而不是class文件常量池编译后内容就不可更改
9、方法区在HotSpot VM中的演进细节
9.1 不同版本JDK中方法区的构成
HotSpot VM中不同版本方法区的变化
-
jdk 1.6及之前: 有永久代 静态变量和字符串常量池(String Table)存放在永久代中(只有HotSpot才有永久代) -
jdk 1.7: 有永久代 但已经逐步“去永久代” 将永久代中的字符串常量池,静态变量移除,保存在堆里 -
jdk 1.8及之后: 无永久代 类型信息,成员信息,方法信息,运行时常量池保存在本地内存的元空间,将字符串常量池和静态变量保存在堆里
为什么要用元空间替代永久代?
永久代的思路就是所有的内存都由JVM分配管理,让JVM控制程序运行时的一切,而为了融合JRockit(JRockit没有永久代,方法区应该存储的数据都保存在物理内存),并且减少方法区出现OOM的问题,就使用元空间替换永久代,将方法区应该存储的数据放在物理内存
原因:
- 永久代的空间大小很难确定,在加载的类过多的情况下易出现OOM 如一个庞大的Web工程,运行时要不断动态地加载很多类,就容易出现OOM 而元空间使用物理内存,仅元空间大小仅限于本地机器可用物理内存大小
- 对永久代的调优很困难,很难避免多次的Full GC
9.2 字符串常量池为什么要放在堆里?
JDK7中将字符串常量池(StringTable)放到了堆空间中,因为永久代的回收效率很低,在Full GC时才会触发,而Full GC是因为老年代的空间不足和永久代空间不足才触发的,这就导致StringTable回收效率不是很高
因为在开发中有大量的字符串被创建,如果将字符串常量池放在永久代,那么就会导致字符串回收效率低,导致永久代空间不足,而将StringTable放到堆里,能及时回收字符串
10、方法区的垃圾回收
JVM规范对方法区的回收是没有明确规定的,因为方法区的回收效果比较难令人满意,尤其是类的卸载,条件相当苛刻,但是对方法区的回收又是必要的,不对方法区进行回收,就会导致方法区的可用容量越来越少,在老版本的HotSpot VM中常常因为对方法区未完全回收导致内存泄漏,进而内存溢出
方法区的GC主要回收两部分内容:废弃常量+不再使用的类型信息
废弃常量是指:方法区中运行时常量池内不被任何地方引用的字面量和直接引用
**不再使用的类型信息是指:**方法区中不再使用的类型信息,成员信息,方法信息
但是类型信息的回收条件很苛刻,需要同时满足:
- 该类所有的对象已经被回收,即堆中不存在该类和该类的子类的对象
- 加载该类的类加载器已经被回收,通常很难达成
- 该类的Class对象没有在任何地方被引用
即使同时满足了上述三个条件,该类型信息也只是允许被回收,而不是和对象一样没有引用就直接被回收。
但是在大量的使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OGSi这类 频繁自定义类加载器的场景中,通常需要JVM具备类型卸载的功能,以保证不会对方法区造成太大压力
|