Java运行时内存分布
Java运行时内存分布分为以下几个部分:
-
方法区
方法区主要存储:已经被JVM加载的类信息(版本、字段、方法、接口),常量,静态常量,即时编译器编译后的代码,数据。方法区是线程间共享的。
-
Java堆
是JVM所管理的内存中最大的一块,仅用来存放对象实例,是GC管理的主要区域。Java堆是线程共享的内存区域,因此我们在多线程开发时,要注意线程安全的问题。
按照存储时间的差异,堆内存可以划分为两个部分:新生代(Eden、Survivor),老年代。不同区域存储生命周期不同的对象,便于GC采取不同的算法进行垃圾回收。
-
虚拟机栈
JVM会为线程中的每个方法创建一个栈帧,因此本地方法栈也是线程私有的。
每一个栈帧包含以下信息:局部变量表、操作数栈、动态链接、返回地址。虚拟机栈会抛出StackOverflowError和OutOfMemoryError。
局部变量表:用来记录该方法产生的局部变量和方法传入的参数,Java文件编译成class文件之后,会记录局部变量表的容量(max_locals);
操作数栈:是一个后入先出的栈,用来执行具体的操作,方法执行过程中,会有各种各样的字节码指令压入和弹出操作数栈,操作数栈的大小也会写入方法的code属性表max_stacks数据项中;
动态链接:是为了支持方法调用过程中的动态连接,当一个方法要调用其他方法时,需要将方法的符号引用转为内存地址的直接引用,符号引用则存储在方法区中,而每一个栈帧都包含该栈帧对应方法的符号引用;
返回地址:当一个方法执行时,只有两种情况会退出这个方法,一是代码执行完毕或者遇到了return,二是方法抛出了没有被处理的异常。无论哪种方式退出,方法执行完成后,都需要回到方法被调用的位置,而返回地址就是为了帮助上层方法来恢复他的执行状态。
public static add(int i, int j) { int k = 2; return i + j + k; }
-
本地方法栈
本地方法栈与虚拟机栈类似,存储的是native方法。在一些虚拟机实现中,已经将本地方法栈与虚拟机栈合二为一了,例如HotSpot。
-
程序计数器
Java程序是多线程的,CPU会给每个线程分配时间片段,因此每个线程需要记住自己在时间片段中的代码执行情况,以便在下一个时间片段到来时继续执行自己线程的代码,因此程序计数器必须是线程私有的,随着线程的创建而创建,消亡而消亡。
代码的循环、分支、跳转以及异常抛出等都由程序计数器来控制。
Java虚拟机对程序计数器这一区域是没有任何OOM异常的。
当一个线程执行的是Java方法时,程序计数器指向虚拟机字节码指令的地址;执行的是native方法时,则为空。
GC回收机制与分代回收策略
GC(Garbage Collection)即垃圾回收,是Java虚拟机的重要知识点。垃圾就是系统运行时,内存中已经没有使用的对象。在Java虚拟机中,认为没有被GC Root对象直接或间接引用的对象,都认为是可回收的垃圾,被称为“可达性分析”算法。
-
GC Root
Java虚拟机中的以下对象被定义为GC Root对象:方法区中静态变量、Java虚拟机栈中引用的对象、仍然存活的线程对象、native方法中JNI引用的对象。
-
什么时候会触发GC?
- Allocation Failure:在给对象分配堆内存时,剩余内存空间不够,系统会触发一次GC;
- System.gc():在Java层调用该方法,手动触发一次GC。
常见的三种GC回收算法
-
标记回收算法
从“GC Roots”集合开始,遍历整个内存,保留被GC Roots直接或间接引用的对象,剩下的对象都标记为垃圾,等待系统回收。分为Mark标记阶段和Sweep清除阶段。
优点:实现简单;
缺点:需要中断线程内其他组件的执行,可能产生内存碎片,提高了GC的频率。
-
复制算法
将现有内存分为两块,每次只使用其中的一块。我们将两块内存称作A内存和B内存,当前使用A内存,当触发GC时,会将GC Roots直接或间接引用的对象移动到B内存当中,并将A内存中的对象全部清除,最后交换A内存和B内存的角色。
优点:按顺序分配内存即可,实现简单,执行高效,不需要考虑内存碎片;
缺点:实际只能使用一半的内存空间,对于生命周期较长的对象,会进行频繁的复制。
-
标记-压缩算法
从根节点出发,对所有可达对象进行一次标记,之后将所有标记的对象压向内存的一边,之后清除边界外的内存空间。分为Mark标记阶段和Compact压缩阶段。
优点:避免只能使用一半的内存空间,不需要考虑内存碎片;
缺点:压缩操作还是需要对对象进行复制,一定程度上降低了效率。
-
JVM分代回收策略
前文说过,按照存储时间的差异,堆内存可以划分为两个部分:新生代(Eden、Survivor),老年代。不同区域存储生命周期不同的对象,便于GC采取不同的算法进行垃圾回收。
对于不同的分代我们采取不同的回收策略。
-
新生代
新生代具体分为三个区域,分别时Eden、Survivor0(S0)、Survivor1(S1),这三个部分按照8:1:1的比例来划分新生代。
初次创建的内存对象会存活在Eden区域,在首次触发GC之后,会遍历Eden区域,将GC Roots直接或间接引用的对象全都复制到S0区域中,之后清除Eden区域留存的对象;当再次触发GC时,会遍历Eden区域和S0区域,将GC Roots直接或间接引用的对象全都复制到S1区域中,之后清除Eden区域和S0留存的对象。S0与S1之间来回复制多次依然存活,则认为该对象的生命周期较长,放到老年区。还有一种特殊情况,当存活的对象总大小大于S0(S1)区域内存空间的大小,会将多余的对象直接放到老年代中。
-
老年代
如果在多次GC后,对象依然存活,则会被放到老年代。老年代的内存空间一般比新生代大,用来存放更多的对象。一般使用标记压缩算法。
-