java VS c++
在学习java虚拟机之前,首先认识一下java和c++的区别:
C++强大之处在于能直接操作内存,嵌套C和汇编,还可以直接操作系统的硬件,这让程序员非常有成就感;
当然能力越大,麻烦事越多,你需要手工去控制很多东西,要一直维护着每个对象从诞生到结束的整个过程.
Java则是把程序员从这些琐屑的底层操作中解放出来,让程序员更关注代码功能层面上的设计,因而可以更容易地写出很优雅的代码;
对于Java来说,好的代码很容易维护(容易理解和修改)很容易扩展(对外扩展更多的功能),明晰的跟说明书一样;
实际上Java和C++无法相互取代,只有适用场景的区别
如果项目迭代速度很快,开发周期短,参与开发的人员很多,对服务器长期运行的性能要求不是很极致(一般来说做了很多优化的C++的代码,在一般的场景下,也就比Java快1.5倍左右),一般就用Java;开源项目一般用Java和脚本语言的比较多;
而如果需要榨干硬件效率,那肯定要用C++了,毕竟C++能很方便的控制底层;这个时候C++的性能就能轻易地跟Java拉开差距;当然代码就比较难看了.
实际上C++也可以写的跟Java一般优雅,当然从易用性上来说还是Java更好,开发效率高,代码易维护.
java技术体系
从广义上讲,Clojure、JRuby、Groovy等运行在java虚拟机上的语言及其相关的程序都属于java技术体系中的一员;仅从传统意义上来看,java技术体系包括:
1. java程序设计语言
2. 各种硬件平台上的java虚拟机
3. Class文件格式
4. Java API类库
5. 来自商业机构和开源社区的第三方Java类库
我们可以把Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK(Java Development Kit),JDK是用于支持Java程序开发的最小环境;
另外,可以把Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE(Java Runtime Environment),JRE是Java程序运行的标准环境。
JVM内存模型
程序计数器
是当前线程执行的字节码的行号的指示器.
字节码解释器通过改变计数器的值来获取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复都靠它完成.
JVM多线程是通过线程轮流切换来实现的。线程切换保证能够恢复到正确的位置。所以每个线程都需要一个独立的程序计数器,是线程私有的内存。
每个线程都有自己的一个计数器,线程之间计数器互不影响。
执行Native方法时,计数器不起作用,值为空(Undefined)
此区域是唯一没有规定OOM的区域.
Java虚拟机栈
归属线程私有,生命周期与归属的线程相同.
虚拟机栈描述的是Java方法执行的内存模型:
方法执行时会创建栈帧(Stack Frame)存储局部变量表、操作数栈、动态链接、方法出口等.
方法的调用到执行完成的过程,对应其栈帧从虚拟机栈中入栈到出栈的过程.
局部变量表存放编译器可知的基本数据类型、对象引用;long、double因为长度为64bit,会占用两个Slot,其他则占用一个,由此可知局部变量表的内存空间在编译期就完成了分配,方法运行时不改变其大小.
本地方法栈:
虚拟机栈为虚拟机使用到的Java方法服务。本地方法栈为虚拟机使用到的Native方法服务。
Java堆
JVM中内存最大的一块,所有线程共享,几乎所有对象实例、数组都在这里诞生.
垃圾回收的主要区域,又称GC堆.
方法区
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
所有线程共享,别名No-Heap(非堆),也叫Permanent Generation(永久代);之所以名为永久代,是因为HotSpot将GC从堆延伸至了方法区,以便于像管理java堆一样管理这部分内存,就省去了专门为方法区编写内存管理代码的工作,实际上利用永久代来实现方法区并不是一个好主意,因为永久代有内存上限更容易遇到内存溢出问题,JDK1.7就把原本放在永久代的字符串常量池移出.
并非被称为永久代,其中的数据就永久了,当类型卸载、常量池回收时也会发生GC,但是比较少见.
当此区无法分配新内存时,抛出OOM异常.
运行时常量池
属于方法区的一部分,保存各种字面量和Class文件中描述的符号引用.
具备动态性,并不要求常量只在编译器产生,运行期间也可以将新的常量放入池中----String.intern()
无法申请到内存时,抛出OOM异常.
对象
对象的创建
虚拟机遇到一条new命令 :
为对象分配空间的任务等同于把一块大小确定的内存从Java堆中划分出来。
1.假设Java堆中的内存是绝对规整的,用过的放一边,没用的放一边,中间放着一个指针作为分界点的指示器。那内存分配就是把那个指针向空闲空间那边挪一个对象大小相等的距离。这种分配方式成为 “指针碰撞Bump the Pointer”
2.如果Java堆中的内存不是规整的。虚拟机就必须维护一个列表,有哪些块是可用的。从列表中找到一块足够大的空间划分给对象实例。并更新这个列表。这种分配方法成为“空闲列表 Free List”
对象在内存中存储的布局分为3块区域:对象头、实例数据和对齐填充,HotSpot VM的自动内存管理系统要求对象的大小必须是8字节的整数倍,因此对象实例部分没有对齐时,需要通过对齐填充来补全。
对象的访问
句柄访问方式:Java堆中分配一块内存作为句柄池,reference中存储对象的句柄地址,句柄中则包含实例数据指针和类型数据指针;
直接指针访问方式:Java堆对象中存放访问类型数据信息,reference直接存储对象地址;
句柄访问方式:此方式的好处在于句柄地址稳定,对象变动时,只需修改句柄,而reference不需改动.
直接访问方式:这种做法的好处是速度快,节省了一次指针定位的开销。
OOM
java.lang.OutOfMemoryError:Java heap space
Java堆溢出,检查虚拟机的堆参数(-Xmx -Xms)
StackOverfolwError OutOfMemoryError
虚拟机栈和本地方法栈溢出:
在单线程条件下,不管是栈帧深度太大或者虚拟机栈容量太小,当内存无法分配都会报StackOverflowError
若不断建立线程,为每个线程的栈分配的内存越大,反而越容易产生内存溢出,由于虚拟机提供了参数来控制Java堆和方法区的这两部分的内存最大值,剩下的内存几乎由虚拟机栈和本地方法栈瓜分,每个线程分配到的栈容量越大,可以建立的线程就越少,因此建立线程时越容易把剩下的内存耗尽。
因此,若建立过多线程导致内存溢出,只能通过减少最大堆和减少栈容量来换取更多线程。
Java方法区溢出
JDK1.7 开始逐步“去永久代”
String.intern()是一个Native方法。它的作用是:
如果字符串常量池中已经包含一个等于此String对象的字符串。则返回一个代表池中这个字符串的String对象。否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
垃圾回收算法
垃圾的判断
1.引用计数算法:
原理:给对象添加一个引用计数器,有一个地方引用了它,则计数器加1;引用失效时,计数器减1;任何时刻计数器都为0的对象判定为不可用.
特点:实现简单、判定效率高,有无法识别不可用对象的情况(即对象之间相互循环引用,导致计数器不可能为0).
2.可达性分析算法:
GC Roots Tracing:将名为“GC Roots”的对象作为根,向下搜索,做过的路径成为引用链,当GC Roots到一个对象不可达,则此对象是不可用的.
引用
无论是通过引用计数器算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。Java中有以下几种引用:
1.强引用:类似于Object obj = new Object(),只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2.软引用:有用但并非必需的对象。在系统将要发生内存溢出异常时,把这些对象列入回收范围之中进行第二次回收,如果这次回收还没有足够内存,才抛出异常。
3.弱引用:非必需对象。当垃圾回收器工作时,无论有没有足够内存,都会被回收。
4.虚引用:最弱的引用关系,目的是能在这个对象被回收时收到一个系统通知。
垃圾收集算法
标记 - 清除 Mark - Sweep
标记清除: 算法分为标记和清除两个阶段。首先标记处要回收的对象。在标记完成之后统一回收所有被标记的对象。
优点:简单,容易实现
缺点:效率低,产生大量空间碎片
复制 Copying
优点:效率高
缺点:占用内存大
现在的商业虚拟机都采用这种算法来回收新生代。IBM公司的专门研究表明,新生代中的对象98%是朝生夕死的。所以并不需要按照1:1的比来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
当回收时,将Eden和Survivor中还存活的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的比例是8:1。 10%的空间被浪费。当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。(Handle Promotion)
标记 – 整理 Mark - Compact
性能和空间上的折中
能够清理掉边界意外的内存。
垃圾收集器
新生代:Serial、ParNew、Parallel Scavenge
老年代:Serial Old、Parallel Old、CMS
Serial/Serial Old
都是单线程,在进行垃圾收集时需要暂停其他所有的工作线程,直到收集结束(stop the world)
serial为新生代收集器,采用复制算法;serial old为老年代收集器,采用标记-整理算法
对于运行在Client模式下的虚拟机来说是个很好的选择
单线程的优点:CPU核不多时,单线程没有线程交互的资源开销,效率较高
ParNew/Parallel Old
都是多线程,适用于运行在Server模式下的虚拟机
ParNew为新生代收集器,采用复制算法;Parallel Old为老年代收集器,采用标记-整理算法
Parallel Scavenge(吞吐量优先收集器)
为新生代收集器,多线程,采用复制算法,主要关注CPU吞吐量(吞吐量=用于代码运行时间 / 总时间)
GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:将新生代空间调小收集时间会缩短,但收集行为也会更加频繁,导致停顿时间缩短但吞吐量降低。
CMS Concurrent Mark Sweep
为老年代收集器,基于标记-清除算法,关注最短回收停顿时间,整个过程包括四个部分:
1.初始标记 CMS initial mark
2.并发标记 CMS concurrent mark
3.重新标记 CMS remark
4.并发清除 CMS concurrent sweep
初始标记仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing 的过程。而重新标记则是为了修正并发标记期间因用户程序继续运行导致标记产生变动的那一部分对象的标记记录
重新标记时间比初始标记时间长,但远比并发标记时间短。初始标记 & 重新标记 这两个步骤仍然需要 “Stop The World”
CMS收集器的缺点:
1.吃CPU: CMS的默认启动回收线程数量是(CPU数量+3)/4. 4个CPU,那么占用的资源是25%。2个CPU。占用50%。
2.无法处理浮动垃圾: 并发清理时用户线程还在运行。这一期间产生的垃圾无法进行回收。只能在下一次GC的时候进行回收,因此在CMS运行期间需要在老年代预留一部分内存,若无法满足程序需要,则临时启用Serial Old收集器,但这样停顿时间就变长了。
3.基于标记清除算法,会产生大量的碎片。为了解决这个问题。CMS提供了一个参数,-XX:+UserCMSCompactAtFullCollection(内存碎片合并整理)
G1收集器
特点:并行与并发、分代收集、空间整合(整体看是标记-整理,局部看是复制)、可预测的停顿
也可分为四个部分:
1.初始标记 Initial marking
2.并发标记 Concurrent marking
3.最终标记 Final Marking
4.筛选回收 Live Data Counting and Evaluation
前三部和CMS一致,最后根据用户所期望的GC停顿时间来制定回收计划
并行和并发
并行 Parallel , 是指多条垃圾收集线程并行工作。但此时用户线程仍然处于等待状态。
并发 Concurrent, 是指用户线程与垃圾收集线程同时执行。(但不一定是并行的,可能会交替执行。)用户程序在继续运行,而垃圾收集程序运行在另外一个CPU上。
内存分配和回收策略
JVM内存分配策略:
1.对象优先分配在Eden上
2.大对象直接进入老年代
3.老对象进入老年代
4.对象动态年龄判定
5.空间分配担保机制
大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快.
当Eden区满的时候,执行Minor GC(新生代GC),将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的).
此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0.
当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的).
当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代.
总结:Eden区是连续的空间,且Survivor总有一个为空;经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中).
什么是大对象
-XX:PretenureSizeThreshold参数,大于这个值的对象直接在老年代进行分配。
什么是老对象
-XX:MaxTenuringThreshold=15参数,默认15,大于这个年龄的对象被复制到老年代
对象动态年龄判定
熬过一次Minor GC ,年龄就+1.若在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代
空间分配担保机制
在Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象空间。如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会看HandlePromotionFailure设置的值是否允许担保失败。如果允许则进行一次Minor GC。如果不允许要先进行一次Full GC