

共享:
方法区:用于存储已被虚拟机加载的类信息(元数据)、常量、静态变量等数据。
堆:用于存储对象实例和数组,是JVM中最大的一块内存区域,也是垃圾回收器主要工作的区域。
私有:
栈:每个线程都有自己的调用栈,用于存储局部变量和部分结果,以及在方法调用时的返回地址。
程序计数器:每个线程都有一个程序计数器,用于存储当前线程执行的字节码的行号指示器。
本地方法栈:用于支持本地方法的执行,存储本地方法的调用状态。

有些区域随着虚拟机的启动而开辟,随着虚拟机的终止而销毁,有的区域则是在运行过程中不断的创建与销毁。

线程私有区:
对于每条线程而言,在创建它们时,JVM都会为它们分配的区域,这些内存区域的生命周期会随着线程的启动、死亡而创建和销毁。这些区域创建后,其他线程是不可见的,只有当前线程自身可以访问。
运行时数据区中的线程私有区域主要包含:程序计数器、虚拟机栈以及本地方法栈。
程序计数器:
每条线程都有且只有一个程序计数器,线程间不相互干扰。生命随线程启动而生,线程销毁而亡。同时也是JVM所有内存区域中唯一不会发生OOM的区域,GC机制不会触及的区域。
当线程执行一个Java方法时,记录线程正在执行的字节码指令地址,当执行引擎处理完某个指令后,程序计数器将指针改向下一条要执行的指令地址,执行引擎根据记录的地址执行。
当线程在执行一些由C/C++编写的Native方法时,PC计数器中则为空。除此作用之外,也可以保证线程发生CPU时间片切换后能恢复到正确的位置执行(恢复上下文)。
可看作当前线程所执行的字节码的行号指示器。


虚拟机栈:
作用:保存方法的局部变量、部分结果,并参与方法的调用和返回。
特点:访问速度仅次于程序计数器。每个方法执行,伴随着入栈,方法执行结束出栈。所以栈不存在垃圾回收问题
栈主要作为运行时执行的单位,栈的作用是负责程序运行时具体如何执行、如何处理数据等工作。生命周期与线程一致,每个线程创建时都会创建一个虚拟机栈。
当执行一个Java方法时,都会为方法生产一个栈帧,每个Java方法的调用到执行结束,对应着虚拟机栈中的一个栈帧的从入栈到出栈的过程。
栈帧需分配的大小在编译期就已确定了,不受运行时变量数据大小影响。
执行引擎只会对位于栈顶的栈帧元素(当前栈帧)操作,与当前栈帧关联的方法被称为当前方法。定义这个方法的类就是当前类。
不同线程栈帧不许存在相互引用,即一个线程栈帧不可能引用另一个线程的栈帧。
一个线程创建一个虚拟机栈,线程中运行一个方法就生产一个栈帧压入栈中。
虚拟机栈的大小有可能是固定的也有可能是动态的,栈帧的大小在编译时就确定了,但是栈帧大小不固定,由具体方法决定。

弹栈的两种方式:
1.正常return。2.抛异常。
一个栈帧中主要包含局部变量表、操作数栈、动态链接、方法出口、附加信息等。

局部变量表:

- 存储内容:基本数据类型1、对象引用2(reference类型,不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向对象的句柄)和 returnAddress 类型3(指向一条字节码指令的地址,已被异常表取代。)
- returnAddress 类型占用一个槽,byte、short、char被转换为int,占1个槽
- long、double、引用类型占2个槽,占两个槽的只需要使用前一个索引即可。
- 线程私有,不存在数据安全问题。
- 局部变量表大小在编译期确定,方法运行期间不会改变局部变量表大小。
- 方法嵌套调用次数由栈大小决定。栈越大,栈帧越小,嵌套调用次数越多。
- 若当前帧由构造方法或实例方法创建,那么该对象引用 this 会放在0索引处,其余参数顺序排列(静态方法不可引用 this,就是因为其局部变量表无this)



操作数栈:

栈帧刚被创建出来时,操作数栈是空的。
每个操作数栈的最大深度在编译期就确定了。


栈顶缓存:将栈顶元素缓存在物理cpu的寄存器或高速缓存,降低对内存的读写。
动态链接:


一开始将class字节码文件加载进内存时,方法区只有符号引用,但是当走到解析阶段时,方法区里符号引用被解析成了直接引用,栈帧中的动态链接实际上是指向了运行时常量池中的直接引用。

静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。


方法出口(方法返回地址):
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。

方法正常退出时PC 计数器的值作为返回地址,即下一条指令的地址。
通过异常退出的,返回地址是要通过异常表来确定的,栈帧不保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。
正常和异常的区别在于:异常完成出口退出的不会给上层调用者任何返回值。
虚拟机栈的特点:


本地方法栈:

本地方法栈下线程私有。



线程共享区:
在运行时,这些区域对于程序中的所有线程而言都是可见的,这些区域的状态不会因为某一条线程的死亡而发生改变,这些区域创建后是与JVM同级别的,伴随JVM的生命周期共生共死。
线程共享区:堆空间、元数据空间(方法区)、直接内存(堆外内存)。
Java堆空间(Heap):
堆的大小在 JVM 启动时就确定了,可通过-Xmx和-Xms 来设定:
- -Xms 用来表示堆的起始内存,等价于-XX:InitialHeapSize
- -Xmx 用来表示堆的最大内存,等价于-XX:MaxHeapSize
如果堆的内存大小超过-Xmx设定的最大内存,就会抛出OOM异常。
通常将-Xmx 和-Xms两个参数设为相同, JVM 一开始就分配了固定大小的堆空间,不再需要根据垃圾回收的结果重新计算和调整堆大小 ,从而提高性能。
堆的大小是随着内存使用情况变化的。
默认情况下,初始堆内存大小为:电脑内存大小/64
默认情况下,最大堆内存大小为:电脑内存大小/4


堆空间其实不连续:


分代的唯一理由:优化GC性能。
分代堆空间:



年轻代:
所有新对象创建的地方。垃圾收集称为Minor GC/Young GC。
Eden+Survivor0+Survivor1(被称为from/to或s0/s1),默认比例是 8:1:1
新创建的对象存入Eden,Eden满了之后执行Minor GC,将存活对象存入s0或s1,每次s0和s1总有一个是空的,不能同时使用。
经过15次GC后活下来移到老年代。
Eden和Survivor区域的大小不是一定的,每次GC后都会重新计算大小。
老年代:
旧一代对象,经过多轮GC存活后晋升的对象。
老年代存满时垃圾收集称为Major GC,需更长时间。
大对象直接进入老年代。避免在Eden和s0和s1之间发生大量的内存拷贝。
默认情况下新生代和老年代的比例为1:2
元空间(方法区):
有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。
不分代堆空间:

不同版本的堆空间结构:








如果不分代的话,GC每次都要遍历一整个堆,堆越大扫描成本越高,而分代之后,可以根据存活时间长短的不同,采用不同的GC算法,提高GC效率,减少停顿时间。
堆总结:

本地内存(方法区+堆外内存):
运行时数据区中的本地内存主要可分为两块,一部分为元数据空间(原方法区) ,另一部分则为直接内存 。在任何一个平台上运行一个进程,操作系统都会为其分配对应的内存,JVM也不例外,在启动时也会向操作系统申请资源分配(内存、CPU、线程数等)。但值得注意的是:元数据空间和直接内存这两块区域,并不处于OS为JVM分配的内存中,而是直接使用物理机的内存进行数据存放,但是本地内存还是会被JVM管理。
元数据空间(原方法区):

方法区是一直存在的,永久代和元空间都是方法区的实现。
运行时常量池是方法区的一部分,存放编译期生成的各种字面量和符号引用。
运行期间也可能将新的常量放入池中,如 string.intern()方法。
方法区可选择固定大小也可扩展。
JVM 关闭后方法区即被释放
永久代是堆的一部分,受垃圾回收器管理
元空间存在于本地内存(堆外内存,不受垃圾回收器管理),不受 JVM 限制,难发生OOM
- 存储内容不同,元空间存储类的元信息。
- 静态变量和常量池等并入堆中。
- 相当于永久代的数据被分到了堆和元空间中。
所以对于方法区,Java8 之后的变化:
- 移除了永久代,替换为元空间
- 永久代中类的元信息转移到了本地内存
- 永久代中的常量池和静态变量转移到了堆






之前 方法区运行时常量池中的字符串常量池则被放置在了堆中,因为在程序运行过程中会随着运行时间的增加,字符串常量池中的字符串会越来越多,所占空间会越来越大,所以将其放在堆中的好处在于:使得字符串常量池在GC机制的范围之内,字符串也会存在回收操作。
同时除开字符串常量池被挪动到了堆内之外,类的静态变量的存储也被放在了堆中。


每一个类在加载的时候就会生成自己独立的运行时常量池,里面存储了这个类的各种常量


直接内存(堆外内存):

一般在使用直接内存的时候,不能将希望寄托给GC机制的全局GC来管理内存,因此我们可以和C语言一样,尝试自己写一个回收直接内存的方法,然后使用完成后自己手动回收申请的内存。