灰色的为单线程私有的,红色的为多个线程共享的
每个线程:独立包括程序计数器丶栈丶本地栈
线程间共享:堆丶堆外内存(永久代或元空间[方法区]丶代码缓存)
程序计数器(PC寄存器)
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
一块很小的内存空间,几乎可以忽略不计。运行速度最快的存储区域
它是程序控制流的指示器,分支丶循环丶跳转丶异常处理丶线程恢复等基础功能都需要依赖这个计数器来完成
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
它是唯一一个JVM规范中没有规定任何OutOtMemoryError情况的区域。
使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码执行。
虚拟机栈
优点:跨平台,指令集小,编译器很容易实现;缺点是性能下降,实现同样的功能需要更多的指令
栈是运行时的单位而堆是存储的单位
Java虚拟机栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(一个栈帧对应一个Java方法),对应着一次次的Java方法调用。 线程是私有的. 生命周期和线程一致
作用:主管Java程序的运行,它保存方法的局部变量(8种基本数据类型丶对象的引用地址),部分结果,并参与方法的调用和返回。
JVM允许Java栈的大小是动态的或者固定不变的。
栈的优点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
JVM直接对Java栈的操作只有两个:
每个方法执行,伴随着进栈(入栈丶压栈);方法结束后的出栈工作
对于栈来说不存在垃圾回收问题
栈中存储
每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在,在这个线程上正在执行的每个方法都各自对应一个栈帧,栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈运行原理
jvm直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一个活动线程中,一个时间点上,只有有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的栈帧。
不同的线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,是的前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出
栈帧的内部结构
局部变量表
操作数栈(或表达式栈)
动态链接(或执行运行时常量池的方法引用)
方法返回地址(或方法正常退出或者异常退出的定义)
一些附加信息
局部变量表
局部变量表也称之为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法内的局部变量,这些数据类型包括各类基本数据类型丶对象引用,以及returnAddress类型。
由于局部变量表时简历在线程的栈上,是线程的私有数据,因此不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到阐述变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
关于Slot的理解
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
局部变量表,最基本的存储单元是Slot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量。
在局部变量表里,32位以内的类型只占用一个slot,64位的类型(long和double)占用两个slot。
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很可能会重复过期局部变量的槽位,从而达到节省资源的目的。
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈
操作数栈,在方法的执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈。
只要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
栈顶缓存技术
将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
Java语言中方法重写的本质
找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C
如果在过程结束;如果不通过类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过,则返回java.lang.IllegalAccessError异常
否则按照继承关系从下往上依次对C的各个父类进行第二部的搜索和验证过程
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
为了提高性能,JVM采用在类的方法区建立一个虚方法表(非虚方法不会出现在表中)来实现。使用索引表来代替查找。每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
帧数据区
方法返回地址
存放调用该方法的PC寄存器(该方法要执行的下一条指令的值)的值。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
动态链接
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
栈的相关面试题
举例栈溢出的情况?(StackIverFlowError)
通过-Xss设置栈的大小;OOM
调整栈大小,就能保证不出现溢出吗?不能
分配的栈内存越大越好吗? 不是,因为会挤占其他的内存空间。
垃圾回收是否会涉及到虚拟机栈?不会的!
方法中定义的局部变量是否线程安全?
本地方法栈
本地方法
一个native method就是一个Java调用非Java代码的接口.一个本地方法是这样一个Java方法,该方法由非Java语言实现,比如C语言。使用native关键字来修饰的方法,不能和abstract一起修饰方法,可以与其他标识符连用(public,private)
本地方法的使用原因
与Java环境交互- Java应用需要与Java外面的环境交互,这时本地方法存在的主要原因
与操作系统的交互 -通过使用本地方法,我们得以用Java实现了jre的底层系统的交互,甚至JVM的一部分就是用C写的。
Sun's Java- Sun的解释器是用C实现的,这使得它可以向一些普通的C一样与外部交互。
Java虚拟机栈用于管理Java方法的调用,而本地方法栈就是用于管理本地方法的调用。
本地方法栈也是线程私有的,允许被实现成固定或者是可动态扩展的内存大小。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
甚至可以直接使用本地处理器中的寄存器
直接从本地内存的堆中分配任意数量的内存
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等。
Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
堆
堆的核心概述
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB)。
(几乎)所有的对象实例以及数组都应该在运行时分配在堆上。
数组和对象可能永远都不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
堆,是GC执行垃圾回收的重点区域
内存细分
JDK7及以前堆内存逻辑上分为三部分:新生代+老年代+永久代
JDK8及之后堆内存逻辑上分为三部分:新生代+老年代+元空间
-Xms 用来设置堆空间(新生代+老年代)的初始内存大小
-Xmx 用来设置堆空间(新生代+老年代)的最大内存大小
-X 是JVM的运行参数 ms是memory start
开发中建议将初始堆内存和最大堆内存设置相同大小,避免频繁的扩容和释放造成不必要的系统压力。
新生代与老年代
存储在JVM中的Java对象可以被划分为两类:
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
另一类对象的生命周期很长,在某些极端的情况下还能够与JVM的生命周期保持一致。
Java堆区进一步可以划分为新生代与老年代
其中新生代又可以划分为Eden空间,from区和to区,默认8:1:1 -XX:SurvivorRatio来修改默认设置
默认新生代与老年代的结构占比为1:2,可以修改-XX:Ratio=4 即1:4
几乎所有的Java对象都是在Eden区被new出来的。(如果Eden放不下就直接进入老年代了)
几乎绝大部分的Java对象的销毁都在新生代进行。
对象分配过程
概述:为新对象分配内存是意见非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
new的对象先放Eden区,此区有大小限制
当Eden的空间填满时,程序又需要创建对象,JVM的垃圾回收期将对Eden区进行垃圾回收,将Eden中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到Eden区
让后在将Eden中剩余的对象移动到from区
如果再次触发GC,此时上次幸存下来的放到from区,如果没有回收,就会放到to区
如果再次经历垃圾回收,此时会重新放回到from区,再去to区
如果一个对象生命周期很长,超过15次,就会进入到老年代。可以设置次数。
Minor GC,Major GC与Full GC
JVM在进行GC时,并非每次都对上面三个内存(新生代,老年代,方法区)区域一起回收,大部分时候回收都指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一部分是部分收集(Partial GC),一种是整堆收集(Full GC)
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
新生代收集(Minor GC /Young GC):只是新生代的垃圾收集
老年代收集(Major GC/Old GC):只是老年代的垃圾收集
目前,只有CMS GC会有单独收集老年代的行为
很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
目前只有G1 GC会有这种行为
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
新生代GC(Minor GC)触发机制:
当新生代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden满,Survivor满不会引发GC。
因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
Minor GC会引发STW(Stop-The-World,在执行垃圾回收算法时,Java应用程序的其他所有线程全部被挂机除了GC,所有Java代码停止,native代码可以执行,但不能与JVM交互),暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代GC(Major GC/Full GC)触发机制:
指发生在老年代的GC,对象从老年代消失时,Major GC或Full GC发生了
出现了Major GC,经常会伴随至少一次的Minjor GC
Major GC的速度一般会比Minjor GC慢10倍以上,STW的时间更长。
如果Major GC 后,内存还不足,就报OOM了。
Major GC的速度一般会比Minor GC慢10倍以上。
Full GC触发机制:
调用System.gc()时,系统建议执行Full GC,但是不必然 执行
老年代空间不足
方法区空间不足
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
由Eden区,from区向to区复制时,对象大小大于to区可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
full GC是开发或调优中尽量避免的。这样STW时间会短一些
堆空间分代思想
其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块。GC的时候要找到哪些对象没用,就会对堆的所有区域进行扫描。而很多对象都是朝生夕死,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储朝生夕死对象的区域进行回收,就会腾出很大的空间出来。
内存分配策略
优先分配到Eden
大对象直接分配到老年代(开发中应尽量避免程序中出现过多的大对象)
长期存活的对象分配到老年代
动态对象年龄判断
如果Survicor区中相同年龄的所有对象大小的总和大于Survicor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
-XX:HandlePromotionFailure
对象分配过程:TLAB
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓冲区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略
几乎所有OpenJDK衍生出来的JVM都提供了TLAB的设计
测试堆空间常用的JVM参数:
-XX:+PrintFlagsInitial :查看所有的参数的默认初始值
-XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
-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:是否设置空间分配担保