做可视化图表的网站,阿里云网站,完整的品牌推广方案,电子商务网站建设利益分析文章目录 概要一、jdk7与jdk8内存结构的差异二、程序计数器三、虚拟机栈3.1 什么是虚拟机栈3.2 什么是栈帧3.3 栈帧的组成 四、本地方法栈五、堆5.1 堆的特点5.2 堆的结构5.3 堆的参数配置 六、方法区6.1 方法区结构6.2 运行时常量池 七、元空间 概要
根据 JVM 规范#xff0… 文章目录 概要一、jdk7与jdk8内存结构的差异二、程序计数器三、虚拟机栈3.1 什么是虚拟机栈3.2 什么是栈帧3.3 栈帧的组成 四、本地方法栈五、堆5.1 堆的特点5.2 堆的结构5.3 堆的参数配置 六、方法区6.1 方法区结构6.2 运行时常量池 七、元空间 概要
根据 JVM 规范JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。 其中各个部分的概述如下:
名称特征作用配置参数异常程序计数器线程私有生命周期与线程相同字节码行号指示器无无虚拟机栈线程私有生命周期与线程相同使用连续的内存空间存储信息如上图-XssStackOverflowError/OutOfMemoryError堆线程共享生命周期与虚拟机相同可以不使用连续的内存地址保存对象实例所有对象实例包括数组都要在堆上分配-Xms -Xsx -XmnOutOfMemoryError方法区线程共享生命周期与虚拟机相同可以不使用连续的内存地址存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据-XX:PermSize:16M-XX:MaxPermSize64M-XX:MetaspaceSize16M-XX:MaxMetaspaceSize64MOutOfMemoryError本地方法栈线程私有为虚拟机使用到的Native 方法服务无StackOverflowError/OutOfMemoryError
JVM分为五大模块 类装载器子系统 、 运行时数据区 、 执行引擎 、 本地方法接口 和 垃圾收集模块 。 一、jdk7与jdk8内存结构的差异
Java7和Java8内存结构的不同主要体现在方法区的实现。 方法区是java虚拟机规范中定义的一种概念上的区域不同的厂商可以对虚拟机进行不同的实现。 我们通常使用的Java SE都是由Sun JDK和OpenJDK所提供这也是应用最广泛的版本。而该版本使用的VM就是HotSpot VM。通常情况下我们所讲的java虚拟机指的就是HotSpot的版本。
JDK7的内存结构 永久代是 hotspot 在1.7及之前才有的设计1.8以及其他虚拟机并不存。可以说永久代是1.7的 hotspot 偷懒的结果他在堆里划分了一块来实现方法区的功能叫永久代。 因为这样可以借助堆的垃圾回收来管理方法区的内存而不用单独为方法区再去编写内存管理程序。 同时代的其他虚拟机如 J9 Jrockit 等没有这个概念。后来 hotspot认识到永久代来做这件事不是一个好主意。1.7已经从永久代拿走了一部分数据静态变量和运行时常量池转移到了堆中)直到1.8彻底去掉了永久代方法区大部分迁移到了 metaspace 注意不是全部不是全部
JDK8的内存结构 从jdk1.8开始已经将方法区中实现的永久代去掉了并用元空间 class metadata space 代替了之前的永久代元空间的存储位置是本地内存/直接内存并且将方法区大部分迁移到了元空间。
方法区Java8之后的变化小结
移除了永久代PermGen替换为元空间Metaspace永久代中的class metadata类元信息转移到了native memory本地内存而不是虚拟机永久代中的interned Strings字符串常量池 和 class static variables类静态变量转移到了Java heap永久代参数PermSize MaxPermSize- 元空间参数MetaspaceSize MaxMetaspaceSize
Java8 为什么使用元空间替代永久代这样做有什么好处呢
官方给出的解释是
移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力因为 JRockit 没有永久代所以不需要配置永久代。永久代内存经常不够用或发生内存溢出抛出异常java.lang.OutOfMemoryError: PermGen 。这是因为在 JDK1.7 版本中指定的 PermGen 区大小为8M由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集但回收率都偏低成绩很难令人满意为 PermGen 分配多大的空间很难确定PermSize 的大小依赖于很多因素比如JVM 加载的 class 总数、常量池的大小和方法的大小等而jdk1.8以后的元空间大小就只受本机总内存的限制如果不设置参数的话因为它使用的是本地内存。
二、程序计数器
程序计数器Program Counter Register:也叫PC寄存器是一块较小的内存空间它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
PC寄存器的特点
1区别于计算机硬件的pc寄存器两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址而相对于虚拟机pc寄存器它表现为一块内存虚拟机的pc寄存器的功能也是存放伪指令更确切的说存放的是将要执行指令的地址。 2当虚拟机正在执行的方法是一个本地native方法的时候jvm的pc寄存器存储的值是undefined。 3程序计数器是线程私有的它的生命周期与线程相同每个线程都有一个。 4此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的在任何一个确定的时刻一个处 理器只会执行一条线程中的指令。 因此为了线程切换后能恢复到正确的执行位置每条线程都需要有一个独立的程序计数器各条线程之间的计数 器互不影响独立存储我们称这类内存区域为“线程私有”的内存。
三、虚拟机栈
3.1 什么是虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的即生命周期和线程相同。Java虚拟机栈和线程同时创建用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 public class StackDemo {public static void main(String[] args) {StackDemo sd new StackDemo();sd.A();}public void A() {int a 10;System.out.println( method A start);System.out.println(a);B();System.out.println(method A end);}public void B() {int b 20;System.out.println( method B start);C();System.out.println(method B end);}private void C() {int c 30;System.out.println( method C start);System.out.println(method C end);}
}3.2 什么是栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
3.3 栈帧的组成
栈帧大体都包含四个区域局部变量表、操作数栈、动态连接、 返回地址
局部变量表 部变量表(Local Variable Table)是一组变量值存储空间用于存放方法参数和方法内定义的局部变量。 包括8种基本数据类型、对象引用reference类型和returnAddress类型指向一条字节码指令的地址。其中64位长度的long和double类型的数据会占用2个局部变量空间Slot其余的数据类型只占用1个。
操作数栈
操作数栈(Operand Stack)也称作操作栈是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者也就是出栈/入栈操作。
操作数栈作用小结
主要用于保存计算过程的中间结果同时作为计算过程中变量临时的存储空间操作数栈就是JVM执行引擎的一个工作区 Java虚拟机的解释执行引擎被称为基于栈的执行引擎其中所指的栈就是指操作数栈为了实现java的跨平台选择了面向操作数栈的指令集架构而没有选择直接基于CPU寄存器的指令集架构由执行引擎面向更底层基于栈的指令集主要的优点就是可移植寄存器由硬件直接提供程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束但是栈架构指令集的主要缺点是执行速度相对来说会稍慢一些因为栈实现在内存之中频繁的栈访问也就意味着频繁的内存访问相对于CPU来说内存始终是执行速度的瓶颈
动态链接 Java虚拟机栈中每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。 动态链接的作用是将符号引用转换成直接引用
返回地址 方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束有两种方式正常地执行完成出现未处理的异常非正常的退出。无论通过哪种方式退出在方法退出后都返回到该方法被调用的位置。方法正常退出时调用者的PC计数器的值作为返回地址即调用该方法的指令的下一条指令的地址。而通过异常退出的返回地址是要通过异常表来确定栈帧中一般不会保存这部分信息。 无论方法是否正常完成都需要返回到方法被调用的位置程序才能继续进行。
四、本地方法栈
本地方法栈Native Method Stacks 与虚拟机栈所发挥的作用是非常相似的 其区别只是虚拟机栈为虚拟机执行Java方法也就是字节码 服务 而本地方法栈则是为虚拟机使用到的本地Native 方法服务。 特点
本地方法栈加载native方法是线程私有生命周期跟线程相同每个线程都有一个跟java虚拟虚拟机栈一样规定了两种类型异常 a) StackOverFlowError :线程请求的栈深度大于所允许的深度。 b) OutOfMemoryError本地方法栈扩展时无法申请到足够的内存
五、堆
对于Java应用程序来说 Java堆Java Heap 是虚拟机所管理的内存中最大的一块。 Java堆是被所 有线程共享的一块内存区域 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例 Java 世界里“几乎”所有的对象实例都在这里分配内存。
5.1 堆的特点
Java虚拟机所管理的内存中最大的一块jvm所有线程共享堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)在虚拟机启动时创建几乎所有的对象实例以及数组都在这里分配内存java堆时垃圾收集器管理的主要区域从内存回收的角度来看由于现在收集器基本都采用分代收集算法所以Java堆还可以细分为新生代和老年代新生代又可以分为Eden 空间、From Survivor空间、To Survivor空间。堆是计算机物理存储上不连续的、逻辑上是连续的也是大小可调节的通过-Xms和-Xmx控制。方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除如果在堆中没有内存完成实例的分配并且堆也无法再扩展时将会抛出OutOfMemoryError异常
5.2 堆的结构
现在垃圾回收器都使用分代理论,堆空间也分类如下: 在Java7 Hotspot虚拟机中将Java堆内存分为3个部分
青年代Young Generation老年代Old Generation永久代Permanent Generation 在Java8以后由于方法区的内存不在分配在Java堆上而是存储于本地内存元空间Metaspace中所以永久代就不存在了 5.3 堆的参数配置
参考参数配置
JVM中存储java对象可以被分为两类:
年轻代(Young Gen)年轻代主要存放新创建的对象内存大小相对会比较小垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Spacefrom 和to。年老代(Tenured Gen)年老代主要存放JVM认为生命周期比较长的对象经过几次的Young Gen的垃圾回收后仍然存在内存大小相对会比较大垃圾回收也相对没有那么频繁. -XX:NewRatioratio Sets the ratio between young and old generation sizes. By default, this option is set to 2. The following example shows how to set the young/old ratio to 1: -XX:NewRatio1 -XX:NewRatio2:默认值标识新生代占1老年代占2新生代占整个堆的1/3; 修改占比 -XX:NewPatio4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5 XX:SurvivorRatioratio Sets the ratio between eden space size and survivor space size. By default, this option is set to 8. The following example shows how to set the eden/survivor space ratio to 4: -XX:SurvivorRatio4 Eden空间和另外两个Survivor空间占比分别为8:1:1 可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio8 堆的总大小可由-Xms 跟-Xmx来配置
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务所以无论什么时候总是有一块 Survivor 区域是空闲着的。因此新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
六、方法区
方法区Method Area 与Java堆一样 是各个线程共享的内存区域 它用于存储已被虚拟机加载的类型信息、常量、 静态变量、 即时编译器编译后的代码缓存等数据。 官方文档方法区
方法区中存储的信息大致可分以下两类
类信息主要指类相关的版本、字段、方法、接口描述、引用等运行时常量池编译阶段生成的常量与符号引用、运行时加入的动态变量
方法区在虚拟机规范里这是一个逻辑概念元空间、永久代是方法区具体的落地实现。 在jdk1.6里用永久代来实现方法区物理空间上用的时堆的内存目的是利用堆的垃圾回收来管理方 法区的内存。字符串常量是运行时常量池的一部分也就是归属于方法区放在了永久代里。这个时候经常会出现的一个错误就是java.lang.OutOfMemoryError: PermGen space。 jdk1.7已经从永久代拿了一部分数据静态变量和运行时常量池转移到了堆中
6.1 方法区结构 类加载器将Class文件加载到内存之后将类的信息存储到方法区中
方法区中存储的内容
类型信息域信息、方法信息运行时常量池
类型信息 对每个加载的类型类Class、接口 interface、枚举enum、注解 annotationJVM必须在方法区中存储以下类型信息
这个类型的完整有效名称全名 包名.类名这个类型直接父类的完整有效名对于 interface或是java.lang.Object都没有父类这个类型的修饰符 public,abstract,final的某个子集这个类型直接接口的一个有序列表
域信息 域信息即为类的属性成员变量 JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。 域的相关信息包括域名称、域类型、域修饰符pυblic、private、protected、static、final、volatile、transient的某个子集
方法信息 JVM必须保存所有方法的以下信息同域信息一样包括声明顺序
方法名称方法的返回类型或void方法参数的数量和类型按顺序方法的修饰符public、private、protected、static、final、synchronized、native、abstract的一个子集方法的字节码bytecodes、操作数栈、局部变量表及大小 abstract和native方法除外异常表 abstract和 native方法除外。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏 移地址、被捕获的异常类的常量池索引
6.2 运行时常量池
在jvm规范中方法区除了存储类信息之外还包含了运行时常量池。这里 首先要来讲一下常量池的分类
Class常量池静态常量池运行时常量池字符串常量池没有明确的官方定义其目的是为了更好地使用String
常量池经常会被搞混要准确地理解首先来看基本定义
静态常量池存放编译期间生成的各种字面量与符号引用 运行时常量池常量池表在运行时的表现形式
编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池(静态常量池)中的信息加载到内存中存储在了方法区的运行时常量池中。
什么叫字面量与符号引用呢
静态常量池
.class 文件中除了有类的版本、字段、方法和接口等描述信息外还有一项信息是常量池 ( Constant Pool Table )用于存放编译期间生成的各种字面量和符号引用之所以说它是静态的常量池是因为这些都只是躺在 .class 文件中的静态数据此时还没被加载到内存.
/*** 1. 使用jdk1.8编译* 2. 使用 javap -v ClassConstantPool.class*/
public class ClassConstantPool {private static String a abc;private String f def;private static int b 123;private final int c 456;private int d 789;private float e;Gucci gucci new Gucci();class Gucci {}
}反编译后截取部分信息如下 字面量给基本类型变量的赋值就叫做字面量或字面值字面量是编译后生成的产物。 比如String a b这里的“b”就是字符串字面量同样类推还有整数字面量浮点类型字面量字符字面量 符号引用符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量JAVA 在编译的时候一个每个 java 类都会被编译成一个 class 文件但在编译的时候虚拟机并不知道所引用类的地址(实际地址)就用符号引用来代替而在类的解析阶段类加载的一个过程就是为了把这个符号引用转化成为真正的地址。 比如 ClassConstantPool 类被编译成一个 class 文件时发现引用了 Gucci 类但是在编译时并不知道 Gucci 类的实际内存地址因此只能使用符号引用 com/ocean/constance/ClassConstantPool$Gucci 来代替。而在类装载器装 载 Guuci 类时此时可以通过虚拟机获取 Guuci类 的实际内存地址因此便可以将符号 com/ocean/constance/ClassConstantPool$Gucci 替换为 Guuci 类的实际内存地址。 运行时常量池
运行时常量池 Runtime Constant Pool 是每一个类或接口的常量池 Constant_Pool 的运行时表示形式它包括了若干种不同的常量从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。
运行时常量池是在类加载完成之后将 静态常量池中的符号引用值转存到运行时常量池中类在解析之后将符号引用替换成直接引用。 另外运行时常量池的物理存储位置要注意两点
运行时常量池在 JDK1.7 版本之后就移到堆内存中了这里指的是物理空间而逻辑上还是属于方法区方法区是逻辑分区。在 JDK1.8 中使用元空间代替永久代来实现方法区但是方法区从定义上并没有改变所谓 “Your father will always be your father” 。变动的只是方法区中内容的物理存放位置运行时常量池和字符串常量池被移动到了堆中而并没有在元空间。但是不论它们物理上如何存放逻辑上还是属于方法区的。
字符串常量池
字符串常量池这个概念是有争议的很多正式的虚拟机规范文档都没有对这个概念作一个明确的官方定义。
以 JDK1.8 为例字符串常量池是存放在堆中并且与 java.lang.String类有很大关系。设计这块内存区域的原因在于 String 对象作为 Java 语言中重要的数据类型是内存中占据空间最大的一个对象。高效地使用字符串可以提升系统的整体性能。
七、元空间
在JDK1.7之前HotSpot 虚拟机用永久代来实现方法区。而从 JDK 1.8 开始移除永久代用元空间来实现方法区它位于本地内存中而不是虚拟机内存中。
永久代跟元空间对比如下
存储位置不同永久代在物理上是堆的一部分和新生代、老年代的地址是连续的而元空间属于本地内存。存储内容不同在原来的永久代划分中永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信 息存储在元空间中静态变量和常量池等并入堆中相当于原来的永久代中的数据被元空间和堆内存给瓜分了。
Metaspace相关参数
XX:MetaspaceSize·初始空间大小达到该值就会触发垃圾收集进行类型卸载同时GC会对该值进行调整如 果释放了大量的空间就适当降低该值如果释放了很少的空间那么在不超过MaxMetaspaceSize时适当提高该值。-XX:MaxMetaspaceSize最大空间默认是没有限制的。如果没有使用该参数来设置类的元数据的大小其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。但是如果没有设置最大值则可能存在bug导致Metaspace的空间在不停的扩展会导致机器的内存不足进而可能出现swap内存被耗尽最终导致进程直接被系统直接kill掉。如果设置了该参数当Metaspace剩余空间不足会抛出java.lang.OutOfMemoryError: Metaspace space-XX:MinMetaspaceFreeRatio在GC之后最小的Metaspace剩余空间容量的百分比减少为分配空间所导致的垃圾收集 -XX:MaxMetaspaceFreeRatio在GC之后最大的Metaspace剩余空间容量的百分比减少为释放空间所导致的垃圾收集