面试中问到“内存模型”,一般是考察Java内存结构和GC,而不是Happens-Before等更深刻、细致的内容。内存模型是考察coder对一门语言的理解能力,从而进一步延伸到对JVM优化,和平时学习的深度上,是Java面试中最重要的一部分。这里整理了内存结构和GC的知识点,Happens-Before模型预计在之后学习过JVM过再来整理。html
若是把内存模型看作一个数据结构,那么面试中考察的重点分为内存结构和GC,不过有时候会单独问到GC,另外大问题分解为小问题也方便理解。c++
JVM的内存结构大概分为:git
在Java的内存结构中,咱们重点关注的是堆和方法区。github
堆的做用是存放对象实例和数组。从结构上来分,能够分为新生代和老生代。而新生代又能够分为Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1)。 全部新生成的对象首先都是放在年轻代的。须要注意,Survivor的两个区是对称的,没前后关系,因此同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。并且,Survivor区总有一个是空的。面试
-Xms设置堆的最小空间大小。-Xmx设置堆的最大空间大小。-XX:NewSize设置新生代最小空间大小。-XX:MaxNewSize设置新生代最小空间大小。 objective-c
此区域是垃圾回收的主要操做区域。算法
若是在堆中没有内存完成实例分配,而且堆也没法再扩展时,将会抛出OutOfMemoryError 异常。 数组
方法区(Method Area)与Java 堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作Non-Heap(非堆),目的应该是与Java 堆区分开来。bash
不少人愿意把方法区称为“永久代”(Permanent Generation),本质上二者并不等价,仅仅是由于HotSpot虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其余虚拟机(如BEA JRockit、IBM J9 等)来讲是不存在永久代的概念的。在Java8中永生代完全消失了。 数据结构
-XX:PermSize 设置最小空间 -XX:MaxPermSize 设置最大空间。
对此区域会涉及可是不多进行垃圾回收。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,通常来讲这个区域的回收“成绩”比较难以使人满意。
根据Java 虚拟机规范的规定, 当方法区没法知足内存分配需求时,将抛出OutOfMemoryError。
每一个线程会有一个私有的栈。每一个线程中方法的调用又会在本栈中建立一个栈帧。在方法栈中会存放编译期可知的各类基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象自己。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法须要在帧中分配多大的局部变量空间是彻底肯定的,在方法运行期间不会改变局部变量表的大小。
-Xss控制每一个线程栈的大小。
在Java 虚拟机规范中,对这个区域规定了两种异常情况:
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的做用是很是类似的,其
区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则
是为虚拟机使用到的Native 方法服务。
在Sun JDK中本地方法栈和方法栈是同一个,所以也能够用-Xss控制每一个线程的大小。
与虚拟机栈同样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError
异常。
它的做用能够看作是当前线程所执行的字节码的行号指示器。
此内存区域是惟一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 状况的区域。
简言之,Java程序内存主要(这里强调主要二字)分两部分,堆和非堆。你们通常new的对象和数组都是在堆中的,而GC主要回收的内存也是这块堆内存。
既然重点是堆内存,咱们就再看看堆的内存模型。
堆内存由垃圾回收器的自动内存管理系统回收。
堆内存分为两大部分:新生代和老年代。比例为1:2。
老年代主要存放应用程序中生命周期长的存活对象。
新生代又分为三个部分:一个Eden区和两个Survivor区,比例为8:1:1。
Eden区存放新生的对象。
Survivor存放每次垃圾回收后存活的对象。
关注这几个问题:
这几个问题都是垃圾回收机制所采用的算法决定的。因此问题转化为,是何种算法?为何要采用此种算法?
在进行垃圾回收以前,咱们须要清除一个问题——什么样的对象是垃圾(无用对象),须要被回收?
目前最多见的有两种算法用来断定一个对象是否为垃圾。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。
优势是简单,高效,如今的objective-c用的就是这种算法。
缺点是很难处理循环引用,好比图中相互引用的两个对象则没法释放。
这个缺点很致命,有人可能会问,那objective-c不是用的好好的吗?我我的并无以为objective-c好好的处理了这个循环引用问题,它实际上是把这个问题抛给了开发者。
为了解决上面的循环引用问题,Java采用了一种新的算法:可达性分析算法。
从GC Roots(每种具体实现对GC Roots有不一样的定义)做为起点,向下搜索它们引用的对象,能够生成一棵引用树,树的节点视为可达对象,反之视为不可达。
OK,即便循环引用了,只要没有被GC Roots引用了依然会被回收,完美!
可是,这个GC Roots的定义就要考究了,Java语言定义了以下GC Roots对象:
有了上面的垃圾对象的断定,咱们还要考虑一个问题,请你们作好内心准备,那就是Stop The World。
由于垃圾回收的时候,须要整个的引用状态保持不变,不然断定是断定垃圾,等我稍后回收的时候它又被引用了,这就全乱套了。因此,GC的时候,其余全部的程序执行处于暂停状态,卡住了。
幸运的是,这个卡顿是很是短(尤为是新生代),对程序的影响微乎其微 (关于其余GC好比并发GC之类的,在此不讨论)。
因此GC的卡顿问题由此而来,也是情有可原,暂时无可避免。
已经知道哪些是垃圾对象了,怎么回收呢?
目前主流有如下几种算法,目前JVM采用的是分代回收算法,而分代回收算法正是从这几种算法发展而来。
标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出全部须要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
优势:简单实现。
缺点:容易产生内存碎片(碎片太多可能会致使后续过程当中须要为大对象分配空间时没法找到足够的空间而提早触发新的一次垃圾收集动做)。
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
优势:实现简单,运行高效且不容易产生内存碎片。
缺点:对内存空间的使用作出了高昂的代价,由于可以使用的内存缩减到原来的一半。
从算法原理咱们能够看出,复制算法算法的效率跟存活对象的数目多少有很大的关系,若是存活对象不少,那么复制算法算法的效率将会大大下降。
该算法标记阶段和Mark-Sweep同样,可是在完成标记以后,它不是直接清理可回收对象,而是将存活对象都向一端移动,而后清理掉端边界之外的内存。
优势:实现简单,不容易产生内存碎片,内存使用高效。
缺点:效率很是低。
因此,特别适用于存活对象多,回收对象少的状况下。
以上几种算法都有各自的优势和缺点,适用于不一样的内存情景。而分代回收算法根据Java的语言特性,将复制算法和标记整理算法的的特色相结合,针对不一样的内存情景使用不一样的回收算法。
这里重复一下两种老算法的适用场景:
复制算法:适用于存活对象不多。回收对象多
标记整理算法: 适用用于存活对象多,回收对象少
两种算法恰好互补,不一样类型的对象生命周期决定了更适合采用哪一种算法。
因而,咱们根据对象存活的生命周期将内存划分为若干个不一样的区域。通常状况下将堆区划分为老年代(Old Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集时只有少许对象须要被回收,使用标记整理算法,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,复制算法,那么就能够根据不一样代的特色采起最适合的收集算法。
如今回头去看堆内存为何要划分新生代和老年代,是否是以为如此的清晰和天然了?
具体来看:
这就是分代回收算法。
对于这个算法,我相信不少人仍是有疑问的,咱们来各个击破,说清楚了就很简单。
这里涉及到一个新生代和老年代的存活周期的问题,好比一个对象在新生代经历15次(仅供参考)GC,就能够移到老年代了。问题来了,当咱们第一次GC的时候,咱们能够把Eden区的存活对象放到Survivor A空间,可是第二次GC的时候,Survivor A空间的存活对象也须要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。
因此,这里就须要两块Survivor空间来回倒腾。
新建立的对象都是放在Eden空间,这是很频繁的,尤为是大量的局部变量产生的临时对象,这些对象绝大部分都应该立刻被回收,能存活下来被转移到survivor空间的每每很少。因此,设置较大的Eden空间和较小的Survivor空间是合理的,大大提升了内存的使用率,缓解了Copying算法的缺点。
我看8:1:1就挺好的,固然这个比例是能够调整的,包括上面的新生代和老年代的1:2的比例也是能够调整的。
新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。
这里原本简单的Copying算法被划分为三部分后不少朋友一时理解不了,也确实很差描述,下面我来演示一下Eden空间和两块Survivor空间的工做流程。
如今假定有新生代Eden,Survivor A, Survivor B三块空间和老生代Old一块空间。
// 分配了一个又一个对象
放到Eden区
// 很差,Eden区满了,只能GC(新生代GC:Minor GC)了
把Eden区的存活对象copy到Survivor A区,而后清空Eden区(原本Survivor B区也须要清空的,不过原本就是空的)
// 又分配了一个又一个对象
放到Eden区
// 很差,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor A区的存活对象copy到Survivor B区,而后清空Eden区和Survivor A区
// 又分配了一个又一个对象
放到Eden区
// 很差,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor B区的存活对象copy到Survivor A区,而后清空Eden区和Survivor B区
// ...
// 有的对象来回在Survivor A区或者B区呆了好比15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程当中忽然发现:
// 很差,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)
Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...复制代码
了解这些是为了解决实际问题,Java虚拟机会把每次触发GC的信息打印出来来帮助咱们分析问题,因此掌握触发GC的类型是分析日志的基础。
GC_FOR_MALLOC
: 表示是在堆上分配对象时内存不足触发的GC。GC_CONCURRENT
: 当咱们应用程序的堆内存达到必定量,或者能够理解为快要满的时候,系统会自动触发GC操做来释放内存。GC_EXPLICIT
: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。GC_BEFORE_OOM
: 表示是在准备抛OOM异常以前进行的最后努力而触发的GC。参考:
本文连接:Java内存模型
做者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,可是必须保留本文的署名及连接。