jvm内存模型图
一,方法区的理解
方法区的定位
《java虚拟机规范》:尽管所有方法区在逻辑上属于堆一部分,但一些简单实现,可能不会进行垃圾收集或进行压缩。对于hotspot,方法区又名:non-heap(非堆),目的:区分堆。方法区看作是一块独立于java堆的内存空间
方法区的基本理解
-
方法区与java堆一样,是各个线程共享的区域.
-
方法区在jvm启动的时候被创建,并且它的实际内存空间中和java堆区一样都是可以不连续的,但逻辑上认为是连续的
-
方法区的大小也跟堆空间一样,可以选择固定大小或者可扩展.
-
方法区大小决定了系统可以保存多少个类,如果类定义太多,导致方法区溢出,jvm同样抛出内存溢出异常
jdk1.8之前oom,java.lang.outofmemoryerror:permgen space
jdk1.8 java.lang.outofmemoryerror:metaspace
-
关闭jvm就会释放这个区域的内存(待)
hotspot虚拟的方法区的演进
在jdk7及以前,方法区被称作永久代,jdk8开始,元空间取带了永久代(元空间不在虚拟机中设置内存,使用本地内存,堆外内存),hostspot可看作方法区永久代等价,本质不等价,《java虚拟机规范》对如何实现方法区,不做统一要求.
在jrockit 和 j9 虚拟机中就没有永久代的概念(方法区),
jdk1.7如图所示:永久代更容易导致java程序oom(超过-xx:maxpermsize上限)
jdk1.8如图所示:使用堆外内存(本地内存)
设置方法区的大小及oom
jdk1.7及之前设置永久代大小
-
-xx:permsize 设置永久代初始分配空间
-
-xx:maxpermsize 设置永久代最大可分配空间
-
jvm加载类信息容量超过设定值,会报异常outofmemoryerror:permgen space
jdk1.7及之前设置永久代大小
-
-xx:metaspacesize:设置初始元空间大小
64位服务端jvm,默认初始元数据区空间21m,初始的高水位线,触及水位线,fullgc触发并卸载没用类,高水位线会被重置。新高水位线值取决于gc后释放了多少元空间。如果释放空间不足,在不超过最大设定值时,适当提高该值。如果释放空间过多,则适当降低该值。如果初始化高水位线设置过低,上述高水位线调整情况会发生很多次,fullgc多次调用。为避免频繁fullgc,建议将
-xx:metaspacesize设置为一个相对较高值
-
-xx:maxmetaspacesize:-1(没有限制,但却又限制于本地内存的可用大小 )
二,方法区的内存结构
方法区存储什么?
《java虚拟机规范》中描述:方法区它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存
如图所示:
类型信息
对于每个加载的类型(类class,接口interface,枚举enum,注解annotation)jvm必须在方法区中存储以下类型信息:
-
这个类型的修饰符(public,abstract,final的某个子集)
-
这个类的完整有效名称(全名=包名.类名)
-
这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
-
这个类型直接接口的一个有序列表
例:public class methodinnerstructest extends object implements comparable, serializable
域信息
jvm必须在方法区中保存类型的所有域的相关信息,以及域的声明顺序,域的相关信息包括:域修饰符(public,private,protected,static,final,volatile,transient的某个子集),域类型,域名称.
例:public int num = 10;
域信息特殊情况:
- 类变量:non-final 类型 ,静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分,类变量被类的所有实例共享,即使没有类实例时,你也可以访问它(空指针调用不会异常)
- 全局常量:static final 进行修饰,每个全局常量在编译阶段被分配。
方法信息
jvm必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 方法的返回类型(包括 void 返回类型),void 在 java 中对应的类为 void.class
- 方法名称
- 方法参数的数量和类型(按顺序)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外),异常表记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
jit缓存
即时编译器(jit)对热点代码做出的缓存优化
运行时常量池 vs 常量池
- 方法区,内部包含了运行时常量池
- 字节码文件,内部包含了常量池,运行时将常量池加载到方法区,就是运行时常量池,执行时,将常量池中的符号引用(字面量)转换为直接引用(真正的地址值)
- 加载类的信息在方法区,需要理解字节码文件
- 要弄清方法区的运行时常量池,需要理解字节码文件中的常量池
- 运行时常量池,相对于class文件常量池:具备动态性
运行时常量池
运行时常量池(runtime constant pool)是方法区一部分;
常量池表(constant pool table)是class字节码文件一部分,(用于存放编译生成各个字面量和对类型、域和方法的符号引用),这部分内容将在类加载后存放到方法区的运行时常量池中。
在加载类和接口到虚拟机后,就会创建对应的运行时常量池,当创建类或接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值。则jvm会抛出oom异常,
jvm为每个已加载的类和接口都维护一个运行常量池,池中的数据像数组项一样,通过索引访问,
运行时常量池包含多种不同的常量,(包括编译期就已经明确的数值字面量,也包括到运行期解析后,才能够获得的方法或者字段引用。)此时不再是常量池中的符号地址,这里转换为真实地址。
运行时常量池,相对于class文件常量池:具备动态性 ,例如:string.intern可以将字符串也放入运行时常量池
常量池
为什么要使用常量池?
一个java源文件中的类、接口、编译后产生字节码文件。而java中的字节码需要数据支持,通常这种数据会很大,以至于不能直接存到字节码里。可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接会用到运行时常量池。编译产生字节码文件需要大量数据支持,不能存在字节码文件中,存到常量池里,字节码包含指向常量池的引用
常量池有什么?
数量值,字符串值,类引用,字段引用,方法引用
常量池总结:常量池,可看做一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
三:方法区演进的具体细节(hotspot)
jdk1.6及之前,有永久代,静态变量存放在永久代上。使用 jvm 虚拟机内存,如图:
jdk1.7,有永久代,但已经逐步去永久代,字符串常量池,静态变量移除,保存在堆中。使用 jvm 虚拟机内存,如图:
jdk8,取消永久代,使用元空间实现方法区(保存类型信息,字段,方法,常量) jvm内存–>本地内存。如图:
总结如图:
永久代为什么要被元空间替代?
-
永久代设置空间大小很难确定;如果动态加载类过多,就容易产生oom,会经常触发full gc
设置-xx:permsize,初始化分配一块连续的内存块;设置过小oom,过大浪费内存
-
存储在本地内存,仅受本地内存限制。
-
对永久代进行调优很困难;
字符串常量池 stringtable 为什么要调整位置?
jdk7中将stringtable从运行时常量池移到堆空间。full gc执行永久代的垃圾回收,永久代回收效率低。full gc触发条件:老年代空间不足、永久代空间不足,开发中会有大量字符串被创建,回收效率低,导致永久代内存不足。移动到堆,提高回收效率
方法区的垃圾回收:
常量池中废弃的常量和不再使用的类型。
常量回收:hotspot对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收
字面量(常量):如文本字符串,被声明为final的常量值等
符号引用(编译原理):类和接口的全限定名,字段的方法和描述符,方法的名称和描述符
方法区类型的回收条件非常苛刻:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收(难达成)
- 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足以上三个条件后,并不是和对象一样立即被回收,仅仅是被允许。hotspot虚拟机提供了-xnoclassgc参数进行控制
在大量使用反射,动态代理,cglib等字节码框架,动态生成jsp以及osgi这类频繁自定义类加载器的场景中,通常都需要java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力