结构
- PC程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
PC寄存器
作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
虚拟机栈
java虚拟机是什么
- 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应这一次次的java方法调用
- 是线程私有的
生命周期
生命周期和线程一致
作用
主管java程序的运行,它保存方法的局部变量(8种基本数据类型、对象引用的地址)、部分结果,并参与方法的调用和返回
特点
-
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
-
jvm直接对java栈的操作只有两个
-
每个方法执行,伴随着进栈(入栈、压栈)
-
执行结束后的出栈工作
-
-
对于栈来说不存在垃圾回收的问题
设置栈内存大小
使用 -XSS 选项来设置线程的最大栈空间
- -Xss1m
- -Xss1024k
- -Xss1045576
虚拟机栈
栈帧
栈帧的内部结构
- 局部变量表
- 局部变量表也被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用,以及returnAddress类型
- 由于局部变量表是建立在线程的栈上,是线程的所有数据,因此不存在数据安全的问题
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。
- 局部变量表中的变量只在当前方法调用中有效。当方法结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量
- 最基本的存储单元是Slot槽
- 32位以内的类型只占用一个slot
- jvm会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可访问到局部变量表中指定的局部变量值
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量会按照顺序复制到局部变量表中的每一个slot上
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序排列
- slot的槽可以重复利用,如果slot槽出了其作用域,那就可以回收了
- 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
- 操作数栈
- 后进先出
- 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 一个方法刚执行的时候,一个新的栈帧会随之被创建出来,这个方法的操作数栈是空的
- 每个操作数栈会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值
- 操作数栈并非采用访问索引的方式来进行数据访问的
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中
- jvm的解释引擎是基础栈的执行引擎
- 动态链接(指向运行时常量池的方法引用)
- 为了将这些符号引用转换为调用方法的直接引用
- 方法返回地址(方法正常退出或异常退出的定义)
- 存放调用该方法的pc寄存器的值
- 当方法退出后都会返回到该方法被调用的位置,方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息
- 一些附加信息
方法的调用
- 非虚方法
- 静态方法、私有方法、final方法、实例构造器、父类方法
- 虚方法
- 能够体现多态的行为
- 类的继承性
- 方法的重写
- 能够体现多态的行为
- 普通调用指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用<init>方法、私有父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用虚方法
- invokeinterface: 调用接口方法
- 动态调用指令
- invokedynamic:动态解析出需要调用的方法,然后执行
栈运行原理
- 不同线程中所包含的栈帧是不允许存在互相引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
- java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令,另外一种是抛出异常。不管使用那种方式,都会导致栈帧被弹出
本地方法栈
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不受虚拟机限制的世界,和虚拟机有相同的权限
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 它甚至可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存
堆
内存结构
- 新生代 (默认和老年代的比例是1:2 -NewRatio 来修改 默认是2)
- 生命周期较短的瞬间对象,这类对象的创建和消亡都非常迅速
- Eden
- s0
- s1
- 生命周期较短的瞬间对象,这类对象的创建和消亡都非常迅速
- 老年代
- 生命周期非常长,在某些极端的情况下还能够与jvm生命周期保持一致
- 永久代(元空间)
堆空间大小设置
- -Xms、-Xmx (-X 是jvm运行参数 ms:memory start mx:memory max)
- -Xms 堆区的起始内存,等价于 -XX:InitialHeapSize
- -Xmx 堆区的最大内存,等价于 -XX:MaxHeapSize
- 通常会把-Xms和-Xmx 的大小设置成一样,目的是为了java垃圾回收机制清理完堆区不需要重新分割计算堆区的大小,从而提高性能
。默认情况下,- 初始内存大小:物理电脑内存 / 64
- 最大内存大小:物理电脑内存 / 4
- 查看设置的参数
- jps / jstat -gc 进程id
- -XX:+PrintGCDetails
对象分配过程
- new的对象先放在eden区,此区有大小限制
- 当eden的空间满时,程序又需要创建对象,jvm垃圾回收器将对eden区进行垃圾回收,将eden区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到eden
- 然后将eden中的剩余对象移动到s0
- 如果再次触发垃圾回收,此时上次幸存下来的s0区的对象,如果没有被回收,就会放到s1
- 如果再次经历垃圾回收,此时会重新放到s0,接着再去s1
- 每次进入s0/s1 此对象的计数都会加1 如果年龄到了15 ,下一次还存活,就会被放到老年代
- 如果new对象eden放不下也会直接到老年代
垃圾回收分类
- 部分收集
- 新生代收集(Minor GC/ Young GC)
- 只是新生代(eden、s0、s1)的垃圾收集
- 老年代收集(Major GC / Old GC)
- 只是老年代的垃圾收集
- 目前,只有CMS GC会有单独收集老年代的行为
- 很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
- 混合回收(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
- 目前只有G1 GC会有这种行为
- 新生代收集(Minor GC/ Young GC)
- 整堆收集(Full GC)
- 收集整个java堆和方法区的垃圾
- 整堆收集
分代式GC策略触发
- 年轻代
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,S区满不会引发GC
- 因为java对象大多数都是朝生夕死的,所以Minor GC非常频繁,一般回收速度比较快
- Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复
- 老年代
- 出现了Major GC,经常会伴随至少一次的Minor GC
- Major GC 的速度一般会比Minor GC慢10倍以上,STW的时间更长
- 如果Major GC 后,内存还不足,就报OOM了
- Major GC的速度一般会比Minor GC慢10倍以上
内存分配策略
- 优先分配到Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态年龄判断
- 如果s区中相同年龄的所有对象大小的总和大于s空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
- 空间分配担保
- -XX:HandlePromotionFailure
TLAB
什么是TLAB
- jvm为每个线程在Eden区分配了一个私有的缓存区域
对象分配过程
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但jvm确实将TLAB作为内存分配首选
- 在程序中,可以通过"-XX:UseTLAB"设置是否开启TLAB空间
- 默认情况下,TLAB空间只占Eden的1%
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试这通过使用加锁机制确保数据操作的原子性,从而直接在eden中分配内存
常用参数
- -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
- -XX:+PrintFlagsFinal: 查看所有的参数的最终值
- -Xms: 初始堆空间内存 (默认为物理内存的1/64)
- -Xmx: 最大堆空间内存 (默认为物理内存的1/4)
- -Xmn: 设置新生代的大小
- -XX:NewRatio: 配置新生代与老年代在堆结构的占比
- -XX:SurvivorRatio: 设置新生代中Eden和s0/s1空间的比例
- -XX:MaxTenuringThreshold: 设置新生代垃圾的最大年龄
- -XX:+PrintGCDetails: 输出详细的GC处理日志
- -XX:HandlePromotionFailure: 是否设置空间分配担保
逃逸分析
概念
逃逸分析的基本行为就是分析对象动态作用域
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸
代码优化
- 栈上分配
- 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
- 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
- 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分可以不存在内存里,而是存在CPU寄存器中
方法区
概述
- 方法区与java堆一样,是各个线程共享的内存区域
- 方法区在jvm启动的时候被创建,并且它的实际的物理内存空间中和java堆区一样都可以是不连续的
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可以拓展
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGen space 或者 java.lang.OutOfMemoryError: Metaspace
- 关闭jvm就会释放这个区域的内存
- jdk7以前,方法区成为永久代,jdk8开始,使用元空间取代了永久代
- 元空间不在虚拟机设置的内存中,而是使用本地内存
- 动态调整参数
- jdk7及以前
- 通过-XX:PermSize来设置永久代初始分配空间,默认是20.75M
- -XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认为64M,64位机器模式是82M
- jdk8及以后
- 元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数
- 默认值依赖于平台
- windows:-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,没有限制
- -XX:MetaspaceSize: 设置初始的元空间大小。如果超过这个大小,Full GC将会被触发并卸载没用的类。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。
- 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁的GC,建议将-XX:MetaspaceSize设置为一个相对较高的值
- jdk7及以前
内存机构
- 类信息(对每个加载的类型 类class、接口interface、枚举enum、注解annotation)
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
- 域信息(field)
- jvm必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final等)
- 方法信息
- 方法名称
- 方法返回类型
- 方法参数的数量和类型
- 方法的修饰符
- 方法的字节码、操作数栈、局部变量表及大小
- 异常表
- 常量、
- (static final) 每个全局常量在编译的时候就会被分配了
- 静态变量、
- non-final:
- 随着类的加载而加载
- 类变量被类的所有实例共享,即使没有类实例也可以访问它
- non-final:
- 运行时常量池
- 字面量
- 对类型、域和方法的符号引用
- 即使编译器编译后的代码缓存等。
方法区的演变
- jdk6
- 方法区的实现为永久代, 包含字符串常量池、静态变量
- jdk7
- 方法区的实现为永久代,字符串常量池和静态变量已经放在堆中
- jdk8
- 方法区的实现为元空间,字符串常量池和静态变量放在堆中,元空间转移到物理内存上