堆、栈、方法区的交互关系
-
运行时数据区为
方法区 堆区 栈区 程序计数器 本地方法栈
方法区和堆区是线程共享的,栈区、程序计数器、本地方法栈每个线程有自己的
-
交互关系
Person person = new Person();
person存放于栈中的栈帧中的局部变量表中。
new Person() 此对象存在于堆中,其中保存了指向对象类型数据的指针
类型数据存放于方法区中
程序计数器/PC 寄存器
-
程序计数器 (PC Register、Program Count Register)
是一块很小的内存空间,几乎可以忽略不计,是运行速度最快的存储区域
每个线程都由独属于自己的程序计数器,生命周期与线程周期一致
他是唯一一个在Java虚拟机规范中没有规定任何OutOtMemoryError情况的区域
-
程序计数器的作用
程序计数器用来存储指向下一条指令的地址,由执行引擎读取下一条指令
-
面试题
- 为什么使用程序计数器记录当前线程的执行地址
CPU需要不停切换各个线程,在切换到某线程后,需要知道当前线程从哪里开始执行
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
- PC寄存器为什么设定为线程私有
多线程在特定的时间内只会执行一个,CPU需要在多个线程之间来回切换,为了保证在继续执行某个线程时可以找到上次中断的位置,需要记录此线程
的执行位置。每个线程有自己的中断位置并发:CPU在多个线程之间快速切换
并行:同时进行 对应 串行:一个一个执行
虚拟机栈
虚拟机栈的相关知识
-
由于跨平台性的设计,Java的指令是根据栈设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的
优点:跨平台,编译容易实现
缺点:性能下降,实现同样的功能需要更多的指令
-
栈是运行时的单位:解决程序的运行问题;堆是存储的单位:解决的是数据存储的问题
-
Java虚拟机栈是每个线程私有的,主管Java程序的运行,内部保存着栈帧,每一个方法调用都对应一个栈帧
-
开发时和栈相关的异常:StackOverflowError(给线程分配的栈空间用尽),OutOfMemoryError(没有内存实现栈扩展)
-
可以使用-Xss选项设置线程的最大栈空间,栈空间的大小决定了方法调用的最大深度
-
栈中的数据以栈帧的格式存在,线程上正在执行的每个方法对应一个栈帧。只有栈顶栈帧是有效的,对应当前正在执行的方法。
此方法执行结束后对应栈帧出栈,此方法如果调用其他方法,对应栈帧入栈。
-
Java有两种返回函数的方式
(1)正常的函数返回,return指令
(2)抛出异常; 这两种方式都会导致栈帧被弹出
栈帧的结构
栈帧中的数据:局部变量表,操作数栈,动态链接,方法返回地址等
-
局部变量表
定义为一个数字数组,存储方法参数和局部变量。局部变量表的大小在编译期间确定,运行时不会改变栈帧中局部变量表中的槽位可以复用
对象引用作为隐式参数会放在index为0的slot处
局部变量表不存在系统初始化的过程,所以局部变量表必须手动初始化。
只要被局部变量中直接或间接引用的对象都不会被回收。
-
操作数栈
主要保存计算过程中用到的操作数每一个操作数栈的深度在编译期就定好了
-
动态链接:每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,动态链接的作用就是为了将这些符号引用转换为
调用方法的直接引用。
-
方法返回地址
存放调用该方法的程序计数器的值,以便方法退出后可以回到该方法调用的地方
方法的调用
- 在JVM中,将符号引用转换为方法的直接引用与方法的绑定机制相关
静态链接:被调用的方法在编译期可知,且运行期保持不变。这种情况下将调用方法的符号引用转换为直接引用的过程称之位静态链接。
动态链接:被调用的方法在编译期无法被确定下来,只能在程序运行时将方法的符号引用转换为直接引用
- 方法的调用:虚方法和非虚方法
非虚方法:方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的(静态方法、私有方法、final方法、构造器、父类方法)
其他方法称为非虚方法
- 虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就有可能影响到执行
效率。为了提高性能,JVM采用在类的方法去建立一个虚方法表,使用索引表来代替查找。虚方法表中存放着各个方法的实际入口。虚方法表在类加载的
链接阶段被创建并开始初始化。
栈的面试题
-
栈顶缓存:操作数栈存储在内存中,频繁读写内存会影响执行速度。HotSpot JVM设计者提出了栈顶缓存技术,将当前栈帧缓存到
CPU寄存器中,提高执行效率
-
举例栈溢出的情况
StackOverFlowError,OutOfMemory,递归结束提交不正确,造成无限递归,方法调用过深,通过-Xss设置栈的大小
StackOverFlowError(栈空间大小指定时,栈空间用尽)
OutOfMemory(栈空间可以动态扩容时,空间用尽)
- 调整栈的大小,可以保证不溢出吗
不能,只能推迟栈移除的时机
- 分配的栈内存越大越好吗?
不是,方法调用深度增加,线程占用的内存增加,线程数减小
- 垃圾回收是否涉及到虚拟机栈
不会
- 方法中定义的局部变量是否线程安全
要看参数是否来自于外部,是否返回到外部。如果在当前方法内部产生并且不做为返回值返回,则是线程安全的。
本地方法
-
一个Native Method就是一个Java调用非Java代码的接口
-
为什么使用Native Method
与Java环境外交互,与操作系统交互
-
本地方法栈
Java虚拟机栈用来管理Java方法的调用,本地方法栈用来管理本地方法的调用。
-
当某个线程调用本地方法时,他就进入一个全新的并且不再受虚拟机限制的世界。他和虚拟机拥有同样的权限。
堆区
堆的核心概述
-
一个JVM实例只有一个堆内存,Java堆区在JVM启动的时候被创建。堆可以处于物理上不连续的内存中,但在逻辑上他应该是连续的。(这里的连续是指虚拟内存
连续)。所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( Thread Local Allocation Buffer,TLAB)。在方法结束后,堆中的对象不会马上
被移除,仅仅在垃圾收集的时候才会被移除。 -
堆空间分为
Java7及以前:新生区、老年区、永久代
Java8及之后:新生区、老年区、元空间新生区可分为:伊甸园区、Survivor0、Survivor1(Survivor为幸存者区)
几乎所有的对象都是在伊甸园区new出来的
-
堆中各空间大小的设置
-Xms 设置堆区的起始内存大小
-Xmx 设置堆区的最大内存大小
一旦堆区的内存超出-Xmx指定的值,将会抛出OutOfMemoryError异常
通常会将-Xms和-Xmx设置为相同值,省去了Java垃圾回收之后重新计算堆区大小和需要扩容时申请内存的开销
默认情况下,初始内存大小为物理内存的1/64,最大内存大小为物理内存的1/4
-XX:NewRatio=ratio,设置新生区与老年区的比例,ration默认为2,表示新生区占1,老年区占2
-XX:SurvivorRatio,设置新生区和幸存者区的比例,默认(伊甸园区/Survivor0/Survivor1 = 8/1/1)
对象分配过程
1.使用new创建一个对象
2.1 如果伊甸园区可以放下,直接放入伊甸园区。
2.2 如果伊甸园区放不下,垃圾回收(YGC/Minor GC)对堆伊甸园区进行垃圾回收。
1. 将垃圾(不再被其他对象所引用的对象)回收
2. 将幸存者放到to区,并将他们的年龄计数器加1。对年龄计数器达到进入老年区条件的对象移入到老年区
3. 判断伊甸园区是否放得下
放的下:分配在伊甸园区
放不下:判断是否能在老年区放下,
放得下分配在老年区。
放不下触发FULL GC。FULL GC之后,如果放得下就放在老年区,放不下就报OOM错误。
Minor GC、Major GC、Full GC
Minor GC / Young GC :对新生代进行垃圾回收
Major GC / Old GC :对老年代进行回收
Full GC:对堆区和方法区进行垃圾回收
注意:很多时候Major GC和Full GC会混用,但是要区分具体是老年代回收还是整堆回收
Minor GC触发机制
当新生代的伊甸园区空间不足时,会触发Minor GC。Minor GC会引发STW(stop the world),暂停其他用户的线程。
Major GC触发机制
老年代空间不足时,会先尝试触发Minor GC,如果空间还不足,会触发Major GC。Major GC比Minor GC慢10倍以上,STW的时间更长。Major GC之后,内存还
不足,会报OOM。
Full GC触发机制
(1)调用System.gc()时,系统建议Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小小于老年代的可用内存
(5)由伊甸园区和from区向to区转移时,to区无法容纳,则把对象转存到老年区,但老年区可用内存不够时,触发Full GC
TLAB
什么是TLAB
从内存模型而不是垃圾回收的角度,对Eden区域继续进行划分,JVM为每一个线程分配了一个私有缓存区域。多线程同时分配内存时,
使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们将这种内存分配策略称为快速分配策略。
JVM将TLAB作为内存分配的首选。
使用-XX:UseTLAB,设置开启TLAB空间。默认情况下,TLAB空间的内存非常小 ,仅占有整个Eden空间的1%。
我们可以通过选项-XX:TLABWasteTargetPercent设置TLAB占用伊甸园区空间的百分比。
一旦对象在TLAB分配失败时,JVM尝试使用加锁机制确保数据操作的原子性。
堆空间的参数设置
-XX:PrintFlagsInitial:查看所有参数的默认初始值
-XX:PrintFlagsFinal:查看所有参数的最终值
-Xms:设置起始堆空间大小
-Xmx:设置最大堆空间大小
-Xmn:设置新生代的大小
-XX:NewRatio:设置新生代与老年代在堆空间中的比例
-XX:PrintGCDetails:处理详细的GC处理日志
其他
- 为什么要把Java堆分代
提升垃圾回收的效率
- 内存分配策略
优先分配到伊甸园区;大对象直接分配到老年代;长期存活的对象分配到老年代;动态对象年龄判断;空间分配担保
逃逸分析
-
逃逸分析的基本行为
分析对象动态作用域,当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。如果他被外部方法调用,
则认为发生了逃逸。 -
使用逃逸分析,编译器可对对象做如下优化
(1)栈上分配
(2)同步省略。一个对象被发现只能从一个线程被访问到。那么对这个对象的操作可以不考虑同步。
(3)分离对象或者标量替换。有的对象可能不需要作为一个连续的内存结构存在,也可以被访问到。那么对象的部分或
全部可以不存储在内存中,而是存储在CPU寄存器中。 -
JIT编译器在编译期间根据逃逸分析的结果,如果发现一个对象没有逃逸出方法的话,就有可优化为栈上分配。这样就无须进行
垃圾回收。 -
Oracle Hstport JVM 中,并未采取栈上分配,因此可以认为所有对象实例都分配在堆上。
方法区
方法区的理解
在《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去
进行垃圾回收或者进行压缩。”
对于HotSpotJVM而言,方法区还有一个别名叫做非堆(non-heap),目的就是要和堆分开
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
方法区在JVM启动的时候被创建,并且他的实际物理内存和堆区一样都可以是不连续的。
方法区的大小和堆区一样可以选择固定大小,也可以选择可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出。虚拟机会报内存溢出错误:
Java.lang.OutOfMemoryError:PermGen space
或者
Java.lang.OutOfMemoryError:Meta space
在JDK7及以前,把方法区称为永久代
在JDK8及以后,把方法区称为元空间
元空间和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存
设置方法区大小与OOM
(1)设置方法区的大小
JDK7及以前 永久代
-XX:PermSize=size(默认是20.75M),
-XX:MaxPermSize=size(32位机器下默认64M,64位机器下默认是82M)
JDK8及以后 元空间
-XX:MetaspaceSize=size(默认是21M)
-XX:MaxMetaspaceSize=size(默认是-1,表示没有限制)
(2)如何解决OOM
要确认是发生了内存泄露还是内存溢出
内存泄露:查看泄露对象到GC Roots的引用链,定位出泄露代码的位置,进行改善
内存溢出:判断是否能够增大空间,在代码上判断能否缩短某些对象的生命周期
方法区的内部结构
深入理解Java虚拟机》中对方法区的描述内容如下:他用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,域信息,
方法信息
类型信息:
(1)类的完整有效名称
(2)类型的直接父类的完整有效名称
(3)类型的修饰符(public、abstract、final的子集)
(4)类型直接接口的有序列表
(5)域(Field)信息:域名称,域类型,域修饰符(注意域和属性的区别)
(6)方法信息:
方法名称
方法的返回类型
方法参数的数量和类型(按顺序)
方法的修饰符(public、protected、private、static、abstract、final、native、synchronized)
方法的字节码、操作数栈、局部变量表及大小
异常表
(7)non-final的类变量
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类变量你也可以访问它
(8)运行时常量池(字节码文件中包含了常量池表,方法区包含了运行时常量池。常量池表在字节码加载后会放到运行时方法区中的运行时常量池中)
JVM为每一个已加载的类型(类或接口)都维护一个常量池。池中的数据象数组一样,通过索引访问
方法区的演进细节
如何实现方法区是虚拟机实现细节,不受《Java虚拟机规范》约束
jdk1.6及以前 永久代,相关信息都存放在永久代上,使用JVM内存
jdk1.7 永久代,但已经去永久代化,字符串常量池,静态变量移除,仅保存在堆中,使用JVM内存
jdk1.8及以后 元空间,字符串常量池,静态变量保存在堆中,使用本地内存
问:永久代为什么要用元空间替换掉
(1)提升了方法区的可用内存空间,减少了方法区OOM的可能
(2)对永久代调优困难
问:字符串常量池为什么要调整:永久代回收效率低,在full gc的时候才会触发。这导致StringTable回收效率不高。易导致永久代空间不足,因此放到
堆里,能够及时回收内存。
方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量,不再使用的类型
对常量池的回收策略:常量没有被任何地方引用就可以被回收
对类型的回收:条件非常苛刻,很难达到