目录
JVM内存结构
一、程序计数器
1.1 Java程序执行过程
1.2 程序计数器作用
1.3 程序计数器特点
1.3.1 线程私有
1.3.2 不存在内存溢出OutOfMemoryError
二、虚拟机栈
2.1 定义
2.2 特点
2.3 虚拟机栈内存什么
2.4 栈内存溢出
2.4.1 栈帧过多内存溢出
2.4.2 栈帧过大内存溢出
2.5 线程诊断
三、本地方法栈
3.1 与虚拟机栈对比
3.2 Native Method
3.2.1 什么是Native Method
3.2.2 为什么调用Native Method
3.3 JVM时如何跑一个本地方法的
四、堆
4.1 堆(heap)的概述
4.2 堆内存结构
4.3 分代
4.3.1 分代的概念
4.3.2 分代结构
五、方法区
5.1 方法区、堆、栈的配合关系
5.2 方法区概述
5.3 方法区的演进
5.3.1 永久代
5.3.2 元空间
5.4 方法区内部结构
5.4.1 具体分析
5.4.2 类型信息
5.4.3 域(Field)(属性)信息
5.4.4 方法(Method)信息
5.4.5 non-final的类变量
5.4.6 全局常量(static+final)
5.5 常量池
5.5.1 什么是常量池
5.5.2 常量池有什么用
5.5.3 方法区使用举例
5.5.4 运行时数据区的详细结果
5.5.5 常量池里有什么
5.6 一些问题
5.6.1 为什么要把永久代改为元空间
5.6.2 String Table为什么要放到堆区
5.7 方法区的垃圾回收
文章内容系个人学习笔记、网课截图和笔记、站内博客资料,如有冒犯,请联系我。
JVM内存结构
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分为若干个不同的数据区域。每个区域都有各自的作用。
分析 JVM 内存结构,主要就是分析 JVM 运行时数据存储区域。JVM 的运行时数据区主要包括:堆、栈、方法区、程序计数器等。
而 JVM 的优化问题主要在线程共享的数据区中:堆、方法区。
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分为若干个不同的数据区域。每个区域都有各自的作用。
分析 JVM 内存结构,主要就是分析 JVM 运行时数据存储区域。JVM 的运行时数据区主要包括:堆、栈、方法区、程序计数器等。
而 JVM 的优化问题主要在线程共享的数据区中:堆、方法区。
一、程序计数器
Program Counter Register 程序计数器是JVM内存结构的组成部分
一个线程的执行,是通过字节码解释器改变当前线程的程序计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行
1.1 Java程序执行过程
1、 Java源代码经过编译成为二进制字节码(Jvm指令
2、二进制字节码(Jvm指令)通过 解释器 解释成 机器码
3、将机器码交给 CPU 执行
1.2 程序计数器作用
在指令执行过程中记录下一条Jvm指令的执行地址
1.3 程序计数器特点
1.3.1 线程私有
Program Counter Register 程序计数器是JVM内存结构的组成部分
一个线程的执行,是通过字节码解释器改变当前线程的程序计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行
1、 Java源代码经过编译成为二进制字节码(Jvm指令
2、二进制字节码(Jvm指令)通过 解释器 解释成 机器码
3、将机器码交给 CPU 执行
1.2 程序计数器作用
在指令执行过程中记录下一条Jvm指令的执行地址
1.3 程序计数器特点
1.3.1 线程私有
在指令执行过程中记录下一条Jvm指令的执行地址
1.3.1 线程私有
解释:每一个线程都有属于它自己的程序计数器,即私有的内存
假设有两个线程:线程1 和 线程2
当 线程1 执行到地址为 10 的Jvm指令,
此时程序计数器内存的指令地址为 11 ,
碰巧线程1的CPU时间片用完了,轮到 线程2 执行(上下文切换)
此时 线程1 的程序计数器保持不变,线程2 有它自己的程序计数器
1.3.2 不存在内存溢出OutOfMemoryError
二、虚拟机栈
2.1 定义
栈是线程运行需要的内存空间-是线程私有的,它的生命周期与线程相同。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间
2.2 特点
限定仅在表头进行插入和删除操作的线性表。即压栈(入栈)和弹栈(出栈)都是对栈顶元素进行操作的,即先进后出的。
2.3 虚拟机栈内存什么
2.1 定义
栈是线程运行需要的内存空间-是线程私有的,它的生命周期与线程相同。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间
2.2 特点
限定仅在表头进行插入和删除操作的线性表。即压栈(入栈)和弹栈(出栈)都是对栈顶元素进行操作的,即先进后出的。
2.3 虚拟机栈内存什么
栈是线程运行需要的内存空间-是线程私有的,它的生命周期与线程相同。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间
限定仅在表头进行插入和删除操作的线性表。即压栈(入栈)和弹栈(出栈)都是对栈顶元素进行操作的,即先进后出的。
2.3 虚拟机栈内存什么
栈帧是栈的元素
① 栈帧是栈中的一个栈元素,是用于帮助虚拟机执行方法调用与方法执行的数据结构,当前线程中,每执行一个方法就会往栈中插入一个栈帧。
② 栈帧本身是一种数据结构,是每个方法运行时需要的内存,封装了方法的局部变量表、动态链接信息、方法返回地址(即返回到方法的调用者)以及操作数栈
③ Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,换句话说,每个线程都会有一个栈,所以对于栈帧来说不存在并发调用的情况,即线程安全④ 活动栈帧-栈顶的栈帧,即当前执行的栈帧
2.4 栈内存溢出
2.4.1 栈帧过多内存溢出
例如方法的设定不合理,导致无限的递归调用
StackOverflowError:栈溢出错误
如果一个线程在计算时所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出 StackOverflowError
OutOfMemoryError:内存不足
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
2.4.2 栈帧过大内存溢出
如栈帧的内存大小超出栈的大小(不易出现)
2.5 线程诊断
案例一:cpu占用过高Linux
- 运行一个java程序
- 使用top命令,查看后台进程对cpu的占用情况
- 查询进程的线程指定指标 ps H -eo pid,tid,%cpu | grep 进程ID
- 查看java进程下的线程运行情况 jstack 进程ID
- 根据ps H -eo pid,tid,%cpu | grep 进程ID 查到的线程ID(10进制)找到对应线程ID(16进制)的信息
三、本地方法栈
3.1 与虚拟机栈对比
① 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用类似
② 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地方法(Native Method)服务
③ 本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常
④ 虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它
当一个线程在运行时会调用JAVA方法和Native方法时,如下图所示(可以相互自由调用)
3.2 Native Method
3.2.1 什么是Native Method
一个Native Method就是一个Java调用非java代码的接口。
Native Method:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
3.2.2 为什么调用Native Method
案例一:cpu占用过高Linux
- 运行一个java程序
- 使用top命令,查看后台进程对cpu的占用情况
- 查询进程的线程指定指标 ps H -eo pid,tid,%cpu | grep 进程ID
- 查看java进程下的线程运行情况 jstack 进程ID
- 根据ps H -eo pid,tid,%cpu | grep 进程ID 查到的线程ID(10进制)找到对应线程ID(16进制)的信息
3.1 与虚拟机栈对比
① 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用类似
② 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地方法(Native Method)服务
③ 本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常
④ 虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它
当一个线程在运行时会调用JAVA方法和Native方法时,如下图所示(可以相互自由调用)
3.2 Native Method
3.2.1 什么是Native Method
一个Native Method就是一个Java调用非java代码的接口。
Native Method:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
3.2.2 为什么调用Native Method
① 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用类似
② 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地方法(Native Method)服务
③ 本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常
④ 虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它
当一个线程在运行时会调用JAVA方法和Native方法时,如下图所示(可以相互自由调用)
3.2.1 什么是Native Method
一个Native Method就是一个Java调用非java代码的接口。
Native Method:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
3.2.2 为什么调用Native Method
一个Native Method就是一个Java调用非java代码的接口。
Native Method:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
① 与java环境外交互:
有时java应用须要与java外面的环境交互。这是本地方法存在的主要缘由,你能够想一想java须要与一些底层系统如操做系统或某些硬件交换信息时的状况。本地方法正是这样一种交流机制:它为咱们提供了一个很是简洁的接口,并且咱们无需去了解java应用以外的繁琐的细节。
② 与操做系统交互:
JVM支持着java语言自己和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些链接到本地代码的库组成。然而无论怎 样,它毕竟不是一个完整的系统,它常常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统经常是强大的操做系统。经过使用本地方法,咱们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,若是咱们要使用一些java语言自己没有提供封装的操做系统的特性时,咱们也须要使用本地方法。
③ Sun's Java
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。
3.3 JVM时如何跑一个本地方法的
我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。
最后需要提示的是,使用本地方法是有开销的,它丧失了java的很多好处。如果别无选择,我们可以选择使用本地方法。
四、堆
4.1 堆(heap)的概述
- 一个进程对应一个JVM实例,一个JVM实例只有一个运行时数据区,所有的线程共享同一个堆,每个线程有私有的程序计数器,本地方法栈,虚拟机栈
- 堆是JAVA内存管理的核心区域,是JVM管理的最大的一块内存空间,一个JVM实例只存在一个堆内存
- 堆在JVM启动时被创建,堆内存大小是可以调节的
- 《JAVA虚拟机规范》规定了堆是物理上不连续,逻辑上连续
- 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收
- 类加载器读取类文件后,会把类,方法、常量、变量、放到堆中,保存我们所有引用类型的真实对象
我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。
最后需要提示的是,使用本地方法是有开销的,它丧失了java的很多好处。如果别无选择,我们可以选择使用本地方法。
4.1 堆(heap)的概述
- 一个进程对应一个JVM实例,一个JVM实例只有一个运行时数据区,所有的线程共享同一个堆,每个线程有私有的程序计数器,本地方法栈,虚拟机栈
- 堆是JAVA内存管理的核心区域,是JVM管理的最大的一块内存空间,一个JVM实例只存在一个堆内存
- 堆在JVM启动时被创建,堆内存大小是可以调节的
- 《JAVA虚拟机规范》规定了堆是物理上不连续,逻辑上连续
- 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收
- 类加载器读取类文件后,会把类,方法、常量、变量、放到堆中,保存我们所有引用类型的真实对象
4.2 堆内存结构
- JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
- 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
- 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
元空间有注意有两个参数:
- MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
- MetaspaceSize :初始化元空间大小,控制发生GC阈值
4.3 分代
4.3.1 分代的概念
1)分代的原因
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
2)分代概念
① 新生成的对象首先放到年轻代Eden区
② 当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区
③ Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区
④ 这样保证了一段时间内总有一个survivor区为空。
⑤ 经过多次Minor GC仍然存活的对象移动到老年代。
⑥ 老年代存储长期存活的对象,占满时会触发Major GC=Full GC⑦ GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。
Minor GC : 清理年轻代
Major GC : 清理老年代
Full GC : 清理整个堆空间,包括年轻代和永久代
所有GC都会停止应用所有线程。
4.3.2 分代结构
1)新生区
新生成的对象优先存放在新生区中,新生区对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。
HotSpot将新生区划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
① GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区(幸存区1)是空的(作为保留区域)。② GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区(幸存区0)中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生区中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年区中,没有达到阀值的对象会被复制到To Survivor区。
③ 接着清空Eden区和From Survivor区(幸存区0),新生区中存活的对象都在To Survivor区。
④ 接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的ToSurvivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。
⑤ GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年区进行分配担保,将这些对象存放在老年区中。
2)老年区
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
3)永久区
永久区存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
JDK1.6之前,永久代,常量池在方法区
JDK1.7,永久代,但是慢慢退化了,去永久代,常量池在堆中
JDK1.8之后,无永久代,常量池在元空间
这个区域常驻内存,用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行的一些环境或者类信息,这个区域不存在垃圾回收。关闭VM虚拟机就会释放这个区域的内存。
一个启动类加载了第三方jar包,tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM(Out of Memory)
4)OOM(Out of Memory)常见原因
① 老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
② 永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
③ 代码bug,占用内存无法及时回收。
OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。可以通过添加个参数-XX:+HeapDumpOnOutMemoryError,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。
五、方法区
5.1 方法区、堆、栈的配合关系
线程共享角度
交互关系
5.2 方法区概述
方法区可以看作是一块独立于JAVA堆的内存空间
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
- 方法区的大小决定可系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace
- 关闭JVM会释放这个区域的内存
怎么会导致方法区的OOM?
方法区主要是存储类信息,那么当加载大量的jar包:Tomcat部署的工程过多(30-50)个、大量动态的生成反射类都会造成OOM
5.3 方法区的演进
5.3.1 永久代
JDK1.8之前方法区使用堆的内存,称为永久代
5.3.2 元空间
JDK1.8之后使用的是操作系统的内存,称为元空间
① 本质上,方法区和永久代并不等价。仅是对HotSpot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求
② 到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替
③ 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存
④ 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常
5.4 方法区内部结构
5.4.1 具体分析
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存(随着JDK的更新迭代,会发生变化)
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存(随着JDK的更新迭代,会发生变化)
5.4.2 类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名:包名+类名)
- 这个类型直接父类的完整有效名(对于interface或者Object来说都是没有父类的)
- 这个类型的修饰符(public ,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
5.4.3 域(Field)(属性)信息
其实这里的域实际上指的就是Java中的属性(成员变量)
- JVM必须在方法区中保存类型的所有域的相关信息和声明顺序。
- 也就是说在Java类中我们属性声明的顺序也就决定了在字节码中的顺序,从而确定了在方法区中存储的顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
5.4.4 方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的某个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表(abstract和native方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
5.4.5 non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。
- 类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
5.4.6 全局常量(static+final)
与被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
5.5 常量池
5.5.1 什么是常量池
1)Class文件常量池
Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poll Table),包括各种字面量和对类型、域和方法的符号引用。
什么是字面量和符号引用?
- 字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;方法内的常量值由栈分配,所以并不算是字面量。
- 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用
2)运行时常量池
运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。
一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用(真实地址)。
运行时常量池就是用来索引和查找字段、方法名称、描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息。
运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemooryError异常。
3)字符串常量池
- 在 JDK1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的对象。
- 在 JDK1.7(含)之后,是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中。
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名:包名+类名)
- 这个类型直接父类的完整有效名(对于interface或者Object来说都是没有父类的)
- 这个类型的修饰符(public ,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
其实这里的域实际上指的就是Java中的属性(成员变量)
- JVM必须在方法区中保存类型的所有域的相关信息和声明顺序。
- 也就是说在Java类中我们属性声明的顺序也就决定了在字节码中的顺序,从而确定了在方法区中存储的顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
5.4.4 方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的某个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表(abstract和native方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
5.4.5 non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。
- 类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
5.4.6 全局常量(static+final)
与被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
5.5 常量池
5.5.1 什么是常量池
1)Class文件常量池
Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poll Table),包括各种字面量和对类型、域和方法的符号引用。
什么是字面量和符号引用?
- 字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;方法内的常量值由栈分配,所以并不算是字面量。
- 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用
2)运行时常量池
运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。
一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用(真实地址)。
运行时常量池就是用来索引和查找字段、方法名称、描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息。
运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemooryError异常。
3)字符串常量池
- 在 JDK1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的对象。
- 在 JDK1.7(含)之后,是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中。
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的某个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表(abstract和native方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。
- 类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
5.4.6 全局常量(static+final)
与被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
5.5 常量池
5.5.1 什么是常量池
1)Class文件常量池
Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poll Table),包括各种字面量和对类型、域和方法的符号引用。
什么是字面量和符号引用?
- 字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;方法内的常量值由栈分配,所以并不算是字面量。
- 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用
2)运行时常量池
运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。
一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用(真实地址)。
运行时常量池就是用来索引和查找字段、方法名称、描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息。
运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemooryError异常。
3)字符串常量池
- 在 JDK1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的对象。
- 在 JDK1.7(含)之后,是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中。
与被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
5.5.1 什么是常量池
1)Class文件常量池
Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poll Table),包括各种字面量和对类型、域和方法的符号引用。
什么是字面量和符号引用?
- 字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;方法内的常量值由栈分配,所以并不算是字面量。
- 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用
2)运行时常量池
运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。
一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用(真实地址)。
运行时常量池就是用来索引和查找字段、方法名称、描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息。
运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemooryError异常。
3)字符串常量池
- 在 JDK1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的对象。
- 在 JDK1.7(含)之后,是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中。
Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poll Table),包括各种字面量和对类型、域和方法的符号引用。
什么是字面量和符号引用?
- 字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;方法内的常量值由栈分配,所以并不算是字面量。
- 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用
运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。
一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用(真实地址)。
运行时常量池就是用来索引和查找字段、方法名称、描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息。
运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemooryError异常。
- 在 JDK1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的对象。
- 在 JDK1.7(含)之后,是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中。
4)常量池存储的数据类型
几种在常量池内存储的数据类型包括:
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
总结:
常量池,可以看做是一张表,虚拟机指令根据这张表找打要执行的类名、方法名、参数类型、字面量等类型。
5.5.2 常量池有什么用
一个Java源文件中的类、接口,编译后产生一个 class字节码文件。而Java中的字节码需要数据支持,
通常这种数据会很大以至于
不能直接存到字节码里,换另一种方式,可以存到常量池,
这个字节码包含指向常量池的引用。
在动态链接的时候回用到运行时常量池。
public class SimpleClass{
public void sayHello(){
System.out.println("hello!");
}
}
这是一个很简单的类,实现的功能也很简单,但是里面却使用了String、System/PrintStream以及Object等结构。这里代码量比较小,如果代码量多,很可能需要引用的结构更多,我们不能在字节码中去存储使用到的所有类,而是只存储其符号引用,等到字节码真正去执行时,将符号引用转化为真正的引用对象。这就用到了常量池。
可以将常量池形象的比喻为:厨房炒菜时的调味料和食材,而将我们具体类的字节码比喻为我们要做的菜。
那么我们做不同的菜可能需要不同的食材和调味料,而这些食材和调味料都放在厨房,获取就比较方便。
5.5.3 方法区使用举例
public class MethodAreaTest{
public static void main(String[] args){
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
}
程序的执行过程中是相互配和的
- 将字节码文件加载到方法区的运行时常量池,存储和类相关的信息。
- 在栈的局部变量表中记录局部变量
- 程序计数器记录下一行要执行的字节码指令的行号
- 在操作数栈中对数据进行操作和存储临时结果
5.5.4 运行时数据区的详细结果
5.5.5 常量池里有什么
这是一个很简单的类,实现的功能也很简单,但是里面却使用了String、System/PrintStream以及Object等结构。这里代码量比较小,如果代码量多,很可能需要引用的结构更多,我们不能在字节码中去存储使用到的所有类,而是只存储其符号引用,等到字节码真正去执行时,将符号引用转化为真正的引用对象。这就用到了常量池。
可以将常量池形象的比喻为:厨房炒菜时的调味料和食材,而将我们具体类的字节码比喻为我们要做的菜。
那么我们做不同的菜可能需要不同的食材和调味料,而这些食材和调味料都放在厨房,获取就比较方便。
public class MethodAreaTest{
public static void main(String[] args){
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
}
程序的执行过程中是相互配和的
- 将字节码文件加载到方法区的运行时常量池,存储和类相关的信息。
- 在栈的局部变量表中记录局部变量
- 程序计数器记录下一行要执行的字节码指令的行号
- 在操作数栈中对数据进行操作和存储临时结果
5.5.4 运行时数据区的详细结果
5.5.5 常量池里有什么
编译如下代码
生成字节码
0: ldc ---在常量池 #2 位置加载信息 //String a
2: astore_1 将加载好的 String a 存入 Slot_1 局部变量
LocalVariableTable --main方法栈帧运行时的局部变量表
5.6 一些问题
5.6.1 为什么要把永久代改为元空间
在《Java虚拟机规范中》说因为JRoctit虚拟机中没有永久代,现在Orcle公司收购了它,所以之后的虚拟机就用元空间来替换永久代。
- 随着Java8的带来,HotSpot VM中再也看不到永久代。但是并不意味着类的元数据信息也消失。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫元空间(Metaspace)
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
用元空间替换永久代的原因:
① 永久代设置空间大小是很难确定的
- 在某些场景下,比如web工程中,需要动态的加载大量的jar包和类,那么就可能会出现OOM问题。
② 对于永久代进行调优是很苦难的。
- 当永久代满,就需要发生GC,而永久代的GC可能会发生较长时间STW,那么就是损失性能的
5.6.2 String Table为什么要放到堆区
从JDK的演进中发现在JDK7中将String Table放在了堆内存中,之前一直是放在永久代(方法区的),并且在后续的版本中,字符串常量一直放在了堆内存中。
这是为什么?
- 因为永久代的回收效率很低,在full GC的时候才触发(虽然说我们也不太愿意看到经常的看到Full GC)。而Full GC只有在老年代空间不足或者永久代空间不足的时候才会触发。
- 这就导致StringTable回收效率不高。而在开发中创建的大量的字符串就得不到及时的回收,导致永久代内存不足。放到堆里,能及时的回收。
5.7 方法区的垃圾回收
方法区的垃圾回收的目标主要包括:
常量池中废弃的常量和不再使用的类型。
用元空间替换永久代的原因:在《Java虚拟机规范中》说因为JRoctit虚拟机中没有永久代,现在Orcle公司收购了它,所以之后的虚拟机就用元空间来替换永久代。
- 随着Java8的带来,HotSpot VM中再也看不到永久代。但是并不意味着类的元数据信息也消失。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫元空间(Metaspace)
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
① 永久代设置空间大小是很难确定的
- 在某些场景下,比如web工程中,需要动态的加载大量的jar包和类,那么就可能会出现OOM问题。
② 对于永久代进行调优是很苦难的。
- 当永久代满,就需要发生GC,而永久代的GC可能会发生较长时间STW,那么就是损失性能的
5.6.2 String Table为什么要放到堆区
从JDK的演进中发现在JDK7中将String Table放在了堆内存中,之前一直是放在永久代(方法区的),并且在后续的版本中,字符串常量一直放在了堆内存中。
这是为什么?
- 因为永久代的回收效率很低,在full GC的时候才触发(虽然说我们也不太愿意看到经常的看到Full GC)。而Full GC只有在老年代空间不足或者永久代空间不足的时候才会触发。
- 这就导致StringTable回收效率不高。而在开发中创建的大量的字符串就得不到及时的回收,导致永久代内存不足。放到堆里,能及时的回收。
5.7 方法区的垃圾回收
方法区的垃圾回收的目标主要包括:
常量池中废弃的常量和不再使用的类型。
从JDK的演进中发现在JDK7中将String Table放在了堆内存中,之前一直是放在永久代(方法区的),并且在后续的版本中,字符串常量一直放在了堆内存中。
这是为什么?
- 因为永久代的回收效率很低,在full GC的时候才触发(虽然说我们也不太愿意看到经常的看到Full GC)。而Full GC只有在老年代空间不足或者永久代空间不足的时候才会触发。
- 这就导致StringTable回收效率不高。而在开发中创建的大量的字符串就得不到及时的回收,导致永久代内存不足。放到堆里,能及时的回收。
方法区的垃圾回收的目标主要包括: 常量池中废弃的常量和不再使用的类型。



