推荐 原创 视频 Java开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发
Lambda在线 > Java面试达人 > 面试必备 | 理解 jvm

面试必备 | 理解 jvm

Java面试达人 2018-03-30

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间。有的区域随着虚拟机进程的启动就存在了, 有的区域则是依赖用户线程。根据《Java虚拟机规范(第二版)》,Java 虚拟机所管理的内存包含以下的几个区域。

运行时数据区

由上图可以看出,在运行时数据区中:虚拟机栈、本地方法栈、程序计数器属于线程隔离的数据区,是单个线程私有的,它们的生命周期与线程相同;而方法区和堆属于所有线程共享的数据区,是所有线程共享的。


程序计数器

程序计数是最小的一块内存区域,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。在多线程环境下,当某个线程失去处理器执行权时,需要记录该线程被切换出去时所执行的程序位置。从而方便该线程被切换回来(重新被处理器处理)时能恢复到当初的执行位置,因此每个线程都需要有一个独立的程序计数器。各个线程的程序计数器互不影响,并且独立存储。

  1. 如果正在执行的是 Native 方法,这个计数器的值则为空(Undefined);

  2. 此内存区域是唯一一个在 java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。


Java 虚拟机栈

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

在 Java 虚拟机规范中,对这个区域规定了两种异常状况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;

  2. 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。


本地方法栈

本地方法栈,虚拟机栈所发挥的作用是非常相似的,它们的区别不过是虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。


Java 堆

也叫做java 堆(Java Heap)、GC 堆(Garbage Collected Heap)是 java 虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在 JVM 启动时创建。该内存区域存放了对象实例及数组(所有 new 的对象)。其大小通过 -Xms(最小值)和 -Xmx(最大值)参数设置,-Xms 为 JVM启动时申请的最小内存,默认为操作系统物理内存的 1/64 但小于 1G,-Xmx 为 JVM 可申请的最大内存,默认为物理内存的 1/4 但小于 1G,默认当空余堆内存小于 40% 时,JVM 会增大 Heap 到 -Xmx指定的大小,可通过 -XX:MinHeapFreeRation= 来指定这个比列;当空余堆内存大于 70% 时,JVM 会减小 heap 的大小到 -Xms 指定的大小,可通过 XX:MaxHeapFreeRation= 来指定这个比列,对于运行系统,为避免在运行时频繁调整 Heap 的大小,通常 -Xms 与 -Xmx 的值设成一样。

Java 堆是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器基本是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代 GC(Minor GC) 任然存活的对象。

  1. 新生代: 程序新创建的对象都是从新生代分配内存,新生代由 Eden Space 和两块相同大小的Survivor Space(通常又称 S0 和 S1 或 From 和 To)构成,在 Sun HotSpot 虚拟机中 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80% + 10%),可通过 -Xmn 参数来指定新生代的大小,也可以通过 -XX:SurvivorRation 来调整 Eden Space 及 Survivor Space 的大小。

  2. 老年代: 用于存放经过多次新生代 GC 任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:1、大对象,可通过启动参数设置 -XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。2、大的数组对象,切数组中无引用外部对象。 老年代所占的内存大小为 -Xmx 对应的值减去 -Xmn 对应的值。

面试必备 | 理解 jvm

  • Young Generation 即图中的 Eden + From Space + To Space

  • Eden 存放新生的对象

  • Survivor Space 有两个,存放每次垃圾回收后存活的对象

  • Old Generation Tenured Generation 即图中的 Old Space 主要存放应用程序中生命周期长的存活对象。


方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于内存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆)。对于 HotSpot 虚拟机,也把方法区成为“永久代”(Permanent Generation),默认最小值为16MB,最大值为64MB,可以通过 -XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。在 JDK 1.7 以后已经逐步改为采用 Native Memory 来实现方法区。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

运行时常量池

运行时常量池是方法区的一部分。Class 文件中处了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。


垃圾回收机制

在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。顾名思义,垃圾回收就是释放垃圾占用的空间,那么在Java中,什么样的对象会被认定为“垃圾”?那么当一些对象被确定为垃圾之后,采用什么样的策略来进行回收(释放空间)?在目前的商业虚拟机中,有哪些典型的垃圾收集器?下面我们就来逐一探讨这些问题。


一.如何确定某个对象是“垃圾”?

二.典型的垃圾收集算法

三.典型的垃圾收集器

四.什么时候进行垃圾回收

一.如何确定某个对象是“垃圾”?

在这一小节我们先了解一个最基本的问题:如果确定某个对象是“垃圾”?既然垃圾收集器的任务是回收垃圾对象所占的空间供新的对象使用,那么垃圾收集器如何确定某个对象是“垃圾”?—即通过什么方法判断一个对象可以被回收了。

在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。

这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。看下面这段代码:

面试必备 | 理解 jvm

最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。

为了解决这个问题,在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

二.典型的垃圾收集算法

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。

1.Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

面试必备 | 理解 jvm

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2.Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

面试必备 | 理解 jvm

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

3.Mark-Compact(标记-整理)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

面试必备 | 理解 jvm

4.Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

三.典型的垃圾收集器

垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面介绍一下HotSpot(JDK 7)虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。

1.Serial/Serial Old

Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

2.ParNew

ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

3.Parallel Scavenge

Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

4.Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

5.CMS

CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

6.G1

G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

四.什么时候进行垃圾回收?

Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。

需要注意的是:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身,很多人来我公司面试时,我都会问这个问题的,70%以上的人回答的含义是回收对象,实际上这是不正确的。

System.gc()

Runtime.getRuntime().gc()  

上面的方法调用时用于显式通知JVM可以进行一次垃圾回收,但真正垃圾回收机制具体在什么时间点开始发生动作这同样是不可预料的,这和抢占式的线程在发生作用时的原理一样。


最后,由于时间原因,简单介绍内存溢出


如何堆溢出:

不断的创建对象,并且保证GC Roots到该对象有可达路径,(不被回收掉)

如:创建一个list,List《Test》list = new ArrayList《Test》,

循环add,list.add(new Test())  ; 


如何栈溢出:

递归。使用局部变量,并循环调用。

或不停的创建线程,也会造成栈溢出。



@

运行常量池溢出:

用String.intern()



版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《面试必备 | 理解 jvm》的版权归原作者「java面试达人」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

关注java面试达人微信公众号

java面试达人微信公众号:gh_4d742b509be4

java面试达人

手机扫描上方二维码即可关注java面试达人微信公众号

java面试达人最新文章

精品公众号随机推荐

下一篇 >>

C语言环境简介