Java平台无关性
Java可跨平台运行,一次编译,到处运行。
Java在设计中采用了虚机器码方式,即Java语言编译后产生的是虚机器码,虚机器码运行在一个解释器上,每一个操作系统均有一个解释器,这样一来,Java就成了平台无关语言。
JVM简介
- 虚拟机:通过软件来模拟出来的具有完整的硬件系统功能的,运行在完全隔离的环境中的完整的计算机系统。
- JVM一样也是通过在实际的计算机中软件虚构出来的,用来模拟一套完整的运行平台。
- Java的一次编写处处运行:是指一段代码可以在不同的平台运行,这就归功于JVM
- 作为Java编译器和os(操作系统)之间的虚拟机解释器,JVM根据不同的os,将Java编译后的目标代码(字节码)解释成不同的os可运行的机器指令。
JVM发展史
- Sun Classic
JDK1.0发布的第一款商用虚拟机。只能采用纯解释器的方式执行Java代码,是一种边编译边执行的一种方式,这种方式使得Java代码的编译时间比解释时间还要长,因此导致“Java很慢”。 - Exact VM
发布于JDK1.2,已经具备现代高性能虚拟机的雏形,支持编译器与解释器混合工作。
Exact VM因为他的准确式内存管理得以命名(Exact Memory Management)。
虚拟机可以知内存中的某个位置的数据具体是什么类型。
由于使用准确式内存管理,Exact VM可以抛弃以前Classic VM 基于handle的对象查找方式,每次定位对象都少了一次间接查找的开销,提升执行性能。 - 现在默认的虚拟机Sun HotSpot VM
前两种虚拟机现在已经不再使用。
HotSpot VM是Sun JDK 和Open JDK中所带的虚拟机,也是Java目前使用范围最广泛的Java虚拟机。
HotSpot VM 既继承了前面两种虚拟机的优点(准确式内存管理),也有许多自己的技术优势,例如热点代码探测技术。- 热代码探测技术
①可以通过执行和计数器找出最具有编译价值的代码,根据执行计数器判断是否达到阈值,没到就解释执行,否则就提交编译请求,否则提交编译请求,通知JIT编译器以方法为单位进行编译。 所以:如果一个方法被频繁调用,或方法中有效循环次数很多,将会触发标准编译和OSR(栈上替换:代码块在解释执行过程中直接切换到本地代码执行)编译动作。
②通过编译器与解释器恰当地协同工作,可以再最优化的程序影响时间与最佳执行性能中取得平衡,即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码(机器执行码)。
- 热代码探测技术
JVM内存区域-运行时数据区域
运行时数据区域分为两种:
1.线程隔离的数据区域;(程序计数器,Java虚拟机栈,本地方法栈)
2.所有线程共享的数据区域。(Java堆,方法区)
- 程序计数器
程序计数器是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,所以程序计数器这类内存区域为“线程私有的内存”。
如果线程正在执行的是Native(本地)方法,这个计数器值则为空。 - 栈介绍
所谓“栈”包括:Java虚拟机栈、本地方法栈;他们作用相似,区别只是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。程序员人为的分为“堆栈”中的“栈”。
栈里存放了编译期可知的各种数据类型(booleanbyte,char,short,int,float,long,double),对象引用和指向了一条字节码指令的地址。
每个方法在执行的同事都会创建一个栈帧用于存储局部表量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应看一个栈帧在虚拟机栈中入栈到出栈的过程。
局部表量表所需的内存空间在编译期间完成分配,其中64位的long
和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
操作数栈也需要操作栈,主要是在方法计算时存放的栈。 - Java堆
Java堆是Java虚拟机所管理的内存中最大的一块,此内存区域就是存放对象实例,几乎所有的实例对象都在这里分配内存。
Java堆是垃圾收集器管理的主要区域;内存回收的角度来看Java堆中还可以细分为:新生代、老年代;新生代细致一点的又Eden空间、From Survivor空间、To Survivor空间。
在实现时,既可以实现固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(-Xmx设置最大内存,-Xms设置初始内存)。 - 方法区
方法区又叫静态区:用于存储已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是他却有一个别名叫做Non-Heap(非堆);
对于HotSpot虚拟机是使用永久代来实现方法区。
Java虚拟机规范堆方法区的限制非常宽松,除了不需要连续的内存和可以选择固定大小的或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,这区域的内存回收目标主要针对常量池的回收和对类型的卸载,条件相当苛刻。
Java中的常量池技术,是为了方便快捷的创建某些对象而出现的,当需要一个对象时,就可以从池中取出一个来(如果没有就创建一个),在需要重复创建相等变量时节省了很多时间。 - 异常
大部分的异常都是为(OutOfMemoryError)OOM异常。
线程请求的深度大于虚拟机所允许的深度,扩展时无法申请到足够的内存。
在堆中没有内存完成实例分配,并且堆也无法再扩展。
当方法区无法满足内存分配需求时。
以上的情况会抛出OOM异常。
垃圾收集器
- 对象的引用
Java中的引用分为:强引用,软引用,弱引用,虚引用(幽灵引用或者幻影引用),这四种引用的强度依次减弱。
强引用:在程序代码中正常的类似于“Person p = new Person();”,这类的引用;垃圾回收器不会回收掉被强引用的对象。
软引用:有用但非必须的对象,Java中提供了SoftReference类来实现软引用;系统在发生内存溢出异常之前,会把只被软引用的对象进行回收。用途:缓存。
弱引用:非必须的对象,JDK中提供了WeakReference类来实现弱引用,比软引用弱一点。垃圾回收不论是否内存不足都会回收只被弱引用关联的对象。
虚引用:对被引用对象的生存时间不影响;无法通过虚引用来取得一个对象实例;为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个通知。JDK提供PhantomReference类来实现虚引用。 - 对象的可达性分析
判断对象的存活方式:引用计数算法、可达性分析;主流的商用程序语言的主流实现中是通过可达性分析。
引用计数算法的基本思想:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能再被使用的。
可达性分析基本思想:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(不可达)时,则证明此对象是不可用的。
GC Roots对象:- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般所说的Native方法)引用的对象
- 对象的生死
不可达的对象真正死亡需要两次标记
当不可达时第一次标记,当对象覆盖finalize()方法并且finalize()方法没有被虚拟机调用过,此对象将会放置在一个叫做F-Queue的队列中,稍后由一个由虚拟机自动建立的低优先级的Finalizer线程去触发这个方法,但并不承诺会等待它运行结束再执行垃圾回收。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中重新与引用链上的任何一个对象建立关联那么他被移除出“即将回收”的集合,否则就被回收了。 - 标记-清除算法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,此方法分为两个阶段:标记、清除。
标记要清除的对象,统一清除;
不足有两个:
①一个是效率问题,标记和清除两个过程的效率都不高;
②另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记+清除+整理=标记整理算法 - 复制算法
复制算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:无内存碎片,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点:实际可用内存缩小为了原来的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代。
1、将内存分为一块较大的Eden空间和两块较小的Survivor空间;
2、每次使用Eden和其中一块Survivor。
3、当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,并清理掉Eden和刚才用过的Survivor空间。
垃圾回收器-GC方式
GC方式分三种:
- Minor GC:
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。每次 Minor GC 会清理年轻代的内存 - Major GC:
Major GC 是清理老年代或者永久代。 - Full GC:
Full GC 是清理整个堆空间—包括年轻代和老年代或者永久代。
内存分配策略
对象在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,此时对象会进入survivor区,当对象满足一些条件后会进入老年代。
三种方式进入老年代:
- 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
- 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- 大对象直接进入老年代:虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
空间分配担保:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代的所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。
如果不成立,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
垃圾收集器
收集器就是内存回收的具体实现。
- 并行(Parallel):指多条垃圾收集线程并行工作,但是此时:用户线程仍然处于线程等待状态。
- 并发(Concurrent):指用户线程和垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个cpu上。
Serial
Serial收集器是最基础、历史最悠久的适合新生代的收集器。
特点:单线程、stop-the-world 、复制算法
缺点:影响用户响应时间
优点:回收时简单高效、对于限定单个cpu环境下,serial收集器由于没有线程交互的开销,专心做垃圾收集,可以获得最高的单线程收集效率。
所以:serial 收集器 对于运行在client模式下的虚拟机来说,是一个很好的选择。
SerialOld收集器是Serial的老年代收集器,采用“标记-整理”
ParNew
ParNew收集器其实是Serial的多线程版本,除了他是使用多条线程来进行垃圾回收之外和Serial是完全一样的。新生代收集器
特点:多线程、stop-the-world
缺点:单个cpu下,运行效果甚至没Serial好。
优点:回收时简单高效、对于限定多个cpu环境下,效果比serial好。
所以:parnew收集器是运行在server模式下的首选收集器。
Parallel Scanvenge
Parallel Scanvenge收集器是一个新生代收集器,采用复制算法。
特点:收集新生代,复制算法,多线程,高吞吐、自适应
1、与其它的收集器侧重垃圾回收时用户的停顿时间不同,它主要侧重与吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
停顿时间越短就越适合需要与用户交互的程序,高吞吐量则是可以高效率地利用cpu时间尽快完成任务。
2、他有一个自适应开关(-XX:+UseAdaptiveSizePolicy):打开后,用户只需要把基本的内存数据(堆最大,初始量)设置好,然后设置更关注最大停顿时间或者更关注吞吐量,收集器会把细节参数自动调节。
Parallel Old 老年代收集器,采用标记-整理算法。
CMS
CMS(concurrent mark sweep)收集器是一个以获取最短回收停顿时间为目标的老年代收集器。
特点:并发收集、低停顿。
基于 标记-清除算法实现,但是整个过程比较复杂一些。过程分为4步:
1、初始标记:仅仅标记GCRoot能直接关联到的对象。速度很快,“stop the world”
2、并发标记:GCRoot Tracing。耗时长和用户线程同步。
3、重新标记:修正并发标记时,由于用户程序运行导致的标记变动。“stop the world”停顿稍长一些。
4、并发清除:耗时长,和用户线程同步。
缺点:吞吐量会变低、浮动垃圾无法处理、标记-清除的碎片(设置参数是 fullgc前开启碎片整理功能,gc停顿时间延长)。
可以兼容的新生代收集器:ParNew和Serial
G1
G1(Garbage-First)收集器是当今收集器领域最前沿成果之一。2004年sun发表第一篇G1论文,10年后才开发出G1的商用版本。
hotspot开发团队赋予它的使命:未来替调CMS收集器。
特点:
1、并行与并发:利用多cpu缩短stop-the-world的时间,使用并发方式解决其它收集器需要停顿的gc动作
2、分代收集:新老代收集区分对待。
3、空间整合:G1从整理看是基于标记-整理,但是局部看是基于复制算法实现的,不会产生碎片。
4、可预测的停顿:能够让使用者指定在M毫秒的时间片段上,消耗在垃圾回收的时间不得超过N毫秒。
过程:初始标记、并发标记、最终标记、筛选回放。前三个和CMS一致,筛选回放是根据用户设置的停顿目标来选择回收价值最高的进行回收。