
运行时数据区分为五个模块:方法区,堆,Java栈,本地方法栈,PC程序计数器。
线程共享:方法区和堆
线程私有:Java栈,本地方法栈,PC程序计数器。
PC程序计数器
PC程序计数器每个线程都有一份,PC寄存器用来储存下一条指令的地址,执行引擎读取其存储的指令地址所指向的指令并执行。可以想象成一个行号的指示器。
使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址?
答:使用PC寄存器存储字节码指令地址保证CPU切换线程回来之后直到该从哪执行。
JVM的字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么设置为线程私有的?
答:为了保证线程切换之间不丢失执行的进度
并行和并发的区别
最本质的区别就是:并发是轮流处理多个任务,并行是同时处理多个任务
Java栈
栈解决程序的运行问题和数据存储问题。
它是线程私有的,一个线程对应一个虚拟机栈,内部是一个个的栈帧,一个栈帧对应着一个方法的调用。
栈帧的内部结构
每一个栈帧包含的内容有局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译代码时,栈帧需要多大的局部变量表,多深的操作数栈都可以完全确定的。
局部变量表(Local Variables)
局部变量表的作用就是存储该方法(一个栈帧代表着一个方法)所使用到的方法参数和局部变量。虚拟机是通过索引定位的方式使用局部变量表。它可以说这个方法的数据池,是我们方法中变量的化身,相当于把我们方法中所需要的变量整合成一个数组对象或集合对象,这个对象的名称就叫做局部变量表。
-
它所需的容量大小是在编译器确定下来的
-
当调用的方法是非static方法的时候,第一个slot(变量槽)存储的是this,分配完方法参数后便会依次分配内部定义的局部变量。
-
slot(变量槽)是可以复用的。因为即使是一个方法内,也是存在作用域的,当离开了某些变量的作用域之后,这些变量对应的 Slot 空间就可以交给其他变量使用。但是这种机制有时候会影响垃圾回收行为,原因很简单,当离开某个作用域时,如果没有新的变量值覆盖之前作用域内的变量(指reference)空间,那么当垃圾回收时,则该引用对应的java堆中的内存则不允许被回收,因为局部变量表中还存在该引用。所以问题在于虚拟机并没有主动清理局部变量表中离开作用域的变量值,而是采用新盖旧的方法被动清理。
public class Test { public static void main(String[] args) { int a = 1; { int b = 1; b = b +1; } int c = 2; } }
c变量在b变量生命周期结束后重新利用了序号2的数组空间,节约资源
注意:正常来说一个slot的占用32位的长度内存,可以存放 boolean、byte、char、short、int、float、reference 和 returnAddress 8种类型,而 对于64位的 long 和 double 变量而言,虚拟机会为其分配两个连续的 Slot 空间。
操作数栈(Operand Stack)
操作数栈是方法执行算术运算或者是调用其他的方法进行参数传递的时候时的媒介,当一个方法刚开始执行时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈的操作。
话不多说,看完图片你就明白了。
动态链接(Dynamic Linking)
-
每个栈帧都包含一个指向当前方法所在类型的运行时常量池的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)
-
Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
-
简单来说动态连接就是指向运行时常量池的一个地址,通过这个地址去调用常量池中所需要的方法。只要用到了这个方法就会调用常量池中相应的方法,而多处用到的都是常量池中的同一方法
方法返回地址(Return Address)
当一个方法开始执行以后,只有两种方法可以退出当前方法:
- 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
- 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
附加信息(省略)
方法中定义的局部变量是否线程安全?
在JVM中,局部变量便是存在与栈帧当中的,栈帧是存在在栈当中,每个栈都是线程私有的,所以线程是安全的。
堆
内存细分
伊甸园区,幸存者0,1区(也叫From区和To区),老年代区。
- 伊甸园区与幸存者区默认比例是8:1,但实际上是6:1,如下图:Eden Space :(Survivor 0+ Survivor 1 )= 6 :1,这是因为hot Spot使用了自适应空间。想要设置成8:1:1必须使用参数-XX:SurvivorRatio=8。
- 新生代和老年代的比例默认是1:2 可以通过-XX:NewRatio来指定
对象分配过程
伊甸园区满的时候会触发**YGC/Minor GC**,将还在使用的对象移动到from区,第二次触发Minor GC时,会将伊甸园区仍在使用的对象移动到to区,如果from区中的对象仍然在使用的话,就将他们移动到to区,来回反复直到当对象的age计数器达到15(默认)时,会将对象从幸存者区提升到老年代。
内存分配策略
-
优先分配到Eden
-
大对象直接分配到老年代
-
长期存活的对象分配到老年区
-
动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于改年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
-
空间分配担保:—XX:HandlePromotionFailure
Minor GC, Major GC, Full GC(后边会详细介绍)
Minor GC:新生代收集
Major GC:老年代收集,只有CMS GC会有单独收集老年代的行为
Mix GC:混合收集,收集整个新生代以及部分老年代的垃圾收集,只有G1 GC会有这种行为。
Full GC:搜集整个Java堆和方法区的垃圾收集
Minor GC
当伊甸园区满的时候触发Minor GC,每次Minor GC会清理新生代的内存。Minor GC会触发STW,等待垃圾回收结束。
Major GC
老年代空间不足时,会先尝试Minor GC,因为JVM会尝试回收幸存者区的内容,看是否能回收幸存者区的垃圾,不让幸存者区的对象晋升到老年代,如果之后空间还不足,则触发Major GC,如果Major GC后,内存还不足,就报OOM了
Major GC的速度一半会比Minor GC慢十倍以上
Full GC
- 调用System.gc()系统建议执行,但不必然执行。
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区,survivor space0(From区)向survivor space1(To区)复制时,对象大小大于To区可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
TLAB(Thread Local Allocation Buffer)堆空间中的不共享部分
JVM为每个线程分配了一个私有缓冲区域,它包含在Eden空间内,可以避免一系列的非线程安全问题。
方法区
方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
-
JDK1.6以及之前,方法区被称为永久代,所有的静态变量都存放在永久代上。
-
JDK1.7有永久代,但是已经在逐步去除永久代,字符串常量池,静态变量保存在堆中。
-
JDK1.8以及之后,永久代消失,类型信息,字段,方法,常量保存在本地内存的元空间中,但字符串常量池和静态变量仍在堆中。
运行时常量池与静态常量池
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
- 所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
- 而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
为什么需要常量池?
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。每一个栈帧内部都包含一个指向运行时常量池中该栈帧所述方法的引用,其实就是某条字节指令后会有#来对常量池引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
字符串常量池

字符串常量池会有助于在运行Java时节省大量的空间,当我们使用双引号创建字符串的时候,JVM会检查字符串常量池中是否有一样的字符串,如果有就返回该字符串的引用,没有的话则会创建新的字符串然后返回其引用。
如果使用new创建字符串,则会强制在堆空间中创建一个String类的对象。我们可以使用intern()方法将其放入字符串常量池或从字符串常量池中查找具有相同的值字符串对象并返回其引用
方法区的垃圾回收
方法区主要回收的是常量池中不再使用的类型和废弃的变量。
本地方法栈
对于一个运行中的Java程序而言,它还可能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但不止如此,它还可以做任何它想做的事情。
如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。
本图片引用自:https://www.cnblogs.com/wade-luffy/p/5813747.html
最后我们总结一下堆中各个部分的作用:
- PC程序计数器:记录下一条执行字节码的地址
- Java栈:由栈帧组成,栈帧包括局部变量表,操作数栈,动态链接,方法的返回地址和一些附加信息。它主要解决的是程序运行问题和数据存储问题。
- 堆:堆包括新生代(新生代包括伊甸园区和幸存者区)老年代。其中YGC/MinorGC是回收伊甸区和幸存者区,MajorGC单独回收老年代,FullGC是回收整个堆。伊甸园区:幸存者区默认是8:1,但是由于虚拟机自适应空间实际上是6:1。新生代和老年代默认是1:2
- 方法区:JDK1.6之前为永久代,静态变量和字符串常量池存在于永久代中。1.7逐渐去除永久代,静态变量和字符串常量池存在于堆空间中。1.8元空间代替永久代,元空间使用的是PC的直接内存,静态变量和字符串常量池仍存在于堆空间中。
- 本地方法栈:对本地方法的调用,如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。