如果我是Android面试官四(JVM特别篇)

最近忙于重构一个模块,好长时间没有更新,今天终于有时间来写一篇。JVM知识作为android面试过程中不可缺少的一环,作为一个Android开发工程师,一定要有所了解。话不多说,下面开始。

一、java内存模型是什么?五个部分都是什么,有什么作用?

这里所指的java内存模型,实际上是Java虚拟机在执行java程序的过程中,把它所管理的内存划分为若干个不同的数据区域。通常包含以下五个部分:程序计数器、方法区、本地方法栈,虚拟机栈,堆。

1、程序计数器

程序计数器是一块较小的内存区域,它可以看成是当前线程执行字节码的指示器。在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。

在Java虚拟机执行多线程任务时,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

2、Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的时候会创建一个栈帧,用于存储局部变量表、操作数栈,动态链接,方法出口等信息。每一方法从调用直至执行完成,都对应着一个栈帧在虚拟机栈里入栈到出栈。

局部变量表存放了编译期可知的各种基本数据类型,对象引用类型,它不等于对象本身,可能是一个指向对象起始地址的引用指针。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对局部变量表区域规定了两种异常,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3、本地方法栈

本地方法栈和虚拟机栈作用非常相似,区别在于虚拟机栈是为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError。

4、Java堆

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。在Java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配内存。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC堆”。由于现在收集器都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代,再细致一点的有Eden空间,FromSurvivor空间,ToSurvivor空间等。从内存分配角度来看线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。划分的目的是为了更好地回收内存,或者更快地分配内存。

Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,就会抛出OutOfMemoryErro异常。

5、方法区

方法区与Java堆一样,是各个线程的共享内存区域,它用于存储已被虚拟机加载的类的信息,常量,静态变量,即时编译器编译后的代码等数据。

6、运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

二、JVM垃圾回收策略,如何判断对象、类需要被回收

1、引用计数器

很多教科书判断对象是否存活的算法是,给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1。当引用失效时,计数器就减1。任何时刻,计数器为0的对象就是不可能再被使用的。

引用计数器算法的实现简单,判定效率也很高。但是,主流Java虚拟机里没有选用引用计数器算法来管理内存,最主要的原因是它很难解决对象之间相互循环引用的问题。

2、可达性分析算法

在主流的商用程序语言的主流实现中,都是通过称可达性分析来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

三、四种垃圾收集算法

1、标记-清除算法

最基础的收集算法是“标记-清除算法”,如同它的名字一样,算法分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。此算法的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高。

另一个是空间问题,标记清除之后会产生大量的不连续的内存碎片。空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续空间而不得不提前触发另一次垃圾收集动作。

2、复制算法

为了解决效率问题,一种称为“复制”的收集算法出现了。它将可用的内存划分为大小相同的两块,每次只使用其中的一块,当这块内存用完了,就将还存活的对象复制到另外一块上面。然后再把已经使用过得内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只要移动堆顶指针,按顺序分配内存即可。实现简单,运行高效。只是代价为将内存缩小为原来的一半,未免有些高昂。

3、标记-整理算法

复制收集算法在对象存活较高时要进行较多的复制操作,效率会降低。更关键的是如果不想浪费50%的空间,就需要有额外的空间进行分配担保,所以在老年代一般不能直接用这种算法。

标记整理算法的标记过程与标记清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动。然后直接清理掉端边界以外的内存。

4、分代收集算法

当前商用虚拟机垃圾收集都采用分代收集算法。此算法根据对象存货周期的不同,将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法进行回收。在新生代,每次回收都有大量对象死去,因此选用复制算法。只需要进行少量对象复制。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,因此必须使用标记-清理或标记-整理算法来回收。

四、垃圾收集器

1、Serial收集器

Serial收集器是最基本,发展历史最悠久的收集器。它是一个单线程收集器,它在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”正是描述的这种情况。

2、ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,收集算法,Stop  The World,对象分配规则,回收策略等都与Serial收集器完全一样。ParNew收集器是许多虚拟机中首选的新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合工作。

3、Parallel Scavenge收集器

PS收集器也是一个新生代收集器,采用复制算法,与其他收集器不同的是。CMS等收集器关注尽可能缩短垃圾收集时用户的停顿时间。而PS收集器目的则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量= 运行用户代码时间、(运行用户代码时间 + 垃圾收集时间)。PS收集器也经常称为“吞吐量优先”收集器。虚拟机根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。这种调节方式称为GC自适应的调节策略。

4、Serial Old收集器

Serial Old是Serial的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

5、Parralel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本。使用多线程和标记整理算法。

6、CMS收集器

Concurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS是一款优秀的收集器,它的优点在于:并发收集,低停顿。

7、G1收集器

Garbage-First 收集器是当今收集器技术发展的最前沿成果之一。它的特点如下

并行与并发

分代收集

空间整合

可预测的停顿

五、内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化解决了两个问题:给对象分配内存以及回收分配给对象的内存。

1、对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden没有足够空间时,虚拟机将发起一次Minor GC。

2、大对象直接进入老年代

所谓的大对象,是指,需要大量连续内存空间的Java对象。

3、长期存活的对象将进入老年代

虚拟机给每个对象一个年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁。当它的年龄增加到一定程度,就会被晋升为老年代。

4、动态对象年龄判断

虚拟机并不是永远要求对象的年龄必须达到设定的阀值才晋升老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

5、空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一次Minor GC。如果小于,则会进行一次Full GC。

猜你喜欢

转载自blog.csdn.net/Kongou/article/details/81430336
今日推荐