这篇文章的素材来自周志明的《深刻理解Java虚拟机》。
做为Java开发人员,必定程度了解JVM虚拟机的的运做方式很是重要,本文就一些简单的虚拟机的相关概念和运做机制展开我本身的学习过程。java
java虚拟机运行在受不一样操做系统操纵的物理机上,不一样的操做系统使用不一样的底层方法来执行不一样的操做,这些方法称之为本地方法:Native Method,本地方法通常执行的都是比较底层的操做,好比说IO、线程管理等,java方法则会执行的通常是相对高级的操做,好比说数逻运算,或者是调用底层的本地方法来完成底层任务。算法
java虚拟机的运行时数据区域将内存分红了不一样的部分协调完成java虚拟机的内存数据交互。数据结构
按照数据存储过程的数据结构能够大体分为:多线程
栈区:并发
虚拟机栈:java虚拟机运行的java方法(java字节码方法)构成的栈空间,这个空间在运行时存储这些方法的局部变量表、操做栈、动态连接和方法出口;jvm
本地方法栈:本地方法在运行时存储数据产生的栈区。工具
堆区:性能
java堆:对象的实例存储在这个共享的堆空间里,因为占有最大的和最有实际意义的空间,这个空间的GC过程时虚拟机运行的重点。学习
方法区:存储虚拟机运行时加载的类信息、常量、静态变量和即时编译的代码,所以能够把这一部分考虑为一个保存相对来讲数据较为固定的部分,常量和静态变量在编译时就肯定下来进入这部份内存,运行时类信息会直接加载到这部份内存,因此都是相对较早期进入内存的。spa
运行时常量池:不是全部的常量都是在编译时就肯定下来进入内存的,仍然会有运行时才进入内存的常量,这部分常量通常是编译时产生的一些固定信息,好比说翻译出的引用等,直接在类加载的时候把它们存入运行时常量池有助于提升性能。
全部的内存区域的数据交互由程序计数器指导虚拟机完成复杂的逻辑步骤。
如何找到一个对象的实例:
Object obj = new Object();
在这个过程当中在虚拟机栈的局部变量表里建立obj引用,在堆内存里建立Object类的一个实例,最后就是把obj引用和这个对象实例关联起来的问题了,另外,咱们须要知道的是,不是全部的实例都完整地保存了全部的类的信息,通常共有的或者静态的类的数据将被保存在方法区中,独有的实例数据才会真的被保存在java堆里,所以每一个引用必须同时找到关联它的实例数据和类数据。针对这个问题,有两个办法来作:
I. 引用存储的只是实例的句柄,句柄在堆的句柄池中,句柄中保存着到堆中真正实例的地址和到方法区中类数据的地址,这样就能够经过这个句柄能够找到这些地址。
II. 引用存储的就是实例在堆中的地址,而实例中是含有能够定位类数据的地址的,也就是经过找到的实例地址能够再去寻找它对应的类的数据。
两个和内存溢出相关的异常:
StackOverflowError:线程申请的栈深度大于虚拟机的规定值;
OutOfMemoryError:线程扩展增长的内存大于虚拟机的要求;
虚拟机栈、本地方法栈和计数器大都是编译期肯定的内存分配,在线程执行完毕后即会清理,内存回收相对比较容易。因此咱们提到的内存回收大都是指堆内存的回收。咱们经过以下几个问题来讲明内存回收机制:
什么样的堆内存是能够回收的呢?简而言之就是那些“没用”的内存,那么怎样的内存是“没用”的呢?即那些经过现有的指针(或称“引用”)条件下再也访问不到的内存对象。因此有这样的算法来描述无效的引用:
(引用计数算法)每一个对象都有一个被引用计数器,被引用一次计数器加1,引用被置空时减1,最终被引用计数器的值为0 的便是“无用”的内存对象,它占用的内存能够被回收。
(这个算法看起来好像没有问题,可是遭遇到循环引用的时候就会出现问题:若是同时将循环引用的双方置空,那么即便被引用计数器不为0也再也访问不到这些对象了,即发生了内存无端占用)。
这个过程体现了互相循环引用可能带来的问题,对象仍被引用可是已经不能被访问了,因此是这种算法的缺陷。
(根搜索算法)将由栈内存或方法区引用的对象做为GCRoots去构建引用链,若是能找到这个对象则说明这个对象可以访问其内存不能被回收,反之经过这些引用链找不到这个对象则说明已是弃用的对象了,其内存是应该被回收的。(上面的互相循环引用的例子就能够解决了,由于这个问题里面虽然其被引用计数器的值不为0,可是已经没有GCRoots可以找到这些内存了,这个问题里的GC Roots是栈内存里的objA和objB,这两个栈内存里的引用被置空,所以引用链里没办法再找到对内存里的对象了。)
肯定了有哪些内存该被回收后GC机制是直接回收内存吗?GC会给这些内存中的某些对象一次机会,就是那些重写过finalize方法的类的对象,GC会执行这个对象重写过的finalize方法,若是在这个方法中对象从新将本身连接给了某个引用使得这块内存区域从新能够被访问,那么GC就不会在此次回收它,可是,这个过程只能执行一次,下一次再被GC遇到的话就不会顾及这个finalize方法而是直接回收了,所以要注意重写的finalize方法只能执行一次。
这个是堆内存中对象的回收,在方法区里保存类信息和常量池的内存一样须要回收,这个过程相对来讲更缓慢也并无那么高效,由于一段时间内线程使用的类和常量池都比较稳定,只有当真的确认有类再也不使用且不被反射使用的时候才会卸载类,当真的没有常量再被使用的时候才会释放常量池中不用的常量。
知道了哪些内存该被回收、回收前的最后确认以后来讲内存回收策略,也就是内存回收的时候到底是依据什么样的算法进行的?
(标记-清除算法)
(复制算法)
(标记-整理算法)
经过这些算法,jvm能够将已不被引用的无效内存回收,标记-清除算法清理获得的内存每每出现碎片,而标记-整理解决了内存碎片却增长了时间消耗,复制算法则会出现内存浪费的问题,结合不一样场景使用不一样算法进行垃圾回收是十分重要的。
了解了内存垃圾回收的算法,咱们来看执行垃圾回收的垃圾收集器。根据堆内存对对象的代的划分咱们对堆内存有这样划分:
各版本和种类的垃圾回收器各有其用武之地,配合使用它们获得最好的效果十分重要。由于在垃圾内存回收的过程当中对每一个对象分代处理,因此对不一样代的垃圾内存有不一样的收集器去回收:建立不久的对象称为新生代,新生代对象的特色便是生死频率高,从生到死的过程很短,因此再回收时有大量的这样的内存存在,因此采用复制算法采用较大的eden:survivor比率将使得内存较完整也较快地回收,同时,老年代的内存存储的是建立好久仍然没有失去引用的对象,这类对象因为长期存在于内存中且将来的生死也经常不肯定,因此须要使用速度慢可是更精确地标记-整理算法。下面是真正执行这些回收过程的收集器:
新生代收集器:(主要使用复制算法)
Serial收集器:单线程+“Stop the World”停顿式收集
ParNew收集器:多线程版本的Serial收集器
Parallel Scavenge收集器:多线程收集器,关注“吞吐量”
老年代收集器:(主要使用标记-整理算法)
Serial Old收集器:Serial的老年代版本
Parallel Old收集器:Parallel的老年代版本
CMS收集器:并发收集、低停顿,关注短期停顿
G1收集器:高级和领先的新型垃圾收集器
JVM虚拟机将会依次对每次即将进入堆内存的对象作出安排,必定时间间隔内对于失去引用的无效内存进行回收,当内存出现溢出的时候试图经过垃圾回收自发解决问题保持系统回归平稳。
申请内存的对象优先被分配到堆内存的Eden区,若是Eden区的空间不足就向survivor区上放,若是仍然放不下就会引起一次发生在新生代的minor GC,在此次GC过程当中,若是发现仍然又放不下的对象,就将这些对象放入老年代内存里去(这种现象是对垃圾回收的统计学规律的挑战,由于理论上大多数新生代内存不该该存活到这个时候,因此这个时候就会引起这种叫作分配担保机制的对象向老年代转移),若是存在失去引用的内存,那么就将剩余存活的对象移往survivor区,剩下的Eden区内存所有清理。
大对象直接进入老年区,上面的描述中咱们已经能够看到大的对象在一旦出现长时间存活的时候会引起分配担保机制进入老年区,因此不如直接在刚开始建立这个对象的时候就把它放入老年区。
长期存活的对象直接进入老年区:同上面的描述,长期存活的对象的移动会耗费资源,因此在建立这些长期存活的对象时就将它直接放入老年区。
动态对象的年龄判断:虚拟机并非一直等待全部的对象都到达老年代的标准才将它们放入老年期,由于那样作可能会使新生代的空间一直很紧张引起没必要要的GC,因此在当Survivor区里的对象中相同年龄的对象的大小达到Survivor区的一半时就能够将其移入老年区。
空间分配担保:当每次执行minor GC的时候应该对要晋升到老年代的对象进行分析,若是这些立刻要到老年区的老年对象的大小超过了老年区的剩余大小,那么执行一次Full GC以尽量地得到老年区的空间。
这里咱们使用一个实例借助VusualVM来查看程序运行过程当中的虚拟机内存分配的过程:
在这个例子中,各类参数均使用默认值:
public class VMTest { private static final int _1MB = 1024*1024; public static void main(String[] args) throws InterruptedException { Thread.sleep(4000); byte[] allocation1; for (int i = 0; i < 400; i++) { allocation1 = new byte[_1MB]; System.out.println("Create One"+i); Thread.sleep(1000); } } }
这个例子中,主线程每次循环向虚拟机申请内存建立新对象,而后在循环结束的时候将引用连接到新的对象,原来的对象就会处于失去引用的状态,每隔一段时间后JVM的minor GC就会使得这些弃用的对象占据的内存被回收。如下便是这个过程当中VisualVM展现的的实时内存各区占据状况:
这个过程当中,咱们能够清楚地看出内存分配的全过程。新的对象做为新生代对象会被分配到新生区的Eden区中,在一个循环中这些对象都会被分配到Eden区中,由于Eden区默认的超过600M的空间足够容纳这些对象,当一段时间后发生minor GC的时候就会将仍然存活的(也就是仍然有有效引用的)对象移至空的Survivor区,在这里是Survivor0区,失去引用的对象占据的Eden区空间将会被回收;下一次monor GC到来以前仍然会进行这样的空间分配,Eden区中会产生新的对象并有一些对象会失去有效引用,下一次minor GC到来的时候会把Eden区中存活的对象(以及Survivor0中存活的对象)移至空的Survivor区中,这里是Survivor1,并将Eden和Survivor0回收。注意,每次minor GC进行的时候都会将一个Survivor(from Space)置空,并将存活的对象移至空Survivor(to Space)里,若是Survivor(to Space)空间不足,则会引起分配担保机制将这些存活对象移至老年区。