1.堆 栈 方法区的交互关系
1.数据运行时完成结构
- 从线程共享与否的角度来看
- 栈 , 堆 , 方法区的交互关系
2.方法区的理解
<
所以 , 方法区看作是一块独立于Java堆的内存空间
1.方法区的基本理解
- 方法区(Method Area)语Java堆一样, 是各个线程共享的内存区域
- 方法区在JVM启动的时候被创建, 并且他的实际的物理内存空间中和Java堆区一样都可以是不连续的
- 方法区的大小 , 跟堆空间一样, 可以选择固定大小或者可扩展
- 方法区的大小决定了系统可以保存多少个类 , 如果系统定义了太多的类, 导致方法区溢出,虚拟机同样回抛出内存溢出错误: java.lang.OutOfMemoryError: PermGen space 或者 java.lang.OutOfMemoryError: Metaspace
-
2.Hotspot 中方法区的演技
在jdk7及以前 , 习惯上把方法区, 称之为永久代. jdk8开始, 使用元空间取代了永久代.
- 本质上 , 方法区和永久代并不等价, 仅仅是对hotspot而言的, <
>对如何实现方法区, 不做统一要求 . 例如: BEA JRockit/IBM J9 中不存在永久代的概念 - 现在来看, 在jdk8以前 , 使用永久代的方式更加容易造成Java程序OOM(超过:MaxPermSize上限)
3.方法区7/8概述
4.Hotspot 中方法区的演进
- 而到了JDK 8 中, 终于完全废弃了永久代的概念, 改而用JRockit , J9一样的本地内存中实现的元空间(Meta Space)来替代.
- 元空间的本质和永久代类似, 都是对JVM规范中方法区的实现, 不过元空间与永久代最大的区域在于: 元空间不在虚拟机设置的内存中, 而是使用了本地内存
- 永久带, 元空间二者并不只是名字变了. 内部结构也调整了
- 根据<
>的规定, 如果方法区无法满足新的内存分配需求时, 将抛出OOM异常.
3.设置方法区大小和OOM
- 方法区大小不必是固定的, jvm可以根据应用的需要动态调整
JDK7及以前:
- 通过-XX:PermSize来设置永久代初始化分配空间, 默认值是20.75M
- -XX:MaxPermSize来设定永久代最大可分配空间, 32位机器默认是64M, 64位机器默认是82M
- 当JVM加载的类信息容量超过了这个值, 会报异常OutOfMemoryError:PermGen space
- jpd 查看进程Id , jinfo -flag PermSize [进程Id] jinfo -flag MaxPermSize [进程Id]
JDK8以后:
- 元数据区大小可以使用参数-XX:MetaspaceSize:和-XX:MaxMetaspaceSize指定, 替代上述原有的两个参数
- 默认值依赖于平台. windows下, -XX:MetaspaceSize是21M , -XX:MaxMetaspaceSize的值是-1,即没有限制
- 与永久代不同 ,如果不指定大小, 默认情况下, 虚拟机会耗尽所有的可用系统内存. 如果元数据区发生溢出
- -XX:MetaspaceSize: 设置初始的元空间大小 , 对于一个64位的服务器端JVM来说 , 其默认的-XX:MetaspaceSize值为21, 这个就是初始的高水位线, 一旦触及这个水位线,Full GC将会被触及并卸载没用的类(即这些对应的类加载器不再存活),然后这个高水位线将会重置,新的高水位线的值取决于GC后释放了多少元空间, 如果释放的空间不足, 那么在不超过MaxMetaspaceSize时, 适当提高该值, 如果释放空间过多, 则适当降低该值
- 如果初始化的高位水线设置过低 , 上述高水位线调整情况会发生很多次, 通过垃圾回收器的日志可以观察到Full GC多次调用, 为了避免频繁地GC,建议将-XX:MetaspaceSize设置一个相对较高的值
1.MetaSpace 举例
```java
import jdk.internal.org.objectweb.asm.ClassWriter; import jdk.internal.org.objectweb.asm.Opcodes;
/**
- JDK6/7: -XX:PermSize=5m -XX:MaxPermSize=5m *
- JDK8以上: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m *
- Exception in thread “main” java.lang.OutOfMemoryError: Compressed class space *
- @author anda
@since 1.0 */ public class OOMTestDemo extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTestDemo oomTestDemo = new OOMTestDemo();
for (int i = 0; i < 10000; i++) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] bytes = classWriter.toByteArray();
oomTestDemo.defineClass("Class" + i, bytes, 0, bytes.length);
j++;
}
} finally {
System.out.println("j = " + j);
}
} }
<a name="hzira"></a>
## 2.如何解决这些OOM
1. 要解决OOM异常或heap space的异常, 一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer) 对dump , 出来的堆转储快照进行分析, 重点是确认内存中的对象是否有必要的, 也就是要分清楚到底是出现内存泄露(Memory Leak) 还是内存溢出(Memory Overflow)
1. 如果内存泄露 , 可以进一步通过工具查看泄露对象到GC Roots 的引用链, 于是就能找到泄露对象是通过怎么的路径与GC Roots相关联导致垃圾收集器无法自动回收他们的, 掌握了泄露对象的类型信息, 以及 GC Roots引用链的信息, 就可以比较准确地定位出泄露代码的位置
1. 若果不存在内存泄露, 换句话说 , 就是内存中的对象确实都还必须存活着, 那就应当检查虚拟机的堆参数(-Xmx与-Xms), 与机器物理内存比对看是否还可以调大, 从代码上检查是否存在某些对象生命周期过长, 持有状态时间过长的情况, 尝试减少程序运行时的内存消耗
<a name="nc3mx"></a>
# 4.方法区内部结构
<a name="ngZdP"></a>
## 1.方法区图解
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25783451/1652714657239-f49284de-1973-4f68-8d43-31d46ba034c6.png#clientId=ua4ab3b3f-4a08-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=491&id=u2190cd0e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=982&originWidth=1984&originalType=binary&ratio=1&rotation=0&showTitle=false&size=125594&status=done&style=none&taskId=u420fa612-7746-42db-9a20-79e80a4957e&title=&width=992)
<a name="h7AFE"></a>
## 2.方法区(Method Area)存储什么?
<<深入理解Java虚拟机>>书中堆方法区(Method Area)存储内容描述如下:<br />它用于存储已被虚拟机加载的 **类型信息 , 常量 , 静态变量 , 即时编译器编译后的代码缓存**等.<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25783451/1652715091656-cd9e6ddf-3ecb-4548-bfa1-d1d7c66dc0d8.png#clientId=ua4ab3b3f-4a08-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=129&id=uf941cf9f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=258&originWidth=1574&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30543&status=done&style=none&taskId=ube8de63d-a232-4cbe-8f25-e9cf4fbf8a6&title=&width=787)
- 类信息
对每个加载的类型(类class, 接口interface , 枚举 enum , 注解 annotation), JVM必须在方法区存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类型)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
- 这个类型的修饰符(public , abstract , final的某个子集)
- 这个类型直接接口的一个有序列表
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25783451/1652715536487-1b601245-01c7-47a8-884f-8563f1fe0ad8.png#clientId=ua4ab3b3f-4a08-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=175&id=u2dc8fef1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=350&originWidth=2146&originalType=binary&ratio=1&rotation=0&showTitle=false&size=76098&status=done&style=none&taskId=ua8e8a96d-f11a-4b79-a3fb-78a507825cd&title=&width=1073)
- 域(Field)信息
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括, 域名称 , 域类型, 域修饰符(public , private , protected , static , final , volatile , transient某个子集)
- 方法(Method)信息
- 方法名称
- 方法的返回类型(或void)
- 方法的修饰符(public , private , protected , static , final , synchronized1 , native , abstract的一个子集)
- 方法的字节码(bytecodes) , 操作数栈 , 局部变量表以及大小(abstract和native方法除外)
- 异常表(abstract 和 native 方法除外)
- 每个异常处理的开始位置 , 结束位置, 代码处理在程序计数器中的偏移地址 , 被捕获的异常类的常量池索引
- non-final的类变量
- 静态变量和类关联在一起, 随着类的加载而加载 , 他们成为类数据在逻辑上的一部分
- 类变量被类的所有实例共享, 即使没有类实例时你也可以访问它.
```java
class OrderTest {
public static int count = 1;
public static final int count_1 = 1;
public static String count_2 = "2";
public static final String count_3 = "4";
}
补充说明:
被声明为final的类变量, 的处理方法不同, 每个全局常量在编译的时候就会被分配了
以上不全对 , 如果使用final+static, 只有一下情况才会被分配
- 是基本类型或者是String类型 , 并且是显式赋值 , 如果使用方法,构造器赋值等等就会出现在初始化时期才会分配这个变量, 比如 ```java private static final String test_ = new String(“123”);这里赋值就是使用构造器赋值 , 所以会在初始化时期才会分配变量
private static final Integer test_2 = 3; 这个是不是也是显式赋值, 那么会在编译时期分配呢 , 答案不是 , 也会在初始化时期分配 , 原因是, Integer底层会使用Integer.valueOf进行装箱操作.相当于是方法赋值了
<a name="OPgHe"></a>
## 3.常量池
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25783451/1652717531513-9261c681-c33d-49cb-9b91-f59adf696b87.png#clientId=ua4ab3b3f-4a08-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=521&id=uc0164442&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1042&originWidth=1858&originalType=binary&ratio=1&rotation=0&showTitle=false&size=133499&status=done&style=none&taskId=u5ed15a43-5929-4f73-85b5-dc91d634d1d&title=&width=929)
- 方法区, 内部包含了运行时常量池.
- 字节码文件 , 内部包含了常量池
- 要弄清楚方法区 , 需要理解清楚ClassFile, 因为加载类的信息都是在方法区.
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25783451/1652718652234-68ba627e-ccb3-4ccf-896a-41a6182a4ade.png#clientId=ua4ab3b3f-4a08-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=328&id=ue236d592&margin=%5Bobject%20Object%5D&name=image.png&originHeight=656&originWidth=1782&originalType=binary&ratio=1&rotation=0&showTitle=false&size=126057&status=done&style=none&taskId=u8e52cbb8-35c2-44e2-ba89-d6488b6917d&title=&width=891)<br />一个有效的字节码文件中除了包含类的版本信息, 字段 ,方法以及接口等描述信息外,还包含一项信息就是常量池表(Constant Pool Table , 包括各种字面量和对类型, 域, 和方法的符号引用)
<a name="dNpI3"></a>
## 4.为什么需要常量池?
一个java源文件中的类,接口 , 编译后产生一个字节码文件 . 而Java中, 的字节码需要数据支持, 通常这种数据会很大以至于不能直接存到字节码里面, 换另一种方式,可以存到常量池.这个字节码包含了指向常量池的引用, 在动态链接的时候会用到运行时常量池.
- 运行时常量池(Runtime Constant Pool) 是方法区的一部分
- 常量池表(Constant Pool Table)是Class文件的一部分, 用于存放编译期生成的各种字面量与符号引用, 这部分内容将在类加载存放到方法区的运行时常量池中.
- 运行时常量池,在加载类和接口到虚拟机后 , 就会创建对应的运行时常量池
- JVM为每个已加载的类型(类或接口)都维护一个常量池 , 池中的数据项像数组项一样, 是通过索引访问的
- 运行时常量池中包含多种不同的常量, 包括编译器就已经明确的数值字面量, 也包括到运行期解析后才能够获得的方法或者字段引用, 此时不再是常量池中的符号地址了, 这里换位真实地址
- 运行时常量池, 相对于Class文件常量池的另一重要特征: 具备动态性
- 运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富一些.
- 当创建类或接口的运行时常量池,如果构造器运行时常量池所需要的的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemoryError异常.
<a name="m392T"></a>
## 5.图示方法区的使用
```java
/**
* @author anda
* @since 1.0
*/
public class MethodAreaDemo {
/**
* @author anda
* @since 1.0
*/
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
}
6.方法区的演进细节
- 首先明确:只有HotSpot 才有永久代.
- HotSpot 中方法区的变化
- jdk1.6之前: 有永久代(permanent generation), 静态变量存放在永久代上
- jdk1.7: 有永久代,但已经逐步”去永久代”,字符串常量池,静态变量移除,保存在堆中.
- jdk1.8及以后: 无永久代, 类型, 字段 , 方法, 常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆
1.永久代为什么要被元空间替换
- 随着Java8的到来, Hotspot VM中再无永久代, 但是这并并不意味着类的元数据信息也消失了.这些数据被移到了一个与堆不相连的本地内存区域, 这个区域叫做元空间(Metaspace)
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间, 这项改动很有必要的:
- 为什么永久代设置大小是很难确定的?
- 因为永久代设置空间大小是很难确定的. 在某些场景下,如果动态加载类过多,容易产生Perm区的OOM,比如某个实际Web工程中,因为功能点比较多, 在运行过程中,要不断的动态加载很多的类,经常会出现致命的错误 java.lang.OutOfMemoryError:PermGen , 而元空间和永久代之间最大的区别在于,元空间并不在虚拟机中, 而是使用本地内存 . 因此, 默认情况下, 元空间的大小仅仅受本地内存限制
- 对永久代进行调优很是困难的?
- 主要是为了降低Full GC.
- 为什么永久代设置大小是很难确定的?
2.String Table为什么调整位置
- JDK7中将StringTable放到了堆空间中,因为永久代的回收效率很低, 在Full GC的时候才会触发,而Full GC是老年代的空间不足, 永久代不足时才会触发
- 这里就导致了string table回收效率不高, 而我们开发中会有大量的字符串被创建, 回收效率低, 导致永久代内存不足, 放到堆里, 这里能够及时回收
3.方法区的垃圾回收
- 有些人认为方法区(如Hotspot虚拟机中的元空间或者永久代)是没有垃圾收集行为的, 其实不然,<
>对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或者未能完整实现方法区类型卸载的收集器存在(JDK11时期的ZGC收集器就不支持类的卸载) - 般来说这个区域的回收结果比较难令人满意,尤其是类型的卸载, 条件相当苛刻, 但是这部分区域的回收有时又确实是有必要的, 避免内存泄露造成的问题
- 方法区的垃圾收集主要回收两部分内容,常量池中废弃的常量和不在使用的类型.
- 先来说说方法区内, 常量池之中主要存放的两大常量, 字面量和符号引用, 字面量比较接近Java语言层次的常量概念 , 如文本字符串, 被声明为final的常量池, 而符号引用则属于编译原理方面的概念, 包括下面三类常量:
- 类的接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- Hotspot虚拟机常量池的回收策略是很明确的, 只要常量池中的常量没有被任何地方引用,就可以回收.
- 回收废弃的常量与回收Java堆中的非常类似
- 判定一个常量是否”废弃”还是相对简单,而且还要判定一个类型是否属于”不在被使用的类”的条件就比较苛刻,需要同时满足下面三个条件:
- 该类所有的实例都已经被回收, 也就是Java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收, 这个条件除非是经过精心设计的可替换类加载器的场景, 如OSGI,JPS的重载.
- 该类对应的ava.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法
- Java虚拟机被允许满足上述三个条件的无用类进行回收,这里说的仅仅是”被允许”, 而并不是和对象一样,没有引用了就必然会回收,关于是否要对类型进行回收, hotspot虚拟机提供了-Xnoclassgc参数进行控制, 还可以使用-verbose:class以及-XX:+TraceClass-Loading -XX:+TraceClassUnLoading查看类加载和卸载信息.
- 在大量使用反射, 动态代理, CGLib等字节码框架, 动态生成JSP以及OSGI这类频繁自定义类加载器的场景中, 通常都需要Java虚拟机具备类型卸载的能力 , 以保证不会堆方法区造成过大的内存压力
- 先来说说方法区内, 常量池之中主要存放的两大常量, 字面量和符号引用, 字面量比较接近Java语言层次的常量概念 , 如文本字符串, 被声明为final的常量池, 而符号引用则属于编译原理方面的概念, 包括下面三类常量: