这是一篇有关JVM内存管理的文章。这里将会简单的分析一下Java如何使用从物理内存上申请下来的内存,以及如何来划分它们,后面还会介绍JVM的核心技术:如何分配和回收内存。java
要理解JVM的内存管理策略,首先就要熟悉Java的运行时数据区,如上图所示,在执行Java程序的时候,虚拟机会把它所管理的内存划分为多个不一样的数据区,称为运行时数据区。在程序执行过程当中对内存的分配、垃圾的回收都在运行时数据区中进行。对于Java程序员来讲,其中最重要的就是堆区和JVM栈区了。注意图中的图形面积比例并不表明实际的内存比例。程序员
下面来简单的讲一下图中的区块。算法
方法区:存储虚拟机运行时加载的类信息、常量、静态变量和即时编译的代码,所以能够把这一部分考虑为一个保存相对来讲数据较为固定的部分,常量和静态变量在编译时就肯定下来进入这部份内存,运行时类信息会直接加载到这部份内存,因此都是相对较早期进入内存的。编程
- **运行时常量池**:在JVM规范中是这样定义运行时常量池这个数据结构的:Runtime Constant Pool表明运行时每一个class文件的常量表。它包含几种常量:编译期的数字常量、方法和域的引用(在运行时解析)。它的功能相似于传统编程语言的符号表,尽管它包含的数据比典型的符号表要丰富得多。每一个Runtime Constant Pool都是在JVM的Method area中分配的,每一个Class或者Interface的Constant Pool都是在JVM建立class或接口时建立的。它是**属于方法区**的一部分,因此它的存储也受方法区的规范约束,若是常量池没法分配,一样会抛出OutOfMemoryError。
堆区:是JVM所管理的内存中最大的一块。主要用于存放对象实例,每个存储在堆中的Java对象都会是这个对象的类的一个副本,它会复制包括继承自它父类的全部非静态属性。而所谓的垃圾回收也主要是在堆区进行。 根据Java虚拟机规范的规定,Java堆能够处于物理上不连续的内存空间中,只要逻辑是上连续的便可,就像咱们的磁盘空间同样。在实现上,既能够实现成固定大小的,也能够是可扩展的:数组
- 若是是固定大小的,那么堆的大小在JVM启动时就一次向操做系统申请完成,旦分配完成,堆的大小就将固定,不能在内存不够时再向操做系统从新申请,同时当内存空闲时也不能将多余的空间交还给操做系统。 - 若是是可扩展的,则经过 -Xmx和 -Xms两个选项来控制大小,Xmx来表示堆的最大大小,Xms表示初始大小。
JVM栈区:则主要存放一些对象的引用和编译期可知的基本数据类型,这个区域是线程私有的,即每一个线程都有本身的栈。在Java虚拟机规范中,对这个区域规定了两种异常状况:缓存
StackOverflowError
异常OutOfMemoryError
异常StackOverflowError
和OutOfMemoryError
异常。一般咱们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;而经过new关键字和构造器建立的对象放在堆空间;程序中的字面量(literal)如直接书写的100、“hello”和常量都是放在静态存储区中。栈空间操做最快可是也很小,一般大量的对象都是放在堆空间,整个内存包括硬盘上的虚拟内存均可以被当成堆空间来使用。安全
String str = new String(“hello”);
在分析JVM内存分配策略以前,咱们先介绍一下一般状况下操做系统都是采用哪些策略来分配内存的。数据结构
在操做系统中,将内存分配策略分为三种,分别是:编程语言
静态内存分配 是指在程序编译时锯能肯定每一个数据在运行时的存储空间需求,所以在编译时就能够给它们分配固定的内存空间。这种分配策略不容许在程序代码中有可变数据结构(如可变数组)的存在,也不容许有嵌套或者递归的结构出现,由于它们都会致使编译程序没法计算机准确的存储空间需求。函数
栈内存分配 也可称为动态存储分配,是由一个相似于堆栈的运行栈来实现的。和静态内存分配相反,在栈式内存方案执行宏,程序对数据区的需求在编译时是彻底无知的,只有运行时才能知道,可是规定在运行中进入一个程序模块时,必须知道该程序模块所需数据区大小才能为其分配内存。和咱们所数值的数据结构中的栈同样,栈式内存分配按照先进后出的原则进行分配。
堆内存分配 当程序真正运行到相应代码时才会知道空间大小。
JVM内存分配主要基于两种:堆和栈。
先来讲说 栈 。
Java栈的分配是和线程绑定在一块儿的,当咱们建立一个线程时,很显然,JVM就会为这个线程建立一个新的Java栈,一个线程的方法的调用和返回对应这个Java栈的压栈和出栈。当线程激活一个Java方法时,JVM就会在线程的Java栈里新压入一个帧,这个帧天然成了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其余数据。
栈中主要存放一些基本类型的变量数据和对象句柄(引用)。存取速度比堆要快,仅次于寄存器,栈数据能够共享。缺点是,存在栈中的数据大小与生存期必须是肯定的,这也致使缺少了其灵活性。
Java的 堆 是一个运行时数据区,它们不须要程序代码来显示地释放。堆是由垃圾回收来负责的,堆的优点是能够动态地分配内存大小,生存期也没必要事先告诉编译器,由于它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些再也不使用的数据。但缺点是,因为要运行时动态分配内存,存取速度慢。
从堆和栈的功能和做用通俗地比较,堆主要用来存放对象,栈主要用来执行程序,这种不一样主要由堆和栈的特色决定的。
在编程中,如C/C++,全部的方法调用是经过栈进行的,全部的局部变量、形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈向上用就行,就好像工厂中的传送带同样,栈指针会自动指引你到放东西的位置,你所要作的只是把东西放下来就行。在退出函数时,修改栈指针就能够把栈中的内润销毁。这样的模式速度最快,固然要用来运行程序了。须要注意的是,在分配时,如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就是说上虽然分配是在程序运行中进行的,可是分配的大小是肯定的、不变的,而这个“大小多少”是在编译时肯定的,而不是在运行时。
堆在应用程序运行时请求操做系统给本身分配内存,因为操做系统管理内存分配,因此在分配和销毁时都要占用时间,所以用堆的效率很是低。可是堆的优势在于,编译器没必要知道从堆里分配多少存储空间,也没必要知道存储的数据要在堆里停留多长时间。所以,用堆保存数据时会获得更大的灵活性,事实上,因为面向对象的多态性,堆内存分配是必不可少的,由于多态变量所需的存储空间只有在运行时建立了对象以后才能肯定。在C++中,要求建立一个对象时,只需用new命令编制相关命令便可。执行这些代码时,会在堆里自动进行数据的保存。固然,为达到这种灵活性,必然会付出必定的代价——在堆里分配存储空间会花掉更长的时间。
即须要回收的对象。做为编写程序的人,是能够作出“这个对象已经再也不须要了”这样的判断,但计算机是作不到的。所以,若是程序(经过某个变量等等)可能会直接或间接地引用一个对象,那么这个对象就被视为“存活”;与之相反,已经引用不到的对象被视为“死亡”。将这些“死亡”对象找出来,而后做为垃圾进行回收,这就是GC的本质。
即判断对象是否可被引用的起始点。至于哪里才是根,不一样的语言和编译器都有不一样的规定,但基本上是将变量和运行栈空间做为根。各位确定会好奇根对象集合中都是些什么,下面就来简单的讲一讲:
在连续剩余空间中分配内存。用一个指针指向内存已用区和空闲区的分界点,须要分配新的内存时候,只须要将指针向空闲区移动相应的距离便可。
在不规整的剩余空间中分配内存。若是剩余内存是不规整的,就须要用一个列表记录下哪些内存块是可用的,当须要分配内存的时候就须要在这个列表中查找,找到一个足够大的空间进行分配,而后在更新这个列表。
指针碰撞的分配方式明显要优于空闲列表的方式,可是使用哪一种方式取决于堆内存是否规整,而堆内存是否规整则由使用的垃圾收集算法决定。若是堆内存是规整的,则采用指针碰撞的方式分配内存,而若是堆是不规整的,就会采用空闲列表的方式。
要对对象进行回收,首先须要找到哪些对象是垃圾,须要回收。有两种方法能够找到须要回收的对象,第一种叫作引用计数法。
具体方法就是给对象添加一个引用计数器,计数器的值表明着这个对象被引用的次数,当计数器的值为0的时候,就表明没有引用指向这个对象,那么这个对象就是不可用的,因此就能够对它进行回收。可是有一个问题就是当对象之间循环引用时,好比这样:
public class Main { public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.object = object2; object2.object = object1; //最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问, //可是因为它们互相引用对方,致使它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。 object1 = null; object2 = null; } } class MyObject{ public Object object = null; }
其中每一个对象的引用计数器的值都不为0,可是这些对象又是做为一个孤立的总体在内存中存在,其余的对象不持有这些对象的引用,这种状况下这些对象就没法被回收,这也是主流的Java虚拟机没有选用这种方法的缘由。
另外一种方法就是把堆中的对象和对象之间的引用分别看做有向图的顶点和有向边——即可达性分析法。这样只须要从一些顶点开始,对有向图中的每一个顶点进行可达性分析(深度优先遍历是有向图可达性算法的基础),这样就能够把不可达的对象找出来,这些不可达的对象还要再进行一次筛选,由于若是对象须要执行finalize()方法,那么它彻底能够在finalize()方法中让本身变的可达。这个方法解决了对象之间循环引用的问题。上面提到了“从一些对象开始”进行可达性分析,这些起始对象被称为GC Roots,能够做为GC Roots的对象有:
上文中提到的引用均是强引用,Java中还存在其余三种引用,分别是,软引用、弱引用和虚引用,当系统即将发生内存溢出时,才会对软引用所引用的对象进行回收;而被弱引用所引用的对象会在下一次触发GC时被回收;虚引用则仅仅是为了在对象被回收时可以收到系统通知。
即便在可达性分析算法中不可达的对象,也并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相链接的引用链。
Finalize()
方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,若是对象要在finalize()中成功拯救本身————只要从新与引用链上的任何的一个对象创建关联便可,譬如把本身赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。若是对象这时候还没逃脱,那基本上它就真的被回收了。
/** * 此代码演示了两点 * 一、对象能够在被GC时自我拯救 * 二、这种自救的机会只有一次,由于一个对象的finalize()方法最多只能被系统自动调用一次。 */ public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, I am still alive"); } protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscapeGC(); //对象第一次成功拯救本身 SAVE_HOOK = null; System.gc(); //由于finalize方法优先级很低,全部暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no ,I am dead QAQ!"); } //----------------------- //以上代码与上面的彻底相同,但此次自救却失败了!!! SAVE_HOOK = null; System.gc(); //由于finalize方法优先级很低,全部暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no ,I am dead QAQ!"); } } }
最后想说的是:请不要使用finalize()方法,使用try-finalize能够作的更好。这是一个历史遗留的问题——当年为了让C/C++程序员更好的接受它而作出的妥协。
好了,咱们找到了垃圾。来谈谈如何处理这些垃圾吧。
标记清除(Mark and Sweep)是最先开发出的GC算法(1960年)。它的原理很是简单,首先从根开始将可能被引用的对象用递归的方式进行标记,而后将没有标记到的对象做为垃圾进行回收。
经过可达性分析算法找到能够回收的对象后,要对这些对象进行标记,表明它能够被回收了。标记完成以后就统一回收全部被标记的对象。这就完成了回收,可是这种方式会产生大量的内存碎片,就致使了可用内存不规整,因而分配新的内存时就须要采用空闲列表的方法,若是没有找到足够大的空间,那么就要提早触发下一次垃圾收集。
做为标记清除的变形,还有一种叫作标记整理(Mark and Compact)的算法。
标记的过程和标记-清除算法同样,可是标记完成以后,让全部存活的对象都向堆内存的一端移动,最后直接清除掉边界之外的内存。这样对内存进行回收以后,内存是规整的,因而可使用指针碰撞的方式分配新的内存。
“标记”系列的算法有一个缺点,就是在分配了大量对象,而且其中只有一小部分存活的状况下,所消耗的时间会大大超过必要的值,这是由于在清除阶段还须要对大量死亡对象进行扫描。复制收集(Copy and Collection)则试图克服这一缺点。在这种算法中,会将从根开始被引用的对象复制到另外的空间中,而后,再将复制的对象所可以引用的对象用递归的方式不断复制下去。
经过图2咱们能够发现,复制收集方式中,只存在至关于标记清除方式中的标记阶段。因为清除阶段中须要对现存的全部对象进行扫描,在存在大量对象,且其中大部分都即将死亡的状况下,所有扫描一遍的开销实在是不小。而在复制收集方式中,就不存在这样的开销。
可是,和标记相比,将对象复制一份所须要的开销则比较大,所以在“存活”对象比例较高的状况下,反而会比较不利。这种算法的另外一个好处是它具备局部性(Lo-cality)。在复制收集过程当中,会按照对象被引用的顺序将对象复制到新空间中。因而,关系较近的对象被放在距离较近的内存空间中的可能性会提升,这被称为局部性。局部性高的状况下,内存缓存会更容易有效运做,程序的运行性能也可以获得提升。
上文提到了几种GC算法,可是各自的各自的优势,必须放到适合的场景内才能发挥最大的效率。
在JVM堆里分有两部分:新生代(young generate)和老年代(old generation)。
在新生代中长期存活的对象会逐渐向老年代过渡,新生代中的对象每经历一次GC,年龄就增长一岁,当年龄超过必定值时,就会被移动到老年代。
大部分的新建立对象分配在新生代。由于大部分对象很快就会变得不可达,因此它们被分配在新生代,而后消失再也不。当对象重新生代移除时,咱们称之为"Minor GC"。新生代使用的是复制收集算法。
新生代划分为三个部分:分别为Eden、Survivor from、Survivor to,大小比例为8:1:1(为了防止复制收集算法的浪费内存过大)。每次只使用Eden和其中的一块Survivor,回收时将存活的对象复制到另外一块Survivor中,这样就只有10%的内存被浪费,可是若是存活的对象总大小超过了Survivor的大小,那么就把多出的对象放入老年代中。
在三个区域中有两个是Survivor区。对象在三个区域中的存活过程以下:
如上所述,两个Survivor区域在任什么时候候一定有一个保持空白。若是同时有数据存在于两个Survivor区或者两个区域的的使用量都是0,则意味着你的系统可能出现了运行错误。
存活在新生代中但未变为不可达的对象会被复制到老年代。通常来讲老年代的内存空间比新生代大,因此在老年代GC发生的频率较新生代低一些。当对象从老年代被移除时,咱们称之为 "Major GC"(或者Full GC)。 老年代使用标记-清理或标记-整理算法
MaxTenuringThreshold
中要求的年龄(默认是15)。在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代全部对象总空间。
若是小于,虚拟机会查看HandlePromotionFailure设置值是否容许担任失败。
若是容许,那么会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小
前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来做为轮换备份,所以当出现大量对象在Minor GC后仍然存活的状况时(最极端就是内存回收后新生代中全部对象都存活),就须要老年代进行分配担保,让Survivor没法容纳的对象直接进入老年代。与生活中的贷款担保相似,老年代要进行这样的担保,前提是老年代自己还有容纳这些对象的剩余空间,一共有多少对象会活下来,在实际完成内存回收以前是没法明确知道的,因此只好取以前每一次回收晋升到老年代对象容量的平均大小值做为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态几率的手段,也就是说若是某次Minor GC存活后的对象突增,远远高于平均值的话,依然会致使担保失败(Handle Promotion Failure)。若是出现了HandlePromotionFailure失败,那就只好在失败后从新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分状况下都仍是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。