前言
为了达到给所有硬件提供一致的虚拟机平台的目的,jvm牺牲了一些与硬件相关的性能特性。
第一章 走进java
2006年,Sun公式宣布最终会将java开源,并建立了OpenJDK组织源码进行独立管理。
除了极少数产权代码(Sun本身也无权进行开源处理)外,OpenJDK几乎包括了Sun JDK的全部代码。
所以OpenJDK 7 与 Sun JDK 1.7本质史昂就是同一套代码库开发的产品
历史
1996年,sun发布jdk1.0,该jdk中所带的虚拟机就是Classic VM. 这款虚拟机采用纯解释权方式来执行java代码,若用jit编译器,就必须外挂。使用外挂jit编译器后,解释器便不再工作。
若用解释器,解释器的性能本来就比较慢。
若用编译器,编译器需要对每一行代码都进行编译,由于程序相应时间的压力,不敢应用编译耗时较高的优化技术。
由此,“java语言很慢”的形象产生jdk 1.2时,在Solaris平台发布了Exact VM.
使用了准确式内存管理(虚拟机可以知道内存中某个位置的数据具体是什么类型)。
之前Class VM基于handler(句柄)的对象查找方式,每次定位对象多了一次查找开销HotSpot VM:准确式内存管理,热点代码探测技术
Sun JDK,Open JDK都是使用HotSpot VMDalvik VM,即android上使用的jvm。
严格来说,Dalvik VM不是一个java虚拟机;
它没有遵循java虚拟机规范,不能直接执行java的class文件;
使用的是寄存器架构而不是jvm中常用的栈架构Microsoft JVM
微软曾是java技术铁杆支持者,与sun争夺java控制权,令java从跨平台技术变为绑定在windows上的技术是ms的主要目的。
sun起诉微软,微软最终败诉,赔偿sun公司2000万,承诺终止其jvm发展。
有趣的是,xp sp3完全抹去了jvm,sun又登报希望微软不要这么做
未来
- 模块化
- 混合语言(java平台上多语言很合编程正成为主流)
- 多核并行(函数式)
- 进一步丰富语法
- 64位虚拟机
由于指针膨胀和各种数据类型对齐补白的原因,运行在64位系统上的java应用,通常比32位系统额外增加10% - 30%的内存消耗;
多个机构测试显示,64为虚拟机运行速度在各个测试项中几乎全面落后32位虚拟机,两者大约15%左右的性能差距。
32位有个严重的限制:内存不超过4GB;
因此,很多企业使用虚拟机集群等方式继续在32位系统中运行。
计算机最终会完全过渡到64位,jvm也会从32位发展至64位
其他
函数式编程的一个重要优点:程序天然地适合并行运行
第二章 java内存区域与内存溢出异常
运行时数据区
- 方法区
- 堆
- 虚拟机栈
- 本地方法栈
- 程序计数器PC
方法区,堆 :线程共享
栈,pc : 线程私有
本地方法栈中方式使用的语言,使用方式,数据结构并未强制规定,jvm可以自由实现它。(Sun HotSpot 直接把本地方法栈和虚拟机栈合二为一)
程序计数器
每个线程都有一个独立了的PC,
若正在执行java方法,则PC记录的是正在执行的虚拟机字节码指令地址;
若正在执行native方法,则PC值为空。
此内存区域是唯一一个在jvm中不可能发生OutOfMemoryError的区域
java虚拟机栈
栈中包括了局部变量表
局部变量表存放了编译器可知的各种基本数据类型,对象引用,returnAddress类型
本地方法栈
native 方法
java堆
jvm所管理内存中最大的一块.
堆的唯一目的: 存放对象实例,几乎所有的对象实例都在这里分配内存(jvm规范说,所有的对象实例以及数组都要在堆上分配,但随着jit的发展,栈上分配,标量替换等优化技术,打破了这个限制)
从内存分配角度看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)
从内存回收角度,java堆还可以细分
- 新生代(eden, from, to)
- 老年代
不论如何划分,都与存放内容无关
无论哪个区域,存储的都仍然是对象实例
java堆可以位于物理上不连续的内存空间,只要逻辑上连续即可
方法区
存储虚拟机加载的类信息,常量,静态变量,jit编译后的代码等数据
习惯上,称作“永久代”,但并不等价。
HotSpot把gc分代扩展至方法区,或者说使用永久代来实现方法区。
对其它JVM,并不存在永久代的概念
如何实现方法区,不收虚拟机规范约束,用永久代来实现,不是个好主意,永久代很容易内存溢出
方法区主要是针对常量池的回收和对类型的卸载。该区域回收性能较差,尤其是对类型的卸载,条件相当苛刻
运行时常量池
方法区的一部分。
class文件中,包含了常量池信息,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
常量池中的内容:编译器产生,也可能运行期产生
直接内存
并不是虚拟机的一部分,收到本机总内存(RAM+swap)空间的限制。
其他
StackOverflowError
- 线程请求的栈深度大于虚拟机所允许的深度
OutOfMemoryError
- 虚拟机栈扩展时,无法申请到足够的内存
- 对中没有内存完成实例分配,并且堆也无法再扩展
- 方法区无法满足内存分配需求
hotspot虚拟机对象
对象创建
虚拟机遇到new指令时,检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用的类是否已被加载,解析和初始化过。若没有,则先执行相应的类加载过程。
类加载检查通过后,将为新生对象分配内存。
对象所需内存大小在类加载完成后便可完全确定。
若java堆中内存是规整的,使用“指针碰撞”方式为对象分配内存空间;若不是,使用“空闲列表的”内存分配方式。
堆是否规整,由其采用的垃圾回收算法决定。
多线程问题
给A分配内存,指针未来得及修改,对象B又同时使用了原来的指针来分配内存。解决这个问题有两种方案:
- 分配内存空间的动作进行同步处理。(虚拟机使用CAS配上失败重试的方式保证更新操作的原子性)
- 把内存分配的动作按照线程划分在不同的空间之中进行。即内个线程在java堆中预先分配一小块内存(本地线程分配缓冲TLAB)。
- 通过-XX+/-UseTLAB来设定是否使用TLAB
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。
这一步操作保证了java代码中可以不赋初始值就可直接使用。
接下来,虚拟机要对对象进行必要的设置,(对象是哪个类的实例,如何找到类的元数据信息,对象哈希码,对象的GC分代年龄等信息),这些信息存储在对象头之中。
从虚拟机角度来讲,此时对象就产生了。从java程序视角看,对象创建才刚刚开始
执行new指令后,会执行init方法,按照程序员的意愿初始化对象。此时,创建对象完成。
对象内存布局
- 对象头
- 实例数据
- 对齐填充
对象头:
- 对象运行时数据:哈希码,gc分代年龄,锁状态标志,线程持有的锁,偏向线程id,,偏向时间戳等。
32bit或64bit,官方称作“Mark Word” - 类型指针,指向类元数据的指针。虚拟机通过此指针确定对象是哪个类的实例。
实例数据:
- 对象真正存储的有效信息
- 从父类继承的,子类定义的。都会记录下来。
- 存储顺序受到虚拟机分配策略参数和字段在java源码中定义顺序的影响
- hotspot默认分配策略:longs/doubles、ints、shorts、chars、bytes、booleans、oops(ordinary object pointers),相同宽带的字段总是分配到一起。在满足此前提的情况下,父类中的变量会出现在子类之前。
- 若CompactFields参数设为true,默认为true,则子类中较窄的变量也可能会插入到父类变量的空隙之中。
对齐填充
- 仅起到占位符的作用。hotspot要求对象qi’shi起始地址必须是8字节的整数倍。即对象大小必须是8的整数倍。
对象的访问定位
- 句柄
- 直接指针(hotspot使用的方式)
句柄:
- java堆中需要划分一块内存来作为句柄池。
- 对象指针reference中存储的是对象句柄地址
- 句柄中包含对象实例数据与类型数据两个地址
- 对象移动时,只改变句柄中的实例数据指针,reference本身不需要修改
直接指针:
- reference存储的是对象的真实地址
- java堆对象中需要考虑放置类型数据的相关信息
- 速度更快,节省了一次指针定位开销。由于对象访问非常频繁,这类开销也不小
其他
EMA:eclipse memory analyzer:分析dump出的堆转储快照
String.intern()
jdk1.6及以前,该方法会将首次遇到的字符串复制到永久代中,返回的也是永久代中这个字符串实例的引用。
jdk1.7以后,不会再复制实例,只是在常量池中记录首次出现的实例引用
第三章 垃圾收集器与内存分配策略
程序计数器,虚拟机栈,本地方法栈:随线程生,随线程灭。
这些区域不需要过多考虑回收问题。
垃圾收集器主要关注的是堆和方法区。
对象存活检测算法
- 引用计数 :无法解决循环引用问题
- 可达性分析算法:以gc roots的对象作为起始点,向下搜索。
GC roots:
- 虚拟机栈
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
引用分类:
- 强引用,默认。引用存在时,不会回收
- 软引用。描述还有用但是非必需的对象。内存溢出之前,进行回收
- 弱引用。使用WeakReference类来实现。生命周期在xia下一次垃圾收集之前。
- 虚引用。使用PhantonReference类来实现。
不可达的对象,也并非“非死不可”。在finalize()方法中,可以把this参数赋值给某个变量,就保存下了引用。
回收方法区
主要回收废弃常量和无用的类。
回收类,条件相当苛刻:
- 不存在类的实例
- 类的classloader已回收
- class对象没有被引用,没有通过反射访问该类的方法
即使满足以上条件,进说明类对象“可以回收”,但也不一定会回收
GC算法
- 标记-清除算法:标记和清除的效率都不高;产生内存碎片
- 复制算法:实现简单,运行高效。需要额外内存空间进行担保,内存利用率低,浪费50%空间
- 标记-整理算法:先标记,后整理,较少内存碎片问题
- 分代收集算法
HotSpot的算法实现
枚举根节点
- 使用可达性分析方法,时间消耗很大,并且会产生gc停顿。
- 可达性分析需要确保一致性,分析过程中不可以出现引用关系变化的情况,因此gc时会暂停所有java执行线程,即stop the world.
- java虚拟机基本都采用准确是gc,虚拟机有办法直接得知哪些地方存放着对象引用。
- hotspot的实现中,使用OopMap的数据结构记录引用信息。可以快速完成gc roots的枚举
安全点
- hotspot并没有为每条指令都生成OopMap(过于占用空间),只在特定位置记录了这些信息,这些位置称为“安全点”
- 程序在运行时,只有到达安全点是才能停顿下来,执行gc
安全区域
- sleep状态的线程,无法到达安全点。无法gc,需要安全区域来解决
- 安全区域位于一段代码之中,引用关系不会发生变化,在此区域任何地方开始gc都是安全的
垃圾收集器
- Serial收集器。单线程收集,gc是暂停其他所有线程。效率高。client模式下虚拟机的默认新生代收集器。client模式,内存一般100M左右,不会太大,延迟不到100ms,基本可接受。
- ParNew收集器。Serial的多线程版本。使用多个线程执行gc。stop the world。其他与Serial类似。运行在server模式下首选的新生代收集器,主要是因为只有它能与VMS收集器配合工作。单cpu时,比serial要差,适合多cpu环镜
- Parallel Scavenge。关注点在吞吐量,而不是减小stop world时间。吞吐量=运行用户线程时间/(用户线程+gc线程)
- Serial Old 收集器。Serial收集器的老年代版本。单线程,标记-整理算法。client模式下使用
- Parallel Old收集器。parallel老年代版本,标记-整理算法。
- CMS收集器。以获取最短回收停顿时间为目标。标记-清除算法。
- G1收集器。gc技术最前沿成果。面向服务器。hotspot希望未来它可替换CMS。
CMS
工作步骤:
- 初始标记。stop world,但会很快
- 并发标记。stop world,很快
- 重新标记
- 并发清除
优点:并发手机,低停顿
缺点:对cpu资源敏感,(少于4核时,性能慢)。内存碎片(标记-清除算法)。无法处理浮动垃圾。
其他
并发和并行
gc并行:多个gc线程并行工作,用户线程等待
gc并发:用户线程,gc线程同时执行(不一定并行,可能交替执行)
新生代GC(Minor GC):频繁,快速
老年代GC(Major GC/Full GC):比Minor GC慢10倍左右
内存分配与回收策略
- 对象优先在eden分配。eden无足够空间,触发一次Minor GC
- 大对象直接进入老年代
- 长期存活对象进入老年代
- 动态对象年龄判定。虚拟机不强制必须达到年龄才能晋升老年代,survivor中相同年龄所有对象大小的综合大于survivor空间一半时,也可直接进入老年代
- 空间分配担保
第四章 虚拟机性能监控工具与故障处理工具
jdk命令行工具
大多数是jdk/lib/tools.jar类库的一层包装,主要功能在tools类库中实现。因此命令行工具的可执行文件大小都在27kb左右。
- jps:查看本机所有java虚拟机进程
- jstat:收集虚拟机各方面的运行数据
- jinfo:显示虚拟机配置信息
- jmap:生成虚拟机内存转储快照
- jhat:分析jmap生成的dump文件,会启一个webserver,分析内存
- jstack:虚拟机线程快照
- javap:反汇编
jps -v : 输出虚拟机启动时的jvm参数
jstat -gc:监视java堆状况
jstat -gcutil:监视java堆,输出是占用百分比
jdk可视化工具
- jconsole
- VisualVM:功能强大。运行监视和故障处理。
jdk版本选择
版本升级也有不少性能倒退的案例,受程序,第三方包兼容性以及中间件的限制,在企业中升级jdk版本需要慎重考虑。