导图社区 Android知识体系之1---JVM全解
花了几个星期的时间专门研究JVM,梳理知识点后精心制作的JVM全解知识体系图,希望能够帮助Android程序员,作者pwhbys。
编辑于2023-05-25 18:28:50 北京市JVM全解
作者:pwhbys
JVM运行时数据区(5个部分)
线程私有的(3个)
虚拟机栈(不用垃圾回收):虚拟机栈为虚拟机执行 Java方法 (也就是字节码)服务
本地方法栈(不用垃圾回收):本地方法栈则为虚拟机使用到的Native方法服务
程序计数器(不用垃圾回收):指向当前线程正在执行的字节码指令的地址(行号)
特点:每个线程独有、内存较小、独立存储、多线程互不影响、不会OOM。
为什么需要程序计数器(面试)? Java是多线程的,意味着线程切换确保多线程情况下的程序正常执行。
线程共有的(2个)
方法区(有垃圾但不用管):又称为运行时常量池,存放类的静态变量和常量。
堆区(需要垃圾回收):存放直接new出来的对象
Java 运行时内存分配策略(3种)
静态分配(方法区)
满足条件:存放静态数据(全局 static 数据和常量)
回收时机:内存在程序编译时就已经分配好,整个运行期都存在。
优点:生存期也不必事先告诉编译器,自动收走这些不再使用的数据。
缺点:由于要在运行时动态分配内存,存取速度较慢。
堆式分配(堆区)
满足条件:存放 new 出来的对象
详解: 用来存放 所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。
回收时机:不使用时将会由 Java 垃圾回收器来回收
优点:生存期也不必事先告诉编译器,自动收走这些不再使用的数据。
缺点:由于要在运行时动态分配内存,存取速度较慢。
堆内存分配策略
堆内存的空间划分
新生代(PSYoungGen)
Eden空间
From Survivor空间
To Survivor空间
老年代(ParOldGen)
正常情况---分配
对象优先在Eden分配
如果说Eden内存空间不足,就会发生Minor GC。
特殊情况---分配
大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象,比如很长的字符串和大型数组
长期存活的对象将进入老年代
对象年龄:默认15岁,可通过-XX:MaxTenuringThreshold调整.
动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保
新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。
栈式分配(栈区)
满足条件:存放 方法内的局部变量
详解: 用来存放 在方法体内定义的局部变量(一些基本类型的变量和对象的引用变量)
回收时机:方法执行完成后,方法栈中的局部变量会自动回收。
优点:存取速度快,存取速度比堆要快,仅次于直接位于CPU中的寄存器。
缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
加深理解的例子
A类内的局部变量都存在于栈中,包括基本数据类型a1和引用变量b1,b1指向的B对象实体存在于堆中。引用变量object存在于栈中,而object指向的对象实体存在于堆中,包括这个对象的所有成员变量a和b,而引用变量b指向的B类对象实体存在于堆中。
Java内存回收策略
垃圾判定算法
引用计数法
原理:在对象中添加一个引用计数器,如果对象被引用一次,此对象计数器就 +1,引用失效就-1,计数器为0表示对象不再引用,不再引用的对象可以被回收。
优点:实现简单,判断高效。
缺点: 1.需要额外的内存来计数; 2.运行期间需要维护计数器,带来额外的开销; 3.无法解决循环引用问题
例子: Object 1和Object 2其实都可以被回收,但是它们之间还有相互引用,所以它们各自的计数器都为1,则还是不会被回收。
可达性分析法
原理:从GC Roots的对象作为起点向下搜索,搜索走过的路径称为引用链,如果一个对象与GCRoots之间没有任何直接或间接的引用链相连,说明该对象与GCRoots之间不可达,不可达的对象可以被回收。
优点:可以解决循环引用问题
缺点:暂无
GCRoots理解
GCRoots是什么? GCRoots是一种特殊的对象,从字面意思看是垃圾回收的根,其实是JVM确定当前绝对不能被回收的根对象。
GCRoots如何工作? 垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程,触发STW。找到所有的GC Roots之后就可以从这些GC Roots向下搜寻,可达的对象就保留,不可达的对象就回收。
哪些对象可以作为GCRoots?
方法区:类静态属性引用的对象;
1、方法区静态属性引用的对象 全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。
方法区:常量引用的对象;
2、方法区常量池引用的对象 也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。
虚拟机栈(Jav方法):本地变量表中的对象
3、方法栈中栈帧本地变量表引用的对象 属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。
本地方法栈(Native方法):JNI中的对象
4、JNI本地方法栈中引用的对象 和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。
被同步锁持有的对象
5、被同步锁持有的对象 被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛。
对象是否可回收的依据:对象到GCRoots之间是否存在直接或间接的引用链,存在就可达,不存在就不可达。只有对象与GCRoots之间不可达才能被回收。 总结:可达性分析就是JVM首先枚举根节点,找到一些为了保证程序能正常运行所必须要存活的对象,然后以这些对象为根,根据引用关系开始向下搜寻,存在直接或间接引用链的对象就存活,不存在引用链的对象就回收。
垃圾回收算法
复制算法
原理:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:内存连续,没有内存碎片。使用简单,运行高效,
缺点:利用率只有50%,需要内存复制。
应用场景:CMS的新生代;出了G1之外的垃圾回收器的新生代
标记清除算法
原理:算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
优点:利用率100%,不需要内存复制。
缺点:内存空间不连续,有内存碎片。
应用场景:CMS的老年代
标记整理算法
标记整理算法(分3步): 1、标记不用的内存; 2、移动将存活的对象向一端; 3、清理短之外的内存; 总结: 标记整理 = 标记 + 移动 + 清理 标记清除 = 标记 + 清除
原理:首先标记出所有需要回收的对象,然后将所有存活的对象都向一端移动,最后清理掉端边界以外的内存。
优点:利用率100%,没有内存碎片。
缺点:需要内存复制,效率一般般。
应用场景:G1的新生代和老年代;Serial Old/Parallel Old的老年代
算法应用场景(垃圾回收器)
Serial/ParNew/Parallel Scavenge:新生代采用复制算法;
Serial Old/Parallel Old:老年代用标记整理算法。
CMS:新生代用复制算法,老年代用标记清除算法。
G1:新生代和老年代都用标记整理算法
面试题
1、哪些对象可以被回收? 只有与GCRoots不可达(对象与GCRoots之间没有引用链)的对象才能被回收(相关算法有引用计数法和可达性分析法) 2、什么时候回收? 触发垃圾回收机制的时候(1、内存不足自动触发GC回收,2、其他情况下的手动调用GC回收。) 3、怎么回收? 根据内存回收算法,通过垃圾回收器来回收(CMS、G1等)。
其实Java的垃圾回收机制是由JVM框架控制的内存自动回收不用我们管,但是为什么我们那么重视呢? 因为虽然JVM框架中会自动回收是应为框架已经设定好了满足什么条件才能回收,但是有泄漏的情况,我们要做的就是优化GCRoots
GC触发条件:空间不够了
新生代:触发Minor GC
老年代:触发Full GC