文章目录
方法区
- 线程共享
方法区和堆区的关系
方法区的简述
方法区定位
《Java虚拟机规范》:尽管所有方法区在逻辑上属于堆一部分,但一些简单实现,可能不会进行垃圾收集或进行压缩。
对于HotSpot,方法区又名:Non-Heap(非堆),目的:区分堆。
方法区看作是一块独立于Java堆的内存空间
方法区和堆的异同
- 方法区主要存放Class,堆中主要存放实例化对象(堆和方法区的不同点)
- 方法区/Java堆,各个线程共享内存区域(同)
- 方法区/Java堆,在JVM启动时被创建,物理内存空间可以不连续,逻辑空间要连续(同)
- 方法区/Java堆,可以选择固定大小或者可扩展(同)
- 方法区大小决定了系统可以保存多少个类,如果类定义太多,导致方法区溢出,JVM同样抛出内存溢出异常 OOM
- java.lang.OutofMemoryError:PermGen space(永久代OOM)
- java.lang.OutOfMemoryError:Metaspace(元空间OOM)
举例说明方法区OOM
根据Jvm规范,如果方法区无法满足新的内存分配需求,将抛出OOM异常
- 加载大量的第三方的jar包
- Tomcat部署的工程过多(30~50个)
- 大量动态的生成反射类
方法区内部结构
设置方法区大小与OOM
JDK7及之前设置永久代大小
-XX:PermSize 设置永久代初始分配空间
-XX:MaxPermSize 设置永久代最大可分配空间
JVM加载类信息容量超过设定值,会报异常OutofMemoryError:PermGen space
JDK8设置元空间大小
-
-XX:MetaspaceSize:设置初始元空间大小
-
64位服务端JVM,默认初始元数据区空间21M,初始的高水位线
-
触及高水位线,FullGC触发并卸载没用类,高水位线会被重置。新高水位线值取决于GC后释放了多少元空间。
如果释放空间不足,在不超过最大设定值时,适当提高该值。
如果释放空间过多,则适当降低该值。
-
如果初始化高水位线设置过低,上述高水位线调整情况会发生很多次,FullGC多次调用。为避免频繁FullGC,建议将 -XX:MetaspaceSize设置为一个相对较高值
-
-
-XX:MaxMetaspaceSize:-1(没有限制)
不指定大小,虚拟机耗尽所有系统可用内存,一样抛出异常OutOfMemoryError:Metaspace
解决OOM和heap space异常
一般手段:通过内存映像分析工具(如Eclipse Memory Analyzer),对dump出来的堆转存储快照分析,重点确认:内存中的对象是否是必要的。先分清:内存泄露,还是内存溢出
什么是内存泄漏?
大量引用指向某些对象,但是这些对象以后不会使用。这些对象还和GC ROOT有关联,所以也不会被回收
若内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到内存泄露对象时通过怎样的路径与GC Roots相关联,导致垃圾收集器无法自动回收他们。根据引用链信息,可以较准确的定位出泄露代码的位置
如果不存在内存泄露,或者说内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与物理机器内存对比是否还可以调大,从代码检查是否某些对象生命周期过长,持有状态时间过长,尝试减少程序运行时的内存耗用
若内存溢出,jvm参数-Xmx/-Xms 调大,减少程序运行时的内存耗用
内部结构
用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存
类型信息
对于每个加载的类型(类Class,接口Interface,枚举Enum,注解annotation)JVM必须在方法区中存储以下类型信息
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
- 这个类型直接接口的一个有序列表
域(Field)信息
-
域的相关信息包括
- 域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
- 域类型
- 域名称
-
域信息特殊情况
类变量:non-final 类型 不属于特定类实例,随着类的加载而加载
全局常量:static final 进行修饰
每个全局常量在编译阶段被分配。(反编译,查看字节码指令,可以发现 number 的值已写死在字节码文件中)
方法信息
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 方法的返回类型(包括 void 返回类型),void 在 Java 中对应的类为 void.class
- 方法名称、
- 方法参数的数量和类型(按顺序)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外),异常表记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
运行时常量池
方法区内部包含的是运行时常量池而不是常量池。
字节码文件,内部包含了常量池,运行时将常量池加载到方法区,就是运行时常量池,执行时,将常量池中的符号引用(字面量)转换为直接引用(真正的地址值)。
运行时常量池,相对于class文件常量池:具备动态性
常量池
字节码文件包含:类的版本信息、字段、方法以及接口等描述信息还包含常量池表(Constant Pool Table),包括编译生成各个字面量和对类型、域和方法的符号引用。
- 为什么要用常量池?
- 一个java源文件中的类、接口、编译后产生字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大,以至于不能直接存到字节码里。
- 可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接会用到运行时常量池。
- 编译产生字节码文件需要大量数据支持,不能存在字节码文件中,存到常量池里,字节码包含指向常量池的引用
- 常量池有什么?
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
常量池,可看做一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
运行时常量池
创建:在加载类和接口到虚拟机后,就会创建对应的运行时常
当创建类或接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值。则JVM会抛出OOM异常
JVM为每个已加载的类和接口都维护一个运行常量池,池中的数据像数组项一样,通过索引访问
运行时常量池包含多种不同的常量,(包括编译期就已经明确的数值字面量,也包括到运行期解析后,才能够获得的方法或者字段引用。)此时不再是常量池中的符号地址,这里转换为真实地址。
运行时常量池,相对于class文件常量池:具备动态性
常量池数量为N,则索引为1到N-1?
方法区演变
首先明确,只有HotSpot才有永久代
HostSpot可看作方法区永久代等价,本质不等价,《Java虚拟机规范》对如何实现方法区,不做统一要求。
方法区是JVM的规范,永久代,元空间是方法区的实现
HotSpot中方法区的变化
jdk1.6及之前,有永久代,静态变量存放在永久代上。使用 JVM 虚拟机内存
jdk1.7,有永久代,但已经逐步去永久代,字符串常量池,静态变量移除,保存在堆中。使用 JVM 虚拟机内存
jdk8,取消永久代,使用元空间实现方法区(保存类型信息,字段,方法,常量) JVM内存–>本地内存。
元空间永久代,都是对JVM规范中方法区的实现。
元空间永久代区别:元空间不在虚拟机中设置内存,使用本地内存(堆外内存)
永久代为什么要被元空间替代?
永久代设置空间大小很难确定
-
如果动态加载类过多,就容易产生OOM
-
会经常触发Full GC
-
设置-XX:PermSize,初始化分配一块连续的内存块
设置过大:内存浪费
设置过小:OOM
存储在本地内存,仅受本地内存限制
达到-XX:MetaspaceSize–>触发FGC–>进行类型卸载,同时GC会对该值进行调整**(可动态调整)**
如果释放了大量的空间,就适当降低该值
如果释放了很少的空间,那么在不超过MaxMetaspaceSize,适当提高该值。
对永久代进行调优很困难
-
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再用的类型,方法区的调优主要是为了减少Full GC次数
-
有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载)。
-
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏
字符串常量池 StringTable 为什么要调整位置?
JDK7中将StringTable从运行时常量池移到堆空间。Full GC执行永久代的垃圾回收,永久代回收效率低。Full GC触发条件:老年代空间不足、永久代空间不足
开发中会有大量字符串被创建,回收效率低,导致永久代内存不足。
移动到堆,提高回收效率
方法区的垃圾回收
常量池中废弃的常量
HotSpot对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收
回收废弃常量与回收Java堆中对象非常类似
方法区内常量池中主要存放两大类常量:
- 字面量(常量):如文本字符串,被声明为final的常量值等
- 符号引用(编译原理)
- 类和接口的全限定名
- 字段的方法和描述符
- 方法的名称和描述符
方法区类的回收
判断一个类是否被卸载的条件
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收(难达成)
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足以上三个条件后,并不是和对象一样立即被回收,仅仅是被允许。
HotSpot虚拟机提供了-Xnoclassgc参数进行控制
和描述符
- 方法的名称和描述符
方法区类的回收
判断一个类是否被卸载的条件
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收(难达成)
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足以上三个条件后,并不是和对象一样立即被回收,仅仅是被允许。
HotSpot虚拟机提供了-Xnoclassgc参数进行控制
在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力