概要
- 基础知识
- 内存管理
- 执行引擎
- 编译与代码优化
- 高效并发
64位虚拟机 VS 32位虚拟机
-
JVM虚拟机 性能 64位 < 32位
- 64位内存占用更多
- 64位性能弱于32位虚拟机器:垃圾回收管理更大内存,JIT的效率也有不同
普通对象指针压缩,可以在64位虚拟机上优化性能
-XX:+ UseCompressedOops
ps:以上是书本结论,但是物理机上的表现看,64位性能更好,一方面是指令更短,ALU(算术逻辑运算器)和寄存器可以处理更大的整数,特别是在高精度计算上64位更为卓越,32位寄存器有4G内存空间的限制,64位内存空间可以扩展得更大。长期来看64位处理器和64位虚拟机会是趋势,未来应该还是首选64位物理机,虚拟机也会是64位虚拟机。
编译JDK
调试工具GDB
- todo:...
Java内存区域 & 溢出异常
jvm结构
-
【程序计数器】
- 线程私有,不会OOM,Native方法为Undefined,当前线程所执行的字节码的行号指示器。
-
【JAVA虚拟机栈】
- 每一个方法的执行,就是一个栈帧的入栈到出栈
- 栈帧: 局部变量表,操作数栈,动态链接,方法出口
- 局部变量表:基本数据类型,对象引用,和returnAddress
- 长度64的long和double需要占用2个局部变量的空间(slot)其余的占用1个
- 局部变量表需要的空间在编译器件确定,运行期间不会改变
-
【本地方法栈】
- Native方法
-
【Java堆】
- 实例和数组在堆上分配
- JIT编译器的发展和逃逸技术成熟【栈上分配 + 标量替换】实例不一定在堆上创建。
- GC主要是针对的堆
- 收集器主要是:分代收集
- 新生代,老年代,...
-
【方法区】
- 线程共享,虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码数据
- 永久代
- 【运行时常量池】是方法区的一部分,运行时常量池和class文件常量池的区别是具备动态性。
其他:
- 【直接内存】
* 不属于运行时数据区
* Nio中引入了channel和buffer的io方式,使用native函数库直接分配堆外内存
* 然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用操作
* 避免了java堆和native堆中来回复制数据,提高了性能。
对象的创建过程New
1 检查在常量池中是否有一个类的符号引用,如果有,且已经被加载,解析,初始化过,就返回,否则重新创建
2 分配内存,指针碰撞(规整的空间移动一段距离),空闲列表(适用于不规整的空间)
- 注意分配空间如何线程安全:
- CAS 失败重试
- 给每个线程预分配一段空间(TLAB),避免出现线程共享区域
3 必要的类设置:实例归属,元数据信息,hash码,GC分代年纪等等
4 执行init方法
对象的内存布局
对象在内存中布局分为3个区域:对象头,实例数据,对齐填充
- 对象头:运行时数据 和 类型指针
类型指针,指向它类元数据的指针,注意查找对象的数据不一定要经过对象本身(之后会说)
ps:如果对象是一个java数组,还需要记录数组长度 - 实例数据,尽量是相同大小的分配在一起
- 对齐填充,保证数据是8的整数倍
对象的访问定位
句柄 和 直接指针
-
句柄
-
直接指针
- 直接指针只有2次指针访问,速度快
- 句柄的好处是,reference稳定,对象改变影响小
垃圾回收
标记是否垃圾的算法
- 引用计数
- 缺陷:互相引用的时候不会被回收
- 可达性分析,GCroot向下搜索
- 可作为GCroot的对象
- 虚拟机栈的栈帧 中的本地变量表 的引用的 对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI(即Native方法)引用的对象
- 可作为GCroot的对象
引用
- 强引用:引用还在,垃圾回收不会回收
- 软引用:内存溢出前回收
- 弱引用:每次垃圾回收都回收
- 虚引用 Phantom Reference:被垃圾回收的时候可以收到系统通知
回收
- 1 可达性分析之后先标记可以回收
- 2 如果没有覆盖finalize()方法,或者finalize()被调用过,则没有必要执行
- 3 如果有必要执行,进入F-Queue队列中,之后被JVM创建的一个线程回收
注意,finalize()运行代价高,不保证调用顺序,所以不适合用来关闭外部资源,应该用try-finally来关闭资源。
回收方法区(永久代)
- 废弃常量
- 无用的类
- 类的所有实例被回收
- 加载类的ClassLoader被回收
- 类对应的java.lang.Class对象没有在任何地方被引用,也无法通过反射访问类的方法
GC的算法
- 标记清除
- 复制:回收新生代
- 标记整理
- 分代收集
GC的stop the world:
- 大部分GC在做垃圾回收都会挂起所有用户线程,保证回收的数据一致性(比如 Copying这种算法)
- 枚举根结点,一定会stop the world
- 使用安全点,gc的时候让线程运行到安全点,只有到达了安全点才能进行GC
- 如果线程没有cpu时间,也无法走到安全点,那么就要用安全区域来解决,一段代码中,引用关系不变化,这个区域的任意地方GC都是安全的。
性能要求高的环境难以接受,所以要减少频繁的GC,CMS分为几个阶段:
*【initial Mark】
- Concurrent Mark
- 【ReMark】
- Concurrent sweep
initial Mark和Remark会Stop the world. Concurrent Mark和Concurrent Sweep会和用户线程一起运行。虽然CMS减少了stop the world的次数,不可避免地让整体GC的时间拉长了
不同的垃圾收集器,没有一个通用的,HotSpot实现了很多:
新生代的:大多是复制算法
- Serial:单线程收集器,有停顿,简单,适用于允许停顿的客户端程序。
- ParNew:Serial的多线程版本,性能优,除了Serial之外,只有他能和CMS配合工作。在JDK5之后,老年代选用CMS,新生代就只能用Serial和ParNew了。单核环境中,ParNew的性能不如Serial(线程交错的开销)
- 【Parallel Scavenge】,关注的不是减少停顿时间,而是达到一个吞吐量(代码运行时间/(代码运行时间 + GC时间)),所以指定的参数也是垃圾收集的最大停顿时间,以及吞吐量大小
老年代的:大多是标记清除 or 标记整理算法
- Serial Old
- Praallel Old
- 【CMS】,并发,标记清除
CMS:
- 几个阶段:初始标记,并发标记,重新标记,并发回收
- 优点:性能,停顿少
- 缺点:标记清除产生碎片多,无法处理浮动垃圾,并发清理阶段可能还产生新的垃圾,会出现Concurrent Mode Failure,导致额外产生一次GC
G1收集器 VS CMS
- G1更代表未来
- G1过程:初始标记,并发标记,最终标记,筛选回收
- G1分代收集,老年代和年轻代都可以用
- G1 标记-整理,碎片少
- G1 可预测的停顿,垃圾收集时间不超过xx毫秒有可控参数
对象分配策略
上图还需要补充一个点“空间分配担保”
第四章 故障 & 性能监控工具
工具列表
- jps:查看进程,主类
-q 本地虚拟机唯一id
-l 主类全名
-v 启动的JVM参数
-m 给主类的参数
- jstat:虚拟机统计信息监控
- class 类装载,总空间等
- gc 堆的状况,各个区域
- compiler jit编译过的方法和耗时
- jhat:可视化
- jinfo:查看和调整JVM配置信息
- jstack:查看栈
- jmap: 查看堆
- dump 堆快照
- heap 堆详情
- histo 堆统计
调优经验
- 深夜触发定时任务做 fullGC,保持内存在一个稳定水平
- JDK 64 在堆内存溢出的时候 dump 会有十几个G的文件,大文件的dump和分析都比较困难。
类文件的结构
传统编译器: 类文件 --> 编译 --> 本地机器码
java: 类文件(.java)--> 编译 --> 字节码 (.class)--> jvm
构成
- magic
- magic_version
- major_version
- constant_pool_count
- constant_pool
- access_flags
- this_class
- super_class
- interfaces_count
- interfaces
- fields_count
- fields
- methods_count
- methods
- attributes_count
- attributes
指令集
类加载机制
准备阶段
1 如果是类变量,会在解析阶段赋初始化的0值。
比如 static int value=123,这个阶段value是0
分配在【方法区】
而value= 123的putstatic是在编译后,初始化阶段,放在类构造器<clinit>()方法中
如果是实例变量,会在对象实例化的时候,随着对象一起分配在java堆中
对于常量ConstantValue中有值
static final int value = 123
会在编译的时候为value生成ConstantValue属性,在准备阶段value就会变为123
- 类的初始化过程与类的实例化过程的异同?
类的初始化是指:
类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程
类的实例化是指:
在类完全加载到内存中后创建对象的过程。
类实例化的一般过程是:
父类的类构造器<clinit>()
-> 子类的类构造器<clinit>()
-> 父类的成员变量和实例代码块
-> 父类的构造函数
-> 子类的成员变量和实例代码块
-> 子类的构造函数。
编译优化
编译器,解释器,通常jvm采用的是混合模式,但是可以通过参数来指定编译或者解释。
对于热点代码(采样探测 or 计数器),会被即时编译器编译,
即时编译的优化有若干,这里忽略
其他优化策略:
- 公共子表达式消除
- 数组边界检查消除
- 方法内联(减少引用)
- 逃逸分析:对象的栈上分配,同步消除,标量替换