目录
一、内存
内存是非常重要的系统资源,是硬盘和cpu的中间桥梁,承载着系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配和管理的策略
保证了JVM的高校稳定运行。不同的jvm对于内存的划分方式和管理机制存在着部分差异(对于HotSpot主要指方法区)
二、JVM运行时数据区
Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,只有在Java虚拟机退出时才会被销毁
其他数据区域是每个线程的。每线程数据区域在线程创建时创建,在线程退出时销毁。
1.PC寄存器
JVM官方说明
Java虚拟机可以支持多个线程同时执行。每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。在任何时候,每个Java Virtual Machine线程都在执行单个方法的代码,即
该线程的当前方法。如果该方法不是本地的,则pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前正在执行的方法是本地的,则Java虚拟机的pc寄存器的值是
未定义的。
作用
PC寄存器是用来存储指向下一条指令的地址,也即将将要执行的指令代码
由执行引擎读取下一条指令
1.它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
2.在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的
生命周期与线程的生命周期保持一致
3.任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法
程序计数器会存储当前线程正在执行的java方法的JVM指令地址;
或者,如果是在执行native方法,则是未指定值(undefined)
4.它是程序控制流的指示器,分支、循环、跳转、异常处理、
线程恢复等基础功能都需要依赖这个计数器来完成
5.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
6.它是唯一一个在java虚拟机规范中没有规定任何OOM情况的区域
示例:
代码:
public static void main(String[] args) {
int i = 1;
int x = 0;
int y = i + x;
System.out.println(y);
}
字节码:
面试题常问:
1.使用PC寄存器存储字节码指令地址有什么用呢?/为什么要使用PC寄存器记录当前线程的执行地址呢?
CPU是在来回不断切换各个线程,这时候切换回来以后就得知道从哪里继续执行
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
2.PC寄存器为什么会设定为线程私有的
多线程下在一个特定的时间执行其中某一个线程的方法
3.CPU不停地切换,这样必然会导致经常中断或恢复,如何保证分毫无差
为了能够准确地记录各个线程正在执行的当前字节码指令地址
最好的办法自然是为每一个线程都分配一个PC寄存器
这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况
2.本地方法栈
官方说明
Java虚拟机的实现可以使用传统堆栈(俗称“C堆栈”)来支持本地方法(用Java编程语言以外的语言编写的方法)。本地方法栈的实现也可以使用一个翻译为Java虚拟机的指令集的语言如c . Java虚拟机实现,无法加载本地方法,自己不依赖传统的本地方法栈栈不需要供应。如果提供了,通常在创建每个线程时为每个线程分配本机方法堆栈。
该规范允许本地方法栈的大小固定,或者根据计算的需要动态扩展和收缩。如果本机方法堆栈的大小是固定的,那么在创建该堆栈时可以独立地选择每个本机方法堆栈的大小。
Java虚拟机实现可以让程序员或用户控制本地方法栈的初始大小,在本地方法栈大小可变的情况下,还可以控制最大和最小方法栈大小。
以下异常条件与本地方法栈相关:
如果线程中的计算需要比允许的更大的本机方法堆栈,Java虚拟机将抛出StackOverflowError。
如果本机方法堆栈可以动态扩展,并且尝试进行本机方法堆栈扩展,但可用内存不足,或者如果为新线程创建初始本机方法堆栈可用内存不足,则Java虚拟机抛出OutOfMemoryError
作用
1.它的具体做法是Native Method Stack中登记native方法
在Execution Engine执行时加载本地方法库
2.当某个线程调用一个本地方法时它就进入了一个全新的并且不再受虚拟机限制的世界它和虚拟机拥有同样的权限
3.本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区,它甚至可以直接使用本地处理器中的寄存器
直接从本地内存的堆中分配任意数量的内存,并不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、。具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈
4.在hotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一
3.Java虚拟机栈
官方说明
每个Java虚拟机线程都有一个私有的Java虚拟机堆栈,与线程同时创建。Java虚拟机栈存储帧。Java虚拟机堆栈类似于C等传统语言的堆栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。因为除了推入和弹出帧外,JVM堆栈从来不会被直接操作,所以帧可以被堆分配。Java虚拟机堆栈的内存不需要是连续的
作用
内存中的堆和栈
栈是运行的单位,堆是存储的单位;栈解决程序的运行问题,堆解决数据存储的问题
栈的作用
1.每个线程在创建的时候都会创建一个虚拟机栈,其内部保存的一个个栈帧,对应一次次的方法调用
它是线程私有的
2.声明周期和线程是一致的
3.它主管java程序的运行,它保存方法的局部变量(8种基本数据类型、对象引用地址)、部分的结果、并参与方法的调用和返回;
设置栈的大小
-Xss256k
栈的存储结构
每个栈帧中存储着
1.局部变量表
2.操作数栈
3.动态链接
4.方法返回地址
5.一些附加信息
局部变量表
也称为本地变量数组或本地变量表
1.定义为一个数字数组,主要用于存储方法参数和定义在方法内的局部变量这些数据;类型包括各种基本数据类
型、对象印象(reference)以及returnAddress类型
2.因为局部变量表是建立在每个私有线程的栈上,是线程私有的数据,因此不存在数据安全的问题
3.局部变量表所需的容量大小是在编译期间确定下来的,并保存在方法的code属性maximum local variables
数据项中,在方法运行期间是不会改变局部变量表的大小的
4.局部变量表中的变量只在当前方法中调用有效,在方法执行时,虚拟机通过使用局部变量表完成参数变量列表
的传递。当方法结束后,随着栈帧的销毁,局部变量表也会随之销毁
变量槽
上面说到,局部变量表定义为一个数组,所以参数值的存储总是在局部变量数组的index0开始,
到数组长度-1结束;
而局部变量表中,最基本的存储单元是Slot
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long
和double)占用两个slot。 byte、short、char、float在存储前被转换为int,boolean也被转换为int
0表示false,非0表示true;long和double则占据两个slot
1.JVM会为局部变量表中的每一个slot都分配一个访问索引
通过这个索引即可成功访问到局部变量表中指定的局部变量值
2.当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量
将会按照顺序被复制到局部变量表中的每一个slot上
3.如果需要访问局部变量表中一个64bit的局部变量值时
只需要使用前一个索引即可。(比如:访问long或者double类型变量)
4.如果当前帧是由构造方法或者实例方法创建的
那么该对象引用this将会存放在index为0的slot处
其余的参数按照参数表顺序排列
Slot的重复利用:
栈帧中的局部变量表中的槽位是可以重复利用的。如果一个局部变量过了其作用域。那么在其作用域之后申明
的新的局部变量就很有可能会复用过期局部变量的槽位从而达到节省资源的目的
操作数栈
每一个栈帧内部都包含一个运行时常量池或该栈帧所属方法的引用
包含这个引用的目的就是为了支持当前党发的代码能够实现动态链接 invokedynamic指令
在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
方法返回地址
存放调用该方法的PC寄存器的值
一个方法的结束,有两种方式:
正常执行完成
出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置
方法正常退出时,调用者的pc计数器的值作为返回地址
即调用该方法的指令的下一条指令的地址
而通过异常退出时,返回地址是要通过异常表来确定
栈帧中一般不会保存这部分信息
本质上,方法的退出就是当前栈帧出栈的过程。此时
需要恢复上层方法的局部变量表、操作数栈、
将返回值也如调用者栈帧的操作数栈、设置PC寄存器值等
让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:
通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
当一个方法开始执行后,只要两种方式可以退出这个方法:
1、执行引擎遇到任意一个方法返回的字节码指令(return)
会有返回值传递给上层的方法调用者,简称正常完成出口;
一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值
的实际数据类型而定
在字节码指令中,返回指令包含ireturn(当返回值是boolena、byte、char、short和int类型时使用)、
lreturn、freturn、dreturn以及areturn 另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用
2、在方法执行的过程中遇到了异常(Exception)
并且这个异常没有在方法内进行处理
也就是只要在本方法的异常表中没有搜素到匹配的异常处理器
就会导致方法退出,简称异常完成出口
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表
方便在发生异常的时候找到处理异常的代码。
一些附加信息
栈帧中还允许携带与java虚拟机实现相关的一些附加信息
例如,对程序调试提供支持的信息
常见面试题
1.举例栈溢出的情况?(StackOverflowError)
递归调用等,通过-Xss设置栈的大小
2.调整栈的大小,就能保证不出现溢出么?
不能 如递归无限次数肯定会溢出,调整栈的大小只能保证溢出的时间晚一些
3.分配的栈内存越大越好么?
不是,会挤占其他线程的空间
4.垃圾回收是否设计到虚拟机栈?
不会
4.堆
核心概念
1.一个JVM实例只存在一个堆内存,堆也是java内存管理的核心区域
2.Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间(对内存的大小是可以调节的)
3.《Java虚拟机规范》规定,堆可以处于 物理上不连续 的内存空间中 但在 逻辑上它应该被视为连续的
4.所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(TLAB Thread Loacl Allocation Buffer)
面试题:堆空间一定是所有线程共享的么?不是,TLAB线程是在堆中独有的
5.《Java虚拟机规范》中对堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上
从实际的角度看,"几乎"所有的对象的实例都在这里分配内存(几乎是因为可能存储在栈上)
6.数组或对象永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数据再堆中的位置
7.在方法节数后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才被移除
8.堆是GC(垃圾收集器)执行垃圾回收的重点区域
堆的细分内存结构
1.JDK至以前:新生代+老年代+永久代
Young Generation Space:又被分为Eden区和Survior区 ==Young/New==
Tenure generation Space: ==Old/Tenure==
Permanent Space: ==Perm==
2.JDK8以后:新生代+老年代+元空间
Young Generation Space:又被分为Eden区和Survior区 ==Young/New==
Tenure generation Space: ==Old/Tenure==
Meta Space: ==Meta==
JDK1.7中的堆内存结构
JDK1.8中的堆内存结构
设置堆内存大小与OOM
-Xms 用于表示堆的起始内存,等价于 -XX:InitialHeapSize
用来设置堆空间(年轻代+老年代)的初始内存大小
-X : jvm 运行参数
ms : memory start
-Xmx 用于设置堆的最大内存,等价于 -XX:MaxHeapSize
一旦堆区中的内存大小超过了 -Mmx指定的最大内存将会抛出OOM异常
通常会将-Xms和-Xmx两个参数配置相同的值
其目的就是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小从而提高性能
默认情况下,初始内存大小:物理内存大小/64;最大内存大小:物理内存大小/4
手动设置:-Xms600m -Xmx600m
查看设置的参数:
1.打开cmd输入jps,通过jstat -gc 进程id
2.通过添加VM Options -XX:+PrintGCDetails
年轻代与老年代
存储在JVM中的java对象可以被划分为两类:
一类是声明周期比较短的瞬时对象,这类对象的创建和消亡都非常快另外一类对象生命周期非常长在某些情况下还能与JVM生命周期一致
JVM堆区进一步细分可以分为年轻代(YoungGen)和老年代(OldGen)
其中年轻代分为Eden区、Survivor0和Survivor1区(也叫from和to区)
对象分配过程
1.new的对象先放伊甸园区。此区有大小限制
2.当伊甸园区的空间填满时,又有新的对象进来。JVM的垃圾回收期将对伊甸园区
进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁
再加载新的对象放到伊甸园区中
3.将伊甸园区剩余对象移动到幸存者from区中
4.如果再次触发垃圾回收,此时上次幸存下来在from区的
如果没有回收,就会将本次Eden区剩下的和上次from区的
对象都放到了另一个to区当中。
此时to区则变成了from区,from区变成了to区
to区和Eden区会清空
5.通过年龄计数器(默认值15)计算该对象如果达到阈值还存活则放到
老年代中。可以设置参数:-XX:MaxTrnuringThreshold设置阈值
6.当老年代内存不足时,会再次出发GC:Major GC,进行养老区的内存清理
7.若养老区执行了Major GC之后发现依然不够内存保存对象则会报OOM
总结:
针对幸存者s0,s1区:复制之后有交换,谁空谁是to