虚拟机栈(栈是运行时的单位)
1.作用
- 存储一个一个栈帧(Stack Frame),对应着一次次的java方法调用
- 主管java程序的运行,保存着方法的局部变量(8种基本数据类型,如果该局部变量是引用类型变量,则只存储对象的引用地址,该对象本身存储在堆空间中)、部分结果,并参与方法的调用和返回
2.生命周期
与线程的生命周期一致
3.栈的特点
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- JVM直接对java栈的操作只有两个:
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后的出栈
- 对于栈来说不存在垃圾回收问题
4.问题
-
(1)java开发会出现那些异常
java虚拟机规范允许java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的java虚拟机栈,线程请求分配的栈容量超过java虚拟机允许的最大容量,java虚拟机会抛出一个StackOverflowError异常
- 如果Java虚拟机栈可以动态扩展,在尝试扩展时无法申请到足够的内存,或者创建新的线程时没有足够的内存去创建对应的虚拟机栈,Java虚拟机将会抛出OutOfMemoryError异常。
-
(2)举例栈溢出的情况
- 通过-Xss设置栈的大小;OOM
-
(3)调整栈大小,就能保证出现溢出吗?
- 不能,只能让出现溢出的时间晚一点,比如死循环,无论如何都会溢出
-
(4)分配的栈内存越大越好吗?
- 不是,内存空间的大小是固定的,栈空间变大,其他的空间就要相对减少,对整个JVM来说是不好的
-
(5)垃圾回收是否会涉及虚拟机栈?
- 不会,栈是用完就出栈,不存在垃圾
-
(6)方法中定义的局部变量是否线程安全
- 要根据具体情况去分析,如果有多个线程操作,就会出现线程安全问题
5.设置栈的大小
- -Xss256k
6.栈的存储单位
- 每个线程都有自己的栈,栈中的数据以**栈帧**的格式存在
- 在这个线程上正在执行的每个方法都各自对应一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系者方法执行过程中的各种数据信息
7.栈帧的内部结构
7.1 局部变量表(Local Variables)
(1)定义为一个数组,主要存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象引 用,以及returnAddress类型
(2)局部变量表所需的容量大小是在编译期确定的
(3)局部变量表是建立在栈上的,是线程私有的,不存在数据安全问题
(4)最基本的存储单元是Slot槽,32位以内的类型只占用一个slot,64位的类型(long和double)占用两个slot
7.2 操作数栈(Operand Stack)
(1)主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
(2)使用了数组结构来实现,但是不能采用访问索引的方式来进行数据访问,只能通过入栈(push)和出栈(pop)操作来完成一次数据访问
(3)32位类型占用一个栈单位深度,64位类型占用两个栈深度单位
7.3 动态链接(Dynamic Linking),也叫运行时常量池的方法引用
(1)栈顶缓存技术:HotSpot虚拟机采用零地址指令,所以完成一项操作需要使用多次入栈和出栈的操作,这就意味着需要更多的指令分派次数(instruction dispatch)和内存读写次数,由于操作数是存储在内存中的,频繁读写必然影响执行速度,因此提出了栈顶缓存技术,即将栈顶的元素全部缓存在物理CPU的寄存器中,依次降低对内存的读写次数,提升执行引擎的效率。
(2)每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,动态链接就是为了将这些符号引用转换为调用方法的直接引用
7.4 方法返回地址(Return Address)
- 存放调用该方法的PC寄存器的值
7.5 一些附加信息
堆(堆是存储的单位)
1. 堆(新生区+老年区)空间参数设置
-X是jvm的运行参数
- -Xms :memory start,设置堆区起始内存,等价于-XX:InitialHeapSize
- -Xmx:设置堆区的最大内存,等价于-XX:MaxHeapSize
- -Xmn:设置新生代的空间大小,如果与XX:NewRatio发生矛盾,以指定的新生代大小为准
- -XX:NewRatio=2,设置新生代与老年代的比例,默认1:2,表示新生代占1,老年代占2,新生代占整个堆的1/3
- -XX:SurvivorRatio = 8,设置Eden和两个Survivor区的比例,默认是8:1:1
- -XX:-useAdaptiveSizePoliyc,默认Eden和两个Survivor是8:1:1,但是有自适应机制,该命令用于关闭自适应机制
1.1 查看设置的参数
- jps / jstat -gc 进程id
- XX:-PrintGCDetails
2. 堆结构
- jdk1.7之前,堆分为新生区、养老区和持久代
- jdk1.8以后,堆分为新生区、养老区和元空间
- 元空间虽然逻辑上划分为堆,但其实元空间的具体实现是在方法区,元空间使用的是本地内存,属于堆外内存
2.1 新生区
2.1.1 Eden区(伊甸园区)
- 几乎所有的对象最先创建(new)的地方
- TLAB(线程私有)占整个Eden区域的1%,-XX:TLABWasteTargetPercent设置TLAB所占空间在Eden区的百分比,它是在 Java 堆中划分出来的针对每个线程的内存区域,专门在该区域为该线程创建的对象分配内存。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。
2.1.2 survivor区(幸存者区,也叫from、to区)
- survivor区有0区和1区,也叫from、to区,from区和to区是不断改变的
3. 堆空间常用参数设置
-
-XX:+PrintFlagsInitial 查看所有的参数的默认初始值
-
-XX:+PrintFlagsFinal 查看所有的参数的最终值
- jps:查看当前运行中的进程
- jinfo -flag SurvivorRatio 进程id
-
-Xms: 初始化堆空间内存(默认为物理内存的1/64)
-
-Xmx: 最大堆空间内存(默认为物理内存的1/4)
-
-Xmn: 设置新生代的大小
-
-XX:NewRatio: 设置新生代与老年代在堆结构的占比
-
-XX:SurvivorRatio: 设置新生代中Eden与s0/s1空间的比例
-
-XX:MaxTenuringThreshold: 设置新生代垃圾的最大年龄
-
-XX:+PrintGCDetails 输出详细的GC处理日志
-
打印GC的简要信息 ①-XX:+PrintGC ②-verbose:gc
-
-XX: HandlePromotionFailure: 是否设置空间分配担保
-
-Xms和-Xmx设置成相同值的原因
堆内存使用情况的变并不只是单纯的扩大和缩小内存,在此之前还会执行GC,如-Xms设置较小会引起频繁的GC,GC会殷勤STW,所以开发中奖这两个参数设置成相同的值
4. 栈上对象分配
- 对于没有发生逃逸分析的对象(该对象有没有在外部被使用),可以把它分配到栈上,当这个对象所在的方法被使用之后就会弹出栈,可以减少GC的次数
程序计数器(PC寄存器)
1.作用:
- 存储指向下一条指令的地址,由执行引擎读取下一条指令
2.生命周期
与线程的生命周期一致
3.特点
- 是一块很小的内存
- 存储当前线程正在执行的java方法的JVM指令地址
- 如果是本地方法,则是未指定值(undefined)
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域
- 不存在GC问题
4.问题
-
(1)为什么要使用PC寄存器?
因为CPU需要在各个线程之间来回切换,切换回来之后得知道从哪里开始继续执行,JVM字节码解释器就是需要通过改变PC寄存器的值来明确下一条要执行什么样的字节码指令
-
(2)为什么PC寄存器要线程私有?
要记录每个线程线程下一条指令的地址,如果PC寄存器共享,上一个线程的下一条指令地址会被下一个线程的下一条指令地址覆盖
本地方法栈
1. 本地方法(Native Method)
1.1 定义
- 使用native修饰的方法
1.2.使用本地方法的原因
- 与Java环境外交互
- 与操作系统交互,即有时候需要与操作系统或硬件交换信息时,需要使用C、C++的一些方法
- java的解释器是用C实现的,这使得它能像C一样与外部交互
方法区(逻辑上属于堆的一部分)
1. 设置方法区的内存大小
- -XX:MetaSpaceSize=size ,默认是21M
- -XX:MaxMetaSpaceSize 的默认值为-1,表示没有限制
2.存放什么东西
- 类信息、常量、静态变量、即时编译器编译后的代码缓存
3. 会发生OOM,也会GC
4. 要明确的几点
- 只有HotSpot虚拟机才有永久代
- jdk1.6之前,有永久代(permanent generation),静态变量存放在永久代
- jdk1.7,有永久代,但是已经逐渐“去永久代”,字符串常量池、静态变量移除,保存在堆中
- jdk1.8之后,无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍保存在堆中
5. 为什么要使用元空间替代永久代?
- (1)为永久代设置大小比较困难,太小容易发生Full GC和OOM(STW),太大了浪费空间,而元空间使用本地内存,最大空间没有限制
- (2)对永久代的调优比较困难,因为要判断一个类是否不再被使用很困难,会耗费大量时间
- 每个线程都有自己的程序计数器、栈、本地方法栈
- 线程之间共享:堆、堆外内存(永久代或元空间、代码缓存)