运行时数据区
Java虚拟机在执行Java程序时,会把它管理的内存划分为若干不同的数据区域。这区域各有各的用途以及生命周期
# 1.1 方法区
方法区,也称非堆(Non-Heap),是一个被线程共享的内存区域。其中主要存储类的类型信息,方法信息,域信息,JIT代码缓存,运行时常量池等。
(1)方法区是各个线程共享的内存区域,在虚拟机启动时创建
(2)虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来
(3)用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据。
(4)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
在JDK7之前,习惯把方法区称为永久代,而在JDK8之后,又取消了永久代,改用元空间代替。元空间的本质与方法区类似,都是对JVM规范中方法区这一内存区域的一种实现。不
过元空间与永久代的最大区别就是:元空间不在虚拟机设置的内存中,而是直接使用的本地内存。所以元空间的大小并不受虚拟机本身的内存限制,而是受制于计算机的直接内存。
| 版本 | 区别 |
|---|---|
| JDK6及以前 | 有永久代,字符串常量池(String Table),静态变量都存放在永久代上 |
| JDK7 | 有永久代,但字符串常量池,静态变量被从永久代中移出,放到了堆中 |
| JDK8及以后 | 无永久代,改用元空间代替,但字符串常量池,静态变量依然放在堆中 |
注:永久代大小启动的时候指定,启动后不能更改;元空间大小如果不指定,最大就是物理内存。
# 1、类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
这个类型的完整有效名称(全名=包名.类名)
这个类型直接父类的完整有效名称(interface、Object 都没有父类)
这个类型的修饰符(public,abstract,final的某个子类)
这个类型直接接口的一个有序列表
# 2、域信息
JVM在方法区必须保存类型的所有域信息以及域的声明顺序
域的类型包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
# 3、方法信息
方法名称
方法返回类型(或void)
方法参数的数量和类型(按顺序)
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
方法的字节码、操作数栈、局部变量表及大小
方法的字节码(bytecodes,就是方法的实体编译的字节码)、操作数栈、局部变量表及大小(abstract和native方法除外,因为没有方法体)
异常表(abstract和native方法除外,因为没有方法体)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常的常量池索引
# 4、运行时常量池
一个有效的字节码文件除了包含各个类的版本信息、字段、方法以及接口等描述信息外,还包含了一项信息就是常量池表(Constant Pool Table),里面包含有各种字面量和对类型、域和方法的符号引用。
字面量和符号引用:举个简单例子,String str = new String(“你好”);其中"你好"就是字面量,而str则为符号引用。str符号引用需要在类加载的解析阶段转换为直接引用,因为 str的值是可以变化的,我们不能在第一时间确定其真正的值,需要在动态运行中进行解析。符号引用包含以下三类:
类和接口的全限定名称
字段的名称和描述符
方法的名称和描述符
一个字节码文件被加载到虚拟机后,字节码文件中的一些数据,如之前提到过的类型信息,域信息,方法信息等,就会被放置到方法区中,而字节码文件中的常量则会进入方法区中的运行时常量池(注:字符串常量/字面量会放到堆中),JVM为每个已加载的类或者接口都维护一个常量池,即运行时常量池是每个类都有一个。常量池中数据像数组一样,是通过索引访问的。
Java并不要求常量必须在编译时才能产生,即并不是class文件常量池中的内容才能进入运行时常量池,在运行期间也同样可以将新的常量放入池中。比如,可以使用
String.intern()动态生成字符串常量,String.intern()方法的作用就是当池中没有相应的字符串常量时,在运行时动态生成。
# 1.2 堆内存(Java堆)
Java堆是Java虚拟机所管理的内存最大的一块区域,Java堆是线程共享的,在虚拟机启动时创建。
几乎所有的对象实例都在这里分配内存。
字符串常量池(String Table),静态变量也在这里分配内存。
Java堆是垃圾收集器管理的内存区域,有些资料称为GC堆,当对象不再使用了,被当做垃圾回收掉后,这些为对象分配的内存又重新回到堆内存中。
Java堆在逻辑上应该认为是连续的,但是在具体的物理实现上,可以是不连续的。
Java堆可以是固定大小的,也可以是可扩展的。现在主流Java虚拟机都是可扩展的。
-Xmx 最大堆内存
-Xms 最小堆内存
如果Java堆没有足够的内存给分配实例,并且也无法继续扩展,则抛出 OutOfMemoryError 异常。
# 1、堆内存结构
堆内存从结构上来说分为年轻代(YoungGen)和老年代(OldGen)两部分;
年轻代(YoungGen)又可以分为生成区(Eden)和幸存者区(Survivor)两部分;
幸存者区(Survivor)又可细分为 S0区(from space)和 S1区 (to space)两部分;
Eden 区占大容量,Survivor 两个区占小容量,默认比例是 8:1:1;
静态变量和字符串常量池在年轻代与老年代之外单独分配空间。
# 2、分代的意义
年轻代和老年代的划分是为了更好的内存分派及回收,提高效率。
堆是垃圾回收机制的重点区域。我们知道垃圾回收机制有三种,minor gc,major gc 和full gc,针对于堆的就是前两种。年轻代的叫 minor gc,老年代的叫major gc。
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
分代垃圾回收采用分治的思想,进行代际的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
# 3、对象在堆中的初始分配位置
(1)对象优先在Eden 区分配
(2)大对象直接进入老年代
- 上面提到,新创建的对象是优先存放入 Eden 区的,但是新创建的大对象会直接进入老年代。
- 什么是大对象: 10M 的对象算大吗?100M 的对象呢?什么是大对象?大对象的标准是可以由开发者定义的,我们的 JVM 参数中,能够通过 -XX:PretenureSizeThreshold 这个参数设置大对象的标准,可惜的是这个参数只对 Serial 和 ParNew 两款新生代收集器有效。
- 对于不能够设置
-XX:PretenureSizeThreshold参数的JVM来说, Eden 区容量不够存放的对象就是所谓的大对象。
# 4、对象在堆中的转移
新生成的非大对象首先放到年轻代 Eden 区,当 Eden 空间满了,触发 Minor GC,存活下来的对象移动到Survivor0 区,Survivor0 区满后触发执行Minor GC,Survivor0 区存活对象移动到 Suvivor1 区,这样保证了一段时间内总有一个Survivor 区为空。经过多次Minor GC 仍然存活的对象移动到老年代。
如果新生成的是大对象,会直接将该对象存放入老年代。
老年代存储长期存活的对象,GC 期间会停止所有线程等待 GC 完成,所以对响应要求高的应用尽量减少发生 Major GC,避免响应超时。
作用:JVM 通过判断对象的具体年龄来判别是否该对象应存入老年代,JVM通过对年龄的判断来完成对象从年轻代到老年代的转移。
# 5、对象年龄判断
对象年龄(Age)计数器:HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。
年龄增加:对象通常在 Eden 区里诞生,如果经过第一次 Minor GC 后仍然存活,并且能被Survivor容纳的话,该对象会被移动到 Survivor 空间中,并且将其对象年龄设为 1 岁。对象在Survivor区中每熬过一次 Minor GC,年龄就增加 1 岁。
年龄默认阈值:当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。
# 6、Full GC触发机制
触发Full GC执行的情况有如下五种:
- 调用 System.gc() 时,系统建议执行 Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
# 1.3 Java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的,因此也是线程安全的。
Java虚拟机栈是线程私有的,其生命周期和线程相同。
虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被执行,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、参与方法的调用与返回等。
每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中出入栈到出栈的过程
JVM 允许指定 Java 栈的初始大小以及最大、最小容量。
# 1、栈与堆
- 栈是运行时的单位,而堆是存储的单位
- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据
- 堆解决的是数据存储的问题,即数据怎么放,放哪里
# 2、栈帧
- 定义:栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的 Java 虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
- 栈帧初始化大小:在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。 因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
- 栈帧结构:在一个线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
(1)局部变量表
- 在栈帧中,局部变量表占用了大部分的空间,那么接下来我们看下局部变量表的基本概念与特点。
- 基本概念:每个栈帧中都包含一组称为局部变量表的变量列表,用于存放方法参数和方法内部定义的局部变量。
- 特点:
- a.局部变量表的容量以变量槽(Variable Slot)为最小单位;
- b.局部变量表中的 Slot 是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码程序计数器的值已经超过了某个变量的作用域,那么这个变量相应的 Slot 就可以交给其他变量去使用,节省栈空间。
(2)操作数栈
- 方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程;
- 操作数栈的每一个元素可以是任意的 Java 数据类型,32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2;
- 简单的理解,操作数栈存放的是当前正在操作的变量,可以是局部变量或者对象实例字段中的常量和变量。比如执行到代码a=b+c时就会把a,b,c都存入到操作数栈来。
(3)动态链接
- 动态链接保存的是一个引用或者说指针,它指向该栈帧所属方法在运行时常量池(方法区)中的地址,它支持着Java的多态特性。
- 在 class 文件的常量池(存储字面量和符号引用)中存有大量的符号引用(1. 类的全限定名,2. 字段名和属性,3. 方法名和属性),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
- 这些符号引用一部分会在类加载过程的解析阶段转化为直接引用(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),称为静态解析。另外一部分将在运行期期间转化为直接引用,称为动态链接(父类方法链接到子类,会引用子类重写的方法)。
(4)返回地址
- 方法返回地址是方法在PC寄存器中的值,也即是该方法的指令地址,方便执行引擎在执行完该方法后,回到该方法对应的指令行号,这样才能继续执行下去。
- 返回地址代表的是方法执行结束,当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(例如:return),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。
- 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是 Java 虚拟机内部产生的异常,还是代码中使用 throw 字节码指令产生的异常,只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
- 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等。
# 1.4 程序计数器
程序计数器的英文全称是Program Counter Register,又叫程序计数寄存器。Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。JVM中的PC寄存器是对 物理PC寄存器的一种抽象模拟。
程序计数器其实就是一个指针,它指向了我们程序中下一句需要执行的指令,可以看做当前线程执行的字节码的行数指示器。
不管是分支、循环、跳转等代码逻辑,字节码解释器在工作时就是改变程序计数器的值来决定下一条要执行的字节码。
每个线程都有一个独立的程序计数器,在任意一个确定的时刻,一个CPU内核都只会执行一条线程中的指令,CPU切换线程后是通过程序计数器来确定该执行哪条指令。
程序计数器占用内存空间小到基本可以忽略不计,是唯一一个在虚拟机中没有规定任何OutOfMemoryError 情况的区域。
如果正在执行的是Native方法,则这个计数器为空。
# 1.5 本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的。 只不过虚拟机栈为虚拟机执行的Java方法(即字节码)服务,本地方法栈为虚拟机执行的本地方法(Native方法、C/C++ 实现)服务。
与虚拟机栈一样,当栈深度溢出时,抛出 StackOverFlowError 异常。 当栈扩展内存不足时,抛出 OutOfMemoryError 异常。