前言
其实有很多Android开发者不明白,为什么我们需要去学习jvm,在我们实际的开发工作中哪些地方用到了这方面的知识,或者学完这些知识我们在哪些地方能用到。我相信这是困扰很多普通android开发者的一个问题。今天我将通过这篇文章带领大家去学习jvm,同时通过知识点的讲解来告诉大家主要学习jvm有什么用。
一、JVM的内存布局图
主要组成:
从上图我们能很清楚的看到,JVM主要包含两个子系统和两个组件
两个子系统:(1)类装载子系统。(2)执行引擎
两个组件:(1)运行时数据区。(2)本地接口
(1)类装载子系统:它的主要作用是根据全限定名类名(如:java.lang.object)来装载到运行时数据区中。
(2)执行引擎:执行classes中的指令
(3)运行时数据区域:这就是我们常说的JVM的内存(这里也是我们后面需要讲的重点部分)
(4)本地接口:与native libraries交互,是其他编程语言交互的接口。
二、内存模型的工作机制
如上图所示:
1、首先是利用开发工具编写得到Java源码,也就是我们的.java文件
2、再利用Java源码编译器,将java文件转变成.class 文件
3、最后通过类装载子系统讲.class文件,装载到运行时数据去中去。
所以接下来我们讲重点讲解我们的jvm运行时数据区。
三、JVM运行时数据区
JVM运行时数据区主要分为两个部分:
1、线程共享区
2、线程私有区
线程共享区分为:方法区、堆
线程私有区分为:虚拟机栈、本地方法栈、程序计数器
这里有一个要注意的点:JDK 1.8之后(包括1.8)将线程共享区中的方法区改成了元空间,并放入直接内存中
下面会逐一为大家讲解,每个区中的每部分都有神马作用,并在其中穿插讲解部分面试问题。
1、程序计数器
程序计数器主要有两个作用:
(1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码刘的控制。
(2)在多线程的情况下,程序计数器勇于记录当前线程执行的位置,从而当线程被切回来的时候,能够知道该线程上次运行到哪了。
这里给大家解释一下上面两个是什么意思,如下图:
如上图:当系统空出了1微妙,1微妙被分成很多个时间片段。这时候有两个线程,线程1和线程2会去争夺这个时间资源,如果第一个时间片被线程1抢占之后,在第一个时间片的时间内,线程1执行到了如果A点。这时候还没执行完。第二个时间片被线程2抢到了。这时候系统回去执行线程2的程序指令,当第三个时间片被线程1抢到之后,线程1不会从开始的地方执行,而是从A(即上次执行到的地方)去执行。所以这里就用到了程序计数器,相当于确认代码执行位置的指示器。上图的流程也被称为:时间片轮转机制。
这里要注意,程序计数器是在多线程的情况下去干事情的。
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2、虚拟机栈
在上面我们讲过,虚拟机栈这个是属于线程私有区,所以这里我们需要了解,每个线程所包含的内容,如下图:
从上图我们可以看出:
1、虚拟机栈是线程私有的。
2、在线程中对应的有几个方法,就有几个方法栈
3、每个方法栈帧中,都有对应的局部变量表,操作数栈,动态了解,以及方法出口
第一个线程私有的这个就不用解释了,第二个就很好的解释了为什么递归会出现OutOfMemoryError,因为在一个线程中如果循环的去执行方法,每个方法都会去虚拟机栈中开辟一定的空间,如果方法一直进栈而不出栈,如果当虚拟机栈中的空间无法在被申请(也就是满了)那么就会出现:OutOfMemoryError
第三个当我们执行一个方法的时候,如下:
左侧java代码,右侧为字节码。我们在分析虚拟机工作的时候,我们做好还是从右边字节码分析。
首先在操作数栈中开辟一个空间存放常量1(a在操作数栈中入栈),让后将常量1 放入到局部变量表中(a在操作舒展中出栈,在局部变量表中入栈),常量2 如常量1一样。
左边第55行开始:从刚刚入栈到局部变量表的常量1和常量2分别入栈到操作数栈,然后做ADD加法;开辟操作舒栈控件存放计算的结果3,再将计算结果3放入到局部变量表中入栈。最后通过方法出口,讲结果3从局部变量表中出站。
从上面的学习中我们要注意:在虚拟机栈中一般会出现两种错误:StackOverFlowError、OutOfMemoryError,其中OutOfMemoryError这个上面已经讲过了。StackOverFlowError出现主要是,Java 虚拟机栈的内存大小不允许动态扩展,当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
3、本地方法栈
其实是在讲虚拟机栈的时候,第二张图已经讲本地方法栈讲过了。其实本地房发展和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码,如上图)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
4、线程私有---堆
上面主要讲了Java虚拟机运行时区域中的线程私有部分,下面我们将展开对线程公共部分的讲解,也就是我们的堆和方法区,以及垃圾回收机制,性能调优等。
其实在java虚拟机中堆才是虚拟机所管理的内存中最大一块,并且线程共享。这个内存区域主要是用来存放对象实例(注意这里是实例,不是引用)几乎所有的对象实例以及数组都是在这里分配的内存。
堆也是垃圾回收器管理的主要区域,又称为GC 堆(Garbage Collected Heap)。
从上图可以看出,堆的内存划分,主要分为:年轻代和老年代(这里还有个永久代没有画出)。很多小伙伴会疑问为什么要这么划分,主要是java虚拟机根据对象存活的周期不同来划分的。因为堆内存是垃圾回收最频繁的一块区域,如果不进行区域划分,那么新的对象和生命周期长的对象都放在一起,那么堆内存每次进行频繁的垃圾回收的时候,都需要遍历所有的对象,那么这个将会耗费大量的时间,会影响我们的GC效率。总结一句分区是为了提高GC效率。
1、年轻代
年轻代也是mirror gc 区域,年轻代主要分为:Eden,Survivor(From,To)
新生的对象有限放在新生代中(这里指得是新生的小对象,如果是大对象会直接放入老年代),新生的对象生命周期比较短暂,存活率很低,常规的应用在进行一次垃圾回收的时候,会回收70%~95%的空间。
1.1 年轻代MirrorGC流程:
在了解年轻代的MirrorGC流程之前,我们先来了解对象的组成部分,为什么要先了解这部分,因为我们MirrorGc在回收的过程中和对象是息息相关的。
从上图可以看出,一个普通对象主要包括以下部分:
对象头:对象头主要由Markword,和class pointer组成,markword 主要是用来存放对象的hashCode,锁信息或者分代年龄GC等标志信息。
实例数据:存放类的属性数据信息,如果有数组实例则还需要包括数组的长度,以及4自己的对齐。
对齐:由于虚拟机要求对象的其实地址必须是8字节的整数倍。(注意这个对齐数据不是必须存在的,如果你的数据刚刚好是8字节的整数倍,那就不需要对齐操作了)
补充一点为什么要这个对齐操作:为了提高对象的访问效率。
GC流程:对象(这里指的是小对象,上面说了大对象直接去老年代了)new出来首先先去Eden区,当Eden区放满之后,系统会开启GC进程(这里指的是Mirror GC)去分析年轻代(Eden、From、To),回收可回收的对象,不能回收的移动到To 区,同时在对象头中标记(上面说了对象头的markword 存放了GC标记信息。)如果下次Eden区又放满了,同样GC扫描,能回收的回收,不能回收的则放入From区,以此类推,所以总是(Eden +To)和 (Eden+From) 来回数据改变,同时改变对象头。当对象头中的信息到达6岁(并发的伐值,并行是15岁,因为对象头age只占4个字节),则到老年代中去。如果老年代中的数据也满了的话,那么系统会启动GC进程(这里是Full GC)。Full GC 会去扫描整个堆内存区,把能回收的都回收掉,这里注意CMS收集器在运行的时候,会消耗时间并停止所有线程,所以要尽可能的减少Full GC,所以调优主要就是调其它区的大小,减少GC的可能。
注意:堆区用来存对象,栈区用来存函数进行中的变量,栈中存的对象,实际上是对对象的引用。
从上面我们会延伸两个问题:1、什么是CMS收集器 ?2、GC的回收策略是什么?下面我们一一展开。
5、GC如何回收
1)标记清除算法:
该算法分为两个阶段,标记、和清除阶段:首先比较出所有需要回收的对象打上标记,在标记完成后统一对有标记过进行回收,这个是最基础的算法,如下图:
从上图我们能看出:整理之后会产生大量的内存碎片。
还有一个问题就是:效率问题。这种清除方式效率不高,为了解决效率问题推出了复制清除法。
2)复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
3)标记整理法
跟标记清除法一样,只是在清楚的时候不一样,标记整理法是为了解决标记清除法产生内存碎片而生的。对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4)分代收集法
虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
6、垃圾收集器
关于垃圾收集器,这里我只讲一个CMS 收集器,其他收集器有感兴趣的小伙伴们可以自行去了解一下。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现
1)CMS 收集器:
CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,它非常符合在注重用户体验的应用上使用,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
从上面的一句话我们可以看出他的几个特点:1、标记清除法,2、并发收集器, 3、最短GC回收停顿时间(低停顿)
具体收集分四步:
初始标记(暂停所有其他的线程,记录下直接和root相连的对象)
并发标记(同时开启GC和用户线程,用一个闭包去记录可达对象。)
重新标记(重新标记或者修正并发期间用户继续运行导致的标记变动的那一部分对象)
并发清除 (开启用户线程,同时GC线程开始对为标记的区域回收)
因为上面是采用的是标记清除算法,所以也具有标记清除算法的缺点:会产生打量空间碎片、同时对CPU资源敏感,无法处理浮动垃圾。
总结
其实关于JVM的内容还有很多,就比如:上面没有提到的,是如何去判断这个对象是可以回收的还是不可以回收(可达性分析法,引用计数法,强引用,弱应用等),如何进行JVM调优等一些列这样的问题。这里不做深入讨论,感兴趣的小伙伴留言,我到时候再专门出一期关于GC 是如何去判断对象是可回收还是不可回收的问题,和JVM调优。希望通过本次学习能为大家在了解JVM打开一个窗口,从这个窗口对应的再去深入了解java虚拟机。