对于Java程序员来讲,虚拟机和多线程方面的知识是必不可少的。这里就来聊一聊Java虚拟机的一些基础和概念,主要内容源自《深刻理解Java虚拟机》这本书。html
首先为何要有虚拟机呢?由于对象的建立和销毁是一个很频繁的操做,由程序员来维护,一方面成本有点高,增长开发成本;另外一方面,若是操做不当,发生了内存泄露,要本身去调试代码,找出缘由。因此虚拟机的这种机制的诞生能够说是程序员的福音,解放了生产力。但凡事有利也有弊,虚拟机的引入使得Java在性能方面跟C++比仍是有必定差距,像网络游戏或者数据库这种对性能要求比较高的应用,都会选择用C/C++来开发。同时,虽然咱们有了虚拟机,但仍是要对它的运行机制和性能调优有所了解,否则万一发生java内存泄露,就会无从下手了。java
图中的五大数据区域能够分为两类:1.由全部线程共享的数据区域 2. 线程隔离的数据区程序员
第一类:方法区,堆算法
第二类:虚拟机栈,本地方法栈,程序计数器数据库
(图片源自博客http://blog.sina.com.cn/s/blog_ed30769e0102v233.html)网络
能够把它看做是当前线程所执行的字节码的行号指示器。咱们经过改变这个计数器的值来选取下一个须要执行的字节码指令。每一个线程都有一个独立的程序计数器。若是线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是native方法,这个计数器值为空。此内存区域是惟一一个在java虚拟机规范中没有规定任何OutOfMemoryError状况的区域。多线程
它描述的是Java方法执行的内存模型:每一个方法被执行都会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。并发
局部变量表存放了编译期可知的各类基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。局部变量表所需的内存空间是在编译期间完成分配的。函数
这个区域规定了两种异常:布局
1.若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常。
2.内存动态扩展时,若没法申请到足够的内存时会抛出OutOfMemoryError异常。
此方法区与Java虚拟机栈相似,区别在于它是为虚拟机使用到的Native方法服务。有的虚拟机(如Sun HotSpot虚拟机)直接把它们合二为一了。
与虚拟机栈同样,它也会抛出 StackOverflowError和OutOfMemoryError异常。
全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例。
能够经过-Xmx和-Xms的设置来控制堆内存大小。
若是堆没法扩展时,将会抛出OutOfMemoryError异常。
它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java虚拟机对这个区域的限制很是宽松。垃圾收集行为在这个区域是比较少出现的;这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
当方法区没法知足内存分配需求时,将抛出OutOfMemoryError异常。
它是方法区的一部分; 用于存放编译期间生成的各类字面量和符号引用,这部份内容将在类加载后存放到方法区的运行时常量池中。
运行期间也可能将新的常量放入池中,好比说使用String类的intern()方法。
没法申请到内存时会抛出OutOfMemoryError异常。
直接内存并非虚拟机运行时数据区的一部分,也不是虚拟机规范中的内存区域,但这部份内存被频繁地使用,也可能会致使OutOfMemoryError异常出现。
在JDK1.4中加入了NIO,引入了一种基于通道与缓冲区的I/O方式,它可使用Native函数库直接分配堆外内存,而后经过存储在堆里面的DirectByteBuffer对象做为这块内存的引用操做。
Object obj = new Object();
虚拟机栈:会在本地变量表中存储一个对象引用
堆:存储了Object类型全部实例数据值
方法区:存储对象的类信息
不一样的虚拟机的对象访问方式会有所不一样,主流的访问方式有两种:使用句柄和直接指针。
堆会划分出一块内存来做为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息
好处:引用中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而引用自己不须要修改。
堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,引用中直接存储的就是对象地址。
好处:速度更快,节省了一次指针定位的时间开销。
堆溢出:经过-XX:+HeapDumpOnOutOfMemoryError能够在出现内存溢出时Dump出当前的内存堆转储快照以便过后进行分析。
虚拟机栈和本地方法栈溢出:经过-Xss设置栈容量。通常栈深度能够到达1000-2000。栈容量过大,多线程时容易耗尽内存,由于单个线程耗内存比较多。
运行时常量池溢出:常见的PermGen issue。Java8对方法区作了调整,因此这个问题不会再出现了。
方法区溢出:每每出现于动态生成大量Class的应用中。
本机直接内存溢出:可经过-XX:MaxDirectMemorySize指定,若是不指定,则默认与堆的最大值同样。
如何肯定对象已经死去,能够采起回收了呢?
很是直观的一种方法,增长一处引用时加一,减小一处引用时减一。
但它的一个主要缺陷是很难解决对象之间的循环依赖问题。
基本思路:经过一系列GC roots对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC roots没有任何引用链相连时,则证实此对象是不可达的,做为回收对象。
GC roots对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中的引用的对象
2.方法区中的类静态属性引用的对象
3.方法区中的常量引用的对象
4.本地方法栈中JNI的引用的对象
在JDK1.2以后,Java扩充了引用的概念,分为四种:
1.强引用(strong reference):通常引用,有引用,不会回收对象。
2.软引用(soft reference):系统将要发生溢出时,会把它们引用的对象列入回收范围进行一次回收。若内存仍是不够,才抛异常。
3.弱引用(weak reference):被引用的对象只能活到下一次垃圾收集发生以前。
4.虚引用(phantom reference):一个对象是否有虚引用,彻底不会对其生存时间构成影响,也没法经过虚引用取得一个对象实例。设置此引用得目标就是为了能在这个对象被回收时收到一个系统通知。
若是一个对象在根搜索后,没有关联得引用链,它将会被第一次标记而且进行一次筛选,筛选得条件是此对象是否有必要执行finalize()方法。
若是没有覆盖此方法或者方法已被虚拟机调用过,则视为“没有必要执行”。
若是须要执行finalize()方法,则会被放置到一个F-Quenue队列,由低优先级得Finalizer线程去执行。执行此方法,但并不保证运行结束(为了运行效率考虑)。
稍后会对F-Queue进行第二次小规模得标记,只要在finalize方法中与引用链上得任意一个对象产生关联就会在这次标记时被移除“即将回收”的集合。
在堆中,尤为是新生代中,一次垃圾回收能够回收70%~95%的空间,而方法区的垃圾收集效率远低于此。
方法区的垃圾回收主要内容:废弃常量和无用的类。
类须要知足下面3个条件才能算是“无用的类”,能够进行回收:
1.该类全部的实例已经被回收
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法
是否对类进行回收,由参数-Xnoclassgc进行控制
算法分为标记和清除两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收掉全部被标记的对象。
缺点:
1.效率问题,标记和清除过程的效率都不高
2.空间问题,会产生大量不连续的内存碎片。若是发现不能分配内存给大对象时,不得再也不触发一次垃圾回收
将可以使用的内存按容量分为两块,每次只使用其中的一块。
通过实验发现,新生代中的对象98%是朝生夕死的,因此内存能够分配为一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和其中一块Survivor空间;回收内存时,将还存活的对象拷贝到另一块Survivor空间上,而后清理掉另外两块的空间。
Sun HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。当Survivor空间不够时,须要依赖老年代进行分配担保,如有担保直接分配进老年代。
复制算法在对象存活率较高时就要执行较多的复制操做,效率将会变低。因此老年代通常不能直接选用这种算法。
标记-整理算法在标记后,让全部存活的对象向一端移动,而后直接清理掉端边界之外的内存。
历史最悠久的收集器,在JDK1.3.1以前是虚拟机新生代收集惟一的选择。单线程的收集器。
优势:简单高效
缺点:收集时必须暂停其余全部的工做线程(stop the world)。
使用场景:虚拟机运行在Client模式下默认新生代收集器。
它是Serial收集器的多线程版本。默认开启的收集线程数与CPU的数量相同。JDK1.4引入。
除了Serial收集器外,只有它能与CMS收集器配合工做。
使用场景:运行在Server模式下的虚拟机首选的新生代收集器。
目标:达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。适合在后台运算而不须要太多交互的任务。
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间
-XX:GCTimeRatio用来设置吞吐量大小,默认99
-XX:+UserAdaptiveSizePolicy, 开启GC自适应的调节策略。
使用场景:新生代收集器
它是Serial收集器的老年代版本,使用标记整理算法。
使用场景:Client模式下的老年代。
它是Parallel Scavenge收集器的老年代版本。JDK1.6引入。
注重吞吐率的场合,能够考虑采用Parallel Scavenge加Parallel Old收集器。
它是一种以获取最短回收停顿时间为目标的收集器,并发执行。标记清除算法:初始标记,并发标记,从新标记,并发清除。其中初始标记和从新标记仍需stop the world。
缺点:
1.对CPU资源很是敏感。默认回收线程数是(CPU数量+3)/ 4。
2.没法处理浮动垃圾,可能出现“concurrent mode failure”失败而致使另外一次full gc的产生。要是在运行收集期间,预留的内存没法知足程序须要,就会出现“concurrent mode failure”失败,这时会临时采用serial old收集器来进行老年代的垃圾收集,这样停顿时间会变长。
3.收集结束时会产生大量碎片,容易因没法分配大对象,而触发full gc。能够启用参数-XX:+UseCMSCompactAtFullCollection来享受full gc后来一次碎片整理。另外有-XX:CMSFullGCsBeforeCompaction这个参数来设置执行了多少次不压缩的full gc后,进行一次带压缩的。
目标是替代CMS收集器。能够分代收集,不会产生碎片。有分区(region)的概念,优先回收垃圾最多的区域。经过remenbered set来避免全堆扫描。
收集可分为四个步骤:初始标记,并发标记,最终标记,筛选回收。
当Eden区没有足够空间时,会触发一次minor gc。虚拟机提供-XX:+PrintGCDetails这个参数来打印内存回收日志以及进程退出时当前内存各区域的分配状况。
这样作的目的时避免Eden区里面发生大量的内存拷贝。可经过-XX:PretenureSizeThreshold参数设置多大的对象进入老年代。
熬过15次minor gc后,到达15岁,就会被晋升到老年代。关于这个年龄,能够经过-XX:MaxTenuringThreshold来设置。
为了适应不一样程序的内存状况,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代。
在发生minor gc时,虚拟机会检测以前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。
若是大于,则改成进行一次 full gc。
若是小于,先查看HandlePromotionFailure设置(JDK1.6默认开启)是否容许担保失败。若是容许,进行minor gc;若是不容许,仍是要 full gc。
若是最后发生担保失败,仍是要从新发起一次full gc。