解读方法区
什么是方法区?
说的简单点就是Java内存结构中的一块内存区域,用来存放一些东西,存放什么呢?主要就是存放已经被虚拟机加载的类型信息,常量以及一些静态变量等信息,另外对于方法区,还存储着非常重要的一类东西,叫做"常量池"。
比如大家听说的类常量池,也即是Class常量池,然后还有字符串常量池以及运行时常量池!
然后再往上层去理解方法区的话,它则是《JVM规范》所定义的,就好比我们制造某个东西,需要按照一定的规范来制造,哪些东西必须有以及这个东西用来干什么,都是事先规范好的,举个简单的例子,比如我们现在要造一辆汽车,有哪些规范呢?
一些显而易见的,你得有轮子,一般还都是四个,轮子还要分为轮毂以及轮胎,这些就是规范,至于轮毂用什么材料,制造成什么样式等等,这些可以有制造商自己决定,所以也就会有各种各样的轮毂......
同理,这个虚拟机也是一样的,我们平常说Java虚拟机就如同在说汽车一样,我们知道汽车有很多品牌,有不同的制造商生产,那Java虚拟机也是一样的,也有很多种Java虚拟机,我们经常说的Java虚拟机,其实默认就是HotSpot虚拟机,这个虚拟机是目前用的最广的虚拟机,像OracleJDK和OpenJDK中使用的就都是HotSpot虚拟机!
但是我们要知道的是Java虚拟机绝对不等同于HotSpot虚拟机,就好比,汽车不等同于宝马,因为除了宝马还有奔驰和奥迪等,所以Java虚拟机除了HotSpot虚拟机之后还有BEA JRockit和IBM J9等其他虚拟机!
oracle的HotSpot虚拟,BEA System的JRockit虚拟机以及IBM公司的J9虚拟机,并称"三大商业Java虚拟机"
不过就目前而言,使用最多的还是HotSpot虚拟机,我们后面如果没有特殊说明,均是以HotSpot虚拟机为准!
知道了虚拟机的这些知识以后,我们再看方法区,就好比是组成汽车的轮毂,制造汽车轮毂是必须的,那制造Java虚拟机,方法区也是必须的,无论你是HotSpot虚拟机还是其他虚拟机,这个方法区都是必须有的!
在HotSpot虚拟机中,JDK7及之前的实现中,这个方法区都是有,而且直接就叫做方法区,但是在JDK8及之后,这个方法区就没有了,被移除了,但是前面不是说了嘛,这个方法区是必须的,是所有虚拟机都必须有的,咋办?
简单,重新造出一个去替代原有的方法区,就是重新实现,重新生成,所以在JDK1.8及之后方法区被移除,增加了一个叫做元空间的区域,这个元空间其实就是方法区的另一种实现,也可以看作原有的方法区升级改造,成了现在的 元空间 。
就好比,现在制造汽车必须的轮毂可能在以后由于科技技术的发展有了新的制造方法,到那时候可能不叫做轮毂,改叫轮芯了也说不定,但是无论是轮毂还是轮芯其实本质还是那一样东西,只不过在不断升级着!
所以在JDK1.7及之前叫做方法区,但是在JDK1.8及之后就成了元空间!
HotSpot 中方法区的演进
在 JDK1.7 及以前,习惯上把方法区,称为永久代。从 JDK1.8开始,使用元空间代替了永久代。JDK1.8之后,元空间存放在堆外内存中。
《Java虚拟机规范》中,对如何实现方法区,没有做统一的要求。例如,IBM J9 中就不存在永久代的概念。从JDK1.8之后,HotSpot虚拟机完全废弃了永久代的概念,改用与 J9 一样在本地内存中实现的元空间来代替。
元空间的本质与永久代类似,都是对虚拟机规范中的方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
设置方法区大小与OOM
方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整。
jdk7及之前:
-
通过 -XX:Permsize 来设置永久代初始分配空间。默认值是 20.75 M。
-
通过 -XX:MaxPermsize 来设置永久代最大可分配空间。32 位的机器默认是 64M,64位的机器默认是 82M。
-
当 JVM 加载的类信息容量超过了这个值,会报异常 OutOfMemoryError:PermGen Space。
jdk8以后:
元数据大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定。与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常 OutOfMemoryError: Metaspace。
-XX:MetaspceSize:设置初始的元空间大小。对于一个 64 位的服务器端的 JVM,其默认值是 21M。这是初始的高水位线,一旦触及这个水位线,FullGC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足,那么在不超过 MaxMetaspaceSize 时,适当的提高该值。如果释放的空间过多,则适当的降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况就会发生很多次。通过垃圾回收器的日志可以观察到 FullGC 多次调用。为了避免频繁的 GC ,建议将 -XX:MetaspaceSize 设置为一个相对较高的值。
如何解决OOM
要解决 OOM 异常或 Heap Space 的异常,一般的手段是首先通过内存分析工具(Eclipse Memory Analyzer)对 dump 出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)。
内存泄露就是指有大量的引用指向了一些对象,但是这些对象以后不会再被使用了,由于此时它们还和 GC Roots 有关联,所以导致以后这些对象也不会被回收,这就是内存泄露的问题。
-
如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收他们。掌握了泄漏对象的类型信息,以及 GCRoots 引用链的信息,就可以比较准确的定位出泄漏代码的位置。
-
如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xms 和 -Xmx),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
方法区的垃圾回收
有些人认为方法区是没有垃圾收集行为的,其实不然。《java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前sun 公司的 bug 列表中,曾出现过若干个严重的 bug 就是由于低版本的 HotSpot 的虚拟机对此区域未完全回收而导致内存泄露。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
先来说说方法区内的常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近 java 语言层次的 常量概念,如文本字符串、被申明为 final 的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
-
类和接口的全限定名
-
字段的名称和描述符
-
方法的名称和描述符
HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃常量和回收Java堆中的对象非常类似。(关于常量的回收比较简单,重点是类的回收)。
判定一个常量是否"废弃"还是相对简单,而要判定一个类型是否属于"不在被使用的类"的条件就比较苛刻了。需要同时满足下面三个条件:
-
该类所有的实例都已经被回收,也就是 java 堆中不存在该类及其任意派生子类的实例。
-
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGI、JSP的重加载等,否则是很难达成的。
-
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是"被允许",并不是和对象一样,没有引用了必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息。
在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 以及 OSGI 这类频繁自定义类加载器的场景,通常都需要 java 虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。