运行时内存篇
程序计数器
也是线程私有的,不共享,因为cpu时间片轮换的缘故,所以需要记录上次未执行完的线程执行到那条字节码指令了,所以每个线程需要记录当前执行的命令的内存指针,以方便线程再次得到执行的时候按照正确的顺序执行
JVM之栈(虚拟机栈)
基础知识
会gc吗
不会进行gc,因为线程执行结束后整个栈就被回收掉了,不会进入到gc的阶段
可能抛出来的异常
jvm虚拟机规范允许Java的栈大小是动态的或者固定不变的
- 如果是采用固定大小的jvm,如果线程要求分配的栈空间超过了设置的最大容量,会抛出StackOverflowError异常
- 如果是动态扩展的,在jvm发现已经没有了可用的内存了,会抛出去OutOfMemoryError异常
如何设置大小
-Xss size (即:-XX:ThreadStackSize)设置栈的大小(一般默认为512k-1024k,取决于操作系统),现在基本都是默认为1024k
栈帧
方法与栈帧之间的关系
- 在这个线程上正在执行的每个方法都有各自对应一个栈帧(stack frame)
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧,只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与当前栈帧对应的方法就是当前方法,定义这个方法的类就是当前类
栈的FILO原则
JVM直接对Java栈的操作只有两个(遵循着先进后出的原则)
- 每个方法执行,伴随着进栈
- 执行结束后的出栈工作
栈帧总体的结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack 也称为表达式栈)
- 动态链接(Dynamic Linking 或指向运行时常量池的方法引用)
- 方法返回地址(Return Address 或方法正常退出或者异常退出的定义)
- 一些附加信息
局部变量表
- 局部变量表也被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各种基本数据类型(8种),对象引用,以及returnAddress类型
- 局部变量表所需要的容量大小是在编译期间确定下来的,并保存在方法的Code属性的maximum local variables数据项中,在方法的运行期间是不会改变局部变量表的大小的
- 方法嵌套调用的次数由栈的大小决定.一般来说,栈越大,方法嵌套调用次数越多.对一个函数而言,局部变量表越膨胀,他的栈帧就越大,会占用更多的栈空间,导致其嵌套调用次数就会减少
- 局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随着销毁.
非静态的方法会默认有一个this指针指代当前对象 他会占用局部变量表数组的第一个位置
double和long类型会占用局部变量表的两个slot
slot也是会被复用的
局部变量是不会被对象初始化赋值的
与GC Roots的关系
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或者简介引用的对象都不会被回收
操作数栈
- 我们说Java虚拟机栈的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
- 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的(LIFO)的操作数栈,也可以称之为表达式栈
- 操作数栈是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值
- 栈中的任何一个元素都可以是任意的Java数据类型
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
- 操作数栈在方法的执行过程中,根据字节码指令,并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作,往栈中写入数据或者提取数据来完成一次数据访问
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结构压入栈,比如执行复制,交换和求和等操作
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并用return指令返回出去(所以说return是从操作数栈中返回出去的)
栈顶缓存技术
基于栈式架构的虚拟机所使用的的零地址指令更加的紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也意味着需要更多的指令分派次数和内存的读写此时.
由于操作数是存储在内存中的,因此频繁的执行内存读写操作必然会影响cpu的执行速度,为了解决这个问题,Hotspot虚拟机的设计者提出了栈顶缓存(Tos)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此来降低对内存的读写次数,提升执行引擎的执行效率
动态链接
指向运行时常量池的方法引用
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用.包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接,比如说:invokedynamic指令
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里.比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用.
方法的调用
- 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译器可知,切运行期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接.
- 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期间将调用方法的符号引用转换为直接饮用,由于这种引用具有动态性,因此称之为动态链接
对应的绑定机制为:早期绑定和晚期绑定.绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次.
方法的调用指令
- invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态,这也是Java语言中最常见的方法分派方式
- invokeinterface指令用于调用接口方法,他会在运行时搜索由特定对象所实现的这个接口方法,并找出合适的方法进行调用
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器),私有方法和父类方法,这些方法都是静态类型绑定的,不会在调用时进行动态派发
- invokestatic指令用于调用命名类中的类方法(static方法),这是静态绑定的
- invokedynamic调用动态绑定的方法,这个是JDK1.7后新加入的指令,用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法.前面四条调用指令分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的(他是为了动态语言设计的,不过在Java8以后的lambda也会生成一些这个字节码出来,是需要有一个引导器用方法句柄去确认类型的)
方法返回地址
- 存放调用该方法的pc寄存器的值
- 一个方法的结束有两种方式
- 正常执行完成退出
- 出现未处理的异常,非正常退出
- 无论通过哪种方式退出,在方法退出后都返回到该方法调用的位置.方法正常退出时,调用者的pc计数器的值作为返回地址,即调用方法的指令下一条指令的地址.而通过异常退出的,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息
本地方法栈
- Java 虚拟机栈用于管理Java 方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法是使用C/c++语言实现的。
- 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
权限很高,可以随意操作jvm中的东西
JVM之堆
核心概述
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域,也是GC的重点区域。
- Java堆区在jvm启动的时候即被创建,其空间大小也就确定了,是jvm管理的最大的一块内存空间
- 堆内存的大小是可以调节的
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
- Java虚拟机规范描述是所有的对象实例以及数组都应该分配在堆上,不过现在也出来了新的栈上分配的概念(引用对象全在堆上也并不那么绝对了,但是栈上分配也不是直接把对象放栈上了 只是一个标量)
堆的内存结构
结构图示
新生代与老年代
- 存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都十分迅速
- 另外一类是对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
- Java堆区进一步细分的话,可以划分为年轻代(youngGen)和老年代(oldGen)
- 其中年轻代又可以划分为Eden空间,Survivor0空间和Survivor1空间(有时也叫作form区和to区)
- 几乎所有的Java对象都是在Eden区被new出来的(大对象的可能会直接进入老年代)
- 绝大部分的Java对象的销毁都在新生代进行了
TLAB概念
- 从内存模型而不是垃圾回收的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,他包含在Eden空间内
- 默认就是开启的(这样在每个线程新建引用对象需要在堆上分配空间时不需要加锁,提升了效率)
- 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间
- 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
TLAB的补充说明
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选
- 在程序中,开发人员可以通过选项 -XX +/-UseTLAB 设置是否开启TLAB空间
- 默认情况下TLAB的空间非常小,仅占用整个Eden区空间的1%,当然我们可以通过选项 -XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小.
- 一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
分代思想
因为在Java的世界中,大部分的对象的都是临时对象,随着方法的执行完毕就应该被垃圾回收掉,如果不分代的话会导致整个堆上的频繁垃圾回收,会很影响程序的执行效率,所以通过分代把这些需要频繁回收掉的和稳定的长期使用对象分割开,是非常聪明的做法.
堆对象分配
总结的金句
- 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
- 关于垃圾回收
- 频繁在新生区收集
- 很少在老年代收集
- 几乎不在永久代(元空间)收集
分配策略
- 优先分配到Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 空间分配担保
- 动态对象年龄判断
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入到老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
-XX:HandlePromotionFailure
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间:
- 如果大于,则此次Minor GC是安全的
- 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
- 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
在JDK 6 Update 24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC.
分配过程示意图
堆的参数设置
重要设置指令
- -Xms:初始内存 (默认为物理内存的1/64)
- -Xmx:最大内存(默认为物理内存的1/4)
- -XX:NewRatio:配置新生代与老年代在堆结构的占比。赋的值即为老年代的占比,剩下的1给新生代,默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3,-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
- -Xmn:设置新生代的大小。(初始值及最大值)通常用默认即可。
- -XX:SurvivorRatio:设置Eden区与form和to区的大小比例
- 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1
- 开发人员可以通过选项“-XX:SurvivorRatio”调整这个空间比例。比如-XX:SurvivorRatio=8
- windows下显示设置才会是8:1:1,否则居然是6:1:1
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄。超过此值,仍未被回收的话,则进入老年代,默认值为15
- -XX:+PrintGCDetails:输出详细的GC处理日志
注意事项
- 一旦堆区中的内存大小超过"-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常
- 通常会将-Xms -Xmx两个参数设置为相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
- 堆默认最大值的计算方式:如果物理内存小于192m,那么堆最大值为物理内存的一半,如果物理内存大于等于1g,那么堆最大值为物理内存的1/4
- 堆默认最小值计算方式:最少不得少于8m,如果物理内存大于等于1g,那么默认值为物理内存的1/64.最小堆内存在jvm启动的时候就会被初始化
栈与堆总结
栈和堆的区别
- 角度一 栈不会进行垃圾回收.而堆是垃圾回收的重点区域,并且栈抛出的内存异常与堆不同
- 角度二: 栈的执行效率非常高,而堆是存储空间非常大
- 角度三: 栈是属于栈结构,里面由一组组栈帧构成.正好符合方法的执行顺序 堆物理上是可以不连续的,属于一大块的内存空间
- 角度四 :栈管运行 堆管存储,基本类型是能编译期确定好大小的,是可预期的,并且为了执行效率更高,所以直接存储在栈上
方法区
- jdk1.6及之前:有永久代(permanent generation)
- jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
- jdk1.8及之后: 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池仍在堆
- -XX:MetaspaceSize
- -XX:MaxMetaspaceSize:默认值是-1,即不限制
StringTable
- 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
- 字符串常量池中是不会存储相同内容的字符串的
变化
- Java 6及以前,字符串常量池存放在永久代。
- Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。
- Java 8 中,字符串常量仍然在堆。