共计 6990 个字符,预计需要花费 18 分钟才能阅读完成。
一、概述
1.1 办法区了解
《Java 虚拟机标准》中明确阐明:“只管所有的办法区在逻辑上是属于堆的一部分,但一些简略的实现可能不会抉择去进行垃圾收集或者进行压缩。”但对于 HotSpot JVM 而言,办法区还有一个别名叫做 Non-Heap(非堆),目标就是要和堆离开。
所以,办法区看作是一块独立于 Java 堆的内存空间。
办法区次要寄存的是 Class,而堆中次要寄存的是 实例化的对象。
- 办法区(Method Area)与 Java 堆 一样,是各个线程共享的内存区域。
- 办法区在 JVM 启动的时候被创立,并且它的理论的物理内存空间中和 Java 堆区一样都能够是不间断的。
- 办法区的大小,跟堆空间一样,能够抉择固定大小或者可扩大。
-
办法区的大小决定了零碎能够保留多少个类,如果零碎定义了太多的类,导致办法区溢出,虚拟机同样会抛出内存溢出谬误:
java.lang.OutofMemoryError:PermGen space
或者java.lang.OutOfMemoryError:Metaspace
- 加载大量第三方的 jar 包
- Tomcat 部署的工程过多(30~50 个)
- 大量动静的生成反射类
- 敞开 JVM 就会开释这个区域的内存。
1.2 办法区的演进
在 jdk7 及以前,习惯上把办法区,称为永恒代。jdk8 开始,应用元空间取代了永恒代。
- JDK 1.8 后,元空间寄存在堆外内存中。
实质上,办法区和永恒代并不等价。仅是对 HotSpot 而言的。《Java 虚拟机标准》对如何实现办法区,不做对立要求。例如:BEA JRockit / IBM J9 中不存在永恒代的概念。
永恒代更容易导致 Java 程序更容易 OOM(超过 -XX:MaxPermsize 下限)
而到了 JDK8,终于齐全废除了永恒代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间(Metaspace)来代替。
元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。不过元空间与永恒代最大的区别在于:元空间不在虚拟机设置的内存中,而是应用本地内存。
永恒代、元空间二者并不只是名字变了,内部结构也调整了。依据《Java 虚拟机标准》的规定,如果办法区无奈满足新的内存调配需要时,将抛出 OOM 异样。
1.3 设置办法区大小
办法区的大小不用是固定的,JVM 能够依据利用的须要动静调整。
jdk7 及以前
- 通过
-XX:Permsize
来设置永恒代初始调配空间。默认值是 20.75M。 -XX:MaxPermsize
来设定永恒代最大可调配空间。32 位机器默认是 64M,64 位机器模式是 82M。- 当 JVM 加载的类信息容量超过了这个值,会报异样
OutofMemoryError:PermGen space
。
JDK8 当前
元数据区大小能够应用参数 -XX:MetaspaceSize
和 -XX:MaxMetaspaceSize
指定。
默认值依赖于平台。Windows 下,-XX:MetaspaceSize
是 21M,-XX:MaxMetaspaceSize
的值是 -1,即没有限度。
与永恒代不同,如果不指定大小,默认状况下,虚构机会耗尽所有的可用零碎内存。如果元数据区产生溢出,虚拟机一样会抛出异样 OutOfMemoryError:Metaspace
。
-XX:MetaspaceSize
:设置初始的元空间大小。对于一个 64 位的服务器端 JVM 来说,其默认的 -XX:MetaspaceSize
值为 21MB。这就是初始的高水位线,一旦涉及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)而后这个高水位线将会重置。新的高水位线的值取决于 GC 后开释了多少元空间。如果开释的空间有余,那么在不超过 MaxMetaspaceSize
时,适当进步该值。如果开释空间过多,则适当升高该值。
如果初始化的高水位线设置过低,上述高水位线调整状况会产生很屡次。通过垃圾回收器的日志能够察看到 FullGC 屡次调用。为了防止频繁地 GC,倡议将 -XX:MetaspaceSize
设置为一个绝对较高的值。
如何解决 OOM
-
要解决 OOM 异样或 heap space 的异样,个别的伎俩是首先通过内存映像剖析工具(如 Eclipse Memory Analyzer)对 dump 进去的堆转储快照进行剖析,重点是确认内存中的对象是否是必要的,也就是要先分分明到底是呈现了 内存透露 (Memory Leak)还是 内存溢出(Memory Overflow)
- 内存透露就是 有大量的援用指向某些对象,然而这些对象当前不会应用了,然而因为它们还和 GC ROOT 有关联,所以导致当前这些对象也不会被回收,这就是内存透露的问题
- 如果是内存透露,可进一步通过工具查看透露对象到 GC Roots 的援用链。于是就能找到透露对象是通过怎么的门路与 GCRoots 相关联并导致垃圾收集器无奈主动回收它们的。把握了透露对象的类型信息,以及 GCRoots 援用链的信息,就能够比拟精确地定位出透露代码的地位。
- 如果不存在内存透露,换句话说就是内存中的对象的确都还必须存活着,那就该当查看虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存比照看是否还能够调大,从代码上查看是否存在某些对象生命周期过长、持有状态工夫过长的状况,尝试缩小程序运行期的内存耗费。
二、办法区的内部结构
《深刻了解 Java 虚拟机》书中对办法区(Method Area)存储内容形容如下:它用于存储已被虚拟机加载的 类型信息 、 常量 、 动态变量 、 即时编译器编译后的代码缓存 等。
2.1 类型信息
对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVm 必须在办法区中存储以下类型信息:
- 这个类型的残缺无效名称(全名 = 包名. 类名)
- 这个类型间接父类的残缺无效名(对于 interface 或是 java.lang.object,都没有父类)
- 这个类型的修饰符(public,abstract,final 的某个子集)
- 这个类型间接接口的一个有序列表
2.2 域信息
JVM 必须在办法区中保留类型的所有域的相干信息以及域的申明程序。
域的相干信息包含:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient 的某个子集)
2.3 办法信息
JVM 必须保留所有办法的以下信息,同域信息一样包含申明程序:
- 办法名称
- 办法的返回类型(或 void)
- 办法参数的数量和类型(按程序)
- 办法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
- 办法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 办法除外)
- 异样表(abstract 和 native 办法除外)
每个异样解决的开始地位、完结地位、代码解决在程序计数器中的偏移地址、被捕捉的异样类的常量池索引。
2.4 non-final 的类变量
动态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分,类变量被类的所有实例共享,即便没有类实例时,也能够拜访它。
public class MethodAreaTest {public static void main(String[] args) {Order order = new Order();
order.hello();
System.out.println(order.count);
}
}
class Order {
public static int count = 1;
public static final int number = 2;
public static void hello() {System.out.println("hello!");
}
}
如上代码所示,即便咱们把 order 设置为 null,也不会呈现空指针异样。
2.5 运行时常量池
(1)常量池
Java 中的常量池,实际上分为两种状态:动态常量池 和运行时常量池。
所谓 动态常量池 ,即 *.class 文件中的常量池,class 文件中的常量池不仅仅蕴含字符串(数字) 字面量,还蕴含类、办法的信息,占用 class 文件绝大部分空间。这种常量池次要用于寄存两大类常量:字面量 (Literal) 和符号援用量(Symbolic References),字面量相当于 Java 语言层面常量的概念,如文本字符串,申明为 final 的常量值等,符号援用则属于编译原理方面的概念,包含了如下三种类型的常量:
- 类和接口的全限定名
- 字段名称和描述符
- 办法名称和描述符
为什么须要常量池
一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码须要数据反对,通常这种数据会很大以至于不能间接存到字节码里,换另一种形式,能够存到常量池,这个字节码蕴含了指向常量池的援用。
比方:
public class SimpleClass {public void sayHello() {System.out.println("hello");
}
}
尽管上述代码只有 194 字节,然而外面却应用了 String、System、PrintStream 及 Object 等构造。这里的代码量其实很少了,如果代码多的话,援用的构造将会更多,这里就须要用到常量池了。
常量池中有什么
- 数量值
- 字符串值
- 类援用
- 字段援用
- 办法援用
例如上面这段代码:
public class MethodAreaTest2 {public static void main(String args[]) {Object obj = new Object();
}
}
将会被翻译成如下字节码:
new #2
dup
invokespecial
小结
常量池、能够看做是一张表,虚拟机指令依据这张常量表找到要执行的类名、办法名、参数类型、字面量等类型。
(2)运行时常量池
运行时常量池(Runtime Constant Pool)是办法区的一部分。
常量池表(Constant Pool Table)是 Class 文件的一部分,用于寄存编译期生成的各种字面量与符号援用,这部分内容将在类加载后寄存到办法区的运行时常量池中。
运行时常量池,在加载类和接口到虚拟机后,就会创立对应的运行时常量池。
JVM 为每个已加载的类型(类或接口)都保护一个常量池。池中的数据项像数组项一样,是通过索引拜访的。
运行时常量池中蕴含多种不同的常量,包含编译期就曾经明确的数值字面量,也包含到运行期解析后才可能取得的办法或者字段援用。此时不再是常量池中的符号地址了,这里换为实在地址。
运行时常量池相似于传统编程语言中的符号表(symboltable),然而它所蕴含的数据却比符号表要更加丰盛一些。当创立类或接口的运行时常量池时,如果结构运行时常量池所需的内存空间超过了办法区所能提供的最大值,则 JVM 会抛 OutOfMemoryError 异样。
(3)图解实例
如下代码
public class MethodAreaDemo {public static void main(String args[]) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a+b);
}
}
字节码执行过程展现:
首先现将操作数 500 放入到操作数栈中:
而后再存储到局部变量表中:
而后反复一次,把 100 放入局部变量表中,最初再将变量表中的 500 和 100 取出,进行操作:
将 500 和 100 进行一个除法运算,后果入栈:
最初就是输入流,须要调用运行时常量池的常量:
最初调用 invokevirtual(虚办法调用),而后返回:
返回:
程序计数器始终计算的都是以后代码运行的地位,目标是为了不便记录 办法调用后可能失常返回,或者是进行了 CPU 切换后,也能回来到原来的代码进行执行。
2.6 办法区的演进细节
只有 Hotspot 才有永恒代。BEA JRockit、IBM J9 等来说,是不存在永恒代的概念的。原则上如何实现办法区属于虚拟机实现细节,不受《Java 虚拟机标准》管教,并不要求对立。
Hotspot 中办法区的变动:
版本 | 次要变动 |
---|---|
JDK1.6 及以前 | 有永恒代(permanent generation),动态变量存储在永恒代上 |
JDK1.7 | 有永恒代,但曾经逐渐“去永恒代”,字符串常量池,动态变量移除,保留在堆中 |
JDK1.8 | 无永恒代,类型信息,字段,办法,常量保留在本地内存的元空间,但字符串常量池、动态变量依然在堆中。 |
JDK6 的时候:
JDK7 的时候:
JDK8 的时候,元空间大小只受物理内存影响:
为什么永恒代要被元空间代替?
JRockit 是和 HotSpot 交融后的后果,因为 JRockit 没有永恒代,所以 HotSpot 不须要配置永恒代。随着 Java8 的到来,HotSpot VM 中再也见不到永恒代了。然而这并不意味着类的元数据信息也隐没了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做 元空间(Metaspace)。
因为类的元数据调配在本地内存中,元空间的最大可调配空间就是零碎可用内存空间,这项改变是很有必要的,起因有:
- 为永恒代设置空间大小是很难确定的。
在某些场景下,如果动静加载类过多,容易产生 Perm 区的 OOM。比方某个理论 Web 工程中,因为性能点比拟多,在运行过程中,要一直动静加载很多类,经常出现致命谬误。
“Exception in thread‘dubbo client x.x connector’java.lang.OutOfMemoryError:PermGen space”
而元空间和永恒代之间最大的区别在于:元空间并不在虚拟机中,而是应用本地内存。因而,默认状况下,元空间的大小仅受本地内存限度。
-
对永恒代进行调优是很艰难的。
- 次要是为了升高 Full GC
StringTable 为什么要调整地位?
jdk7 中将 StringTable 放到了堆空间中。因为永恒代的回收效率很低,在 Full GC 的时候才会触发。而 Full GC 是老年代的空间有余、永恒代有余时才会触发。
这就导致 StringTable 回收效率不高。而咱们开发中会有大量的字符串被创立,回收效率低,导致永恒代内存不足。放到堆里,能及时回收内存。
动态变量寄存在那里?
动态援用对应的对象实体始终都存在堆空间,只有是对象实例必然会在 Java 堆中调配。
从《Java 虚拟机标准》所定义的概念模型来看,所有 Class 相干的信息都应该寄存在办法区之中,但办法区该如何实现,《Java 虚拟机标准》并未做出规定,这就成了一件容许不同虚拟机本人灵便把握的事件。JDK7 及其当前版本的 HotSpot 虚拟机抉择把动态变量与类型在 Java 语言一端的映射 class 对象寄存在一起,存储于 Java 堆之中。
三、办法区的垃圾回收
有些人认为办法区(如 HotSpot 虚拟机中的元空间或者永恒代)是没有垃圾收集行为的,其实不然。《Java 虚拟机标准》对办法区的束缚是十分宽松的,提到过能够不要求虚拟机在办法区中实现垃圾收集。事实上也的确有未实现或未能残缺实现办法区类型卸载的收集器存在(如 JDK11 期间的 zGC 收集器就不反对类卸载)。
一般来说这个区域的回收成果比拟难令人满意,尤其是类型的卸载,条件相当刻薄。然而这部分区域的回收 有时又的确是必要的。以前 Sun 公司的 Bug 列表中,曾呈现过的若干个重大的 Bug 就是因为低版本的 HotSpot 虚拟机对此区域未齐全回收而导致内存透露。
办法区的垃圾收集次要回收两局部内容:常量池中废除的常量和不再应用的类型。
先来说说办法区内常量池之中次要寄存的两大类常量:字面量和符号援用。字面量比拟靠近 Java 语言档次的常量概念,如文本字符串、被申明为 final 的常量值等。而符号援用则属于编译原理方面的概念,包含上面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 办法的名称和描述符
HotSpot 虚拟机对常量池的回收策略是很明确的,只有常量池中的常量没有被任何中央援用,就能够被回收。
回收废除常量与回收 Java 堆中的对象十分相似。(对于常量的回收比较简单,重点是类的回收)
断定一个常量是否“废除”还是绝对简略,而要断定一个类型是否属于“不再被应用的类”的条件就比拟刻薄了。须要同时满足上面三个条件:
- 该类所有的实例都曾经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器曾经被回收,这个条件除非是通过精心设计的可替换类加载器的场景,如 osGi、JSP 的重加载等,否则通常是很难达成的。 - 该类对应的 java.lang.C1ass 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。I Java 虚拟机被容许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被容许”,而并不是和对象一样,没有援用了就必然会回收。对于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行管制,还能够应用
-verbose:class
以及-XX:+TraceClass-Loading
、-XX:+TraceClassUnLoading
查看类加载和卸载信息 - 在大量应用反射、动静代理、CGLib 等字节码框架,动静生成 JSP 以及 oSGi 这类频繁自定义类加载器的场景中,通常都须要 Java 虚拟机具备类型卸载的能力,以保障不会对办法区造成过大的内存压力。
参考
深刻了解 Java 虚拟机:JVM 高级个性与最佳实际(第 3 版)
触摸 java 常量池