JVM
JVM采用的是HotSpot JIT即时编译器,作为Java这门语言的核心,对笔者和无数猿们来说具有势不可挡的吸引力,对它的原理有一定了解后,笔者认为JVM距离各位大侠并不遥远。了解完相关原理后,每一行代码,甚至一个简单的"new Object()"都能引出千丝万缕的头绪,那么对每一行代码的理解都会深入一个层次。
JVM无关性基石
- 平台无关性
- JSE:Java Starded Edition ,在CS端使用,是常见系统Windows的CS
- JEE:Java Enterprise Edition ,在BS端使用,是常见系统Windows/Unix的BS。在JSE的基础上加了一层WEB容器,用于控制吞吐量,添加类加载器等,如Tomcat就是非常流行的JEE容器
- JME:Java Micro Edition ,在手机、机顶盒、打印机等移动设备和嵌入式设备中使用,是常见系统Android/IOS等的CS
- 语言无关性
- 通过JVM最终接收并解读的class文件格式实现,Scala、JRuby等语言最终也通过各自的编译器将源码编译为class文件(二进制文件),并交由JVM统一解读运行
类加载器
-
每个java程序都有至少三个类加载器
- 引导类加载器,加载rt.jar
- 扩展类加载器,加载jre/lib/ext下的包
- 系统类加载器,加载classpath指定的目录文件
-
双亲委派模型,当需要加载一个类时,所有加载器都会将此加载操作交给自己的父级,当父级在自己的搜索范围内无法搜索到此类时交给自己的下级,来避免重复类出现
- JSE/JME(CS)端的类加载器模型
- JEE(BS)端的类加载器模型
- JSE/JME(CS)端的类加载器模型
对象的创建
-
开辟内存
-
空闲列表(mark-sweep)
堆内存不规整的情况下,JVM维护一个列表,记录着哪些块用过,哪些块空闲,需要分配内存的时候,从列表中计算出分配的内存。大对象较少时适用
-
指针碰撞(mark-compact)
堆内存规整的情况下,空闲内存和已使用的内存通过一个指针分为两份,需要分配内存的时候,将指针向空闲的一边挪动一段距离,并将指针外的空间全部清理。存活率较多时适用
-
复制(mark-copy)
将内存按容量分为大小相等的两块,每次只使用其中的一块,当一块用完了,就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。存活率较小时适用,减少复制操作
-
-
初始化对象
-
创建header
Mark Word
a. 对象hashcode
b. 轻量级锁指针
c. 重量级锁指针
d. 偏向锁线程ID类型指针
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 -
创建body:各类型的字段内容
-
对齐填充:HotSpot VM的自动内存管理系统要求对象起始地址必须8字节的整数倍,如果不满足则自动对齐填充
-
垃圾收集器
-
新生代
-
Serial
单线程收集器,算法用Mark-Copy -
ParNew
多线程收集器,算法用Mark-Copy -
Parallel Scavenge
多线程收集器,算法用Mark-Copy,可控制吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间) )(即:系统共运行了100分钟,用户代码耗时99分钟,GC耗时1分钟)
-
-
老年代
-
Serial Old
单线程收集器,算法用Mark-Copy Mark-Compact -
CMS
多线程收集器,算法用Mark-Copy Mark-Sweep, 低停顿,将GC过程分为了4步,两步为并行执行(与用户代码),缺点:对CPU敏感,核数越多越好 -
Parallel Old
多线程收集器,算法用Mark-Copy Mark-Compact
-
-
新 + 老年代
- G1
多线程收集器,算法用Mark-Copy Mark-Compact 低停顿,将GC过程分为了4步,两步为并行执行(与用户代码),缺点:对CPU敏感,核数越多越好
- G1
JVM模型
-
JVM分为堆,栈两部分,栈是一个INFO(先进后出)的结构,栈中有若干个小的栈帧,一个栈帧相当于一个方法,当一个方法被线程执行时,创建一个栈帧,一个方法调用另一个方法时,后边执行的方法创建栈帧并压栈到它的调用方法所属的栈帧之上,执行完毕后出栈到栈顶。
- 栈帧中的返回值类型其实就是方法的返回值类型
- 操作数栈是记录当前栈帧引用的其它栈帧执行情况
- 局部变量数组是方法中的基本变量,比如int、byte等组成的数组
- 引用指方法中指向的堆中对象的地址,比如 Object obj = new Object(); 那么创建的Object在堆中,obj是对堆中Object的引用
-
堆由静态域,方法区和常量池构成,是JVM在内存中存放数据的主要位置。每一个对象在第一次被调用时,都会在堆内方法区外创建一个实例,该实例中只有其中的静态部分,多个实例的静态部分组成了静态域。而实例域存放的是真实创建过的对象。同一个类,可能有多个实例,但只有一个静态域,静态域可被其它对象直接访问,是共享变量很好的载体。
GC策略
-
何时回收
- eden区空间不够分配新对象时
-
如何回收
- 判定Garbage
-
引用计数算法
给对象添加一个引用计数器,每次被引用时,计数器加1,引用失效时,计数器减1。无法解决两个对象互相引用的情况。
- 引用计数算法JVM模型
- 引用计数算法模型
- 引用计数算法缺陷
- 引用计数算法JVM模型
-
Root搜索算法(被Hotspot采用)
栈帧中的引用、静态域中的引用、常量池中的引用、Native方法的引用,可作为GC Root。当GC Root 失效,则判定它之前引用的对象不可达,那么可以回收。可作为Roots的对象包括 - 栈帧中的引用池,静态域中的引用,常量池中的引用
- Root搜索算法JVM模型
- Root搜索算法模型-多引用
- Root搜索算法模型-多引用回收
- Root搜索算法模型-互相引用
- Root搜索算法模型-互相引用回收
- Root搜索算法JVM模型
-
- 判定Garbage
- 回收Garbage
JVM采用的回收策略是分代回收分为两代:-
eden/survivor
-
新生代一般采用Mark-Copy/标记-复制算法,将内存按容量分为大小相等的两块,一般为[8:1],[eden:survivor],Minor GC时清除eden区不可存活对象,将eden区可存活对象移入survivor,将survivor区可存活对象按照一定规则移入tenure区,再将新对象放入eden中。当survivor空间不足时,将新对象直接移入tenure,当tenure空闲空间不足时,会按照一定规则计算风险,去判定是否需要Full GC(Minor GC + Major GC)
-
eden -> survivor : 当eden区不足开辟空间容纳新对象进入时,触发Minor GC,清除eden区不可存活对象,将eden区可存活对象移入survivor,将survivor区可存活对象按照一定规则移入tenure区
-
survivor -> tenure : 将survivor区超过一定年龄(默认15)的对象移入tenure区,当survivor区相同年龄的对象数量过半,则将大于或等于该年龄的对象全部移入tenure区,当tenure区空间不足容纳survivor区来的新对象时会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试将对象移入tenure区,如果小于,则将直接进行一次Full GC。当Minor GC失败,只好在失败后重新发起一次Full GC。jdk1.7开始会直接进行一次尝试,尝试失败后再Full GC,不再去计算
-
-
tenure
-
部分老年代收集器采用Mark-Sweep/标记-清除算法 : 将不可存活的对象标记,然后一并清除被标记的空间
-
部分老年代收集器采用Mark-Compact/标记-整理算法 : 将不可存活的对象标记,让所有存活的对象向一端移动,然后直接清理掉边界以外的内存
-
-
- 术语
- Minor GC:新生代回收,当eden空间不足时触发Minor GC
- Major GC:老年代回收,当tenure不足时触发Major GC(Full GC)
- Full GC :Minor GC + Major GC
- 注意概念混淆:Full GC出现必然伴随Major GC,Major GC只会出现在Full GC中,Full GC由Minor GC引发
参数配置
-
核心参数
- Xms : 初始堆大小 正式环境与 Xmx 相等,避免运行时自动扩展
- Xmx : 最大堆大小 64XP系统中控制为可用内存的80% , 32XP系统中总占用控制为可用内存的80%
- Xmn : 最大/初始新生代 控制为Xmx的 1/5
- Xss/ThreadStackSize : 可控制Xss为2M,也可控制ThreadStackSize且使用默认值即可
调优
-
JDK的选择
可以选择64位JDK的大内存,也可以选择若干个32位的逻辑集群,Windows中每个进程最多不超过2G,Unix中不超过4G,但是注意64位性能低于32位
-
关键参数的调配
- Xms = Xmx : 初始堆大小最好等于最大堆大小,避免内存扩张
- Xmn = Xms/5 : 最大堆大小最好为最大/初始新生代的5倍
- ThreadStackSize = 16k 或 Xss = 2M
- SurvivorRatio : 8 eden和survivor的比例最好用默认比例
- 收集器的选择
-
java7和java8默认为 Parallel Scavenge + Parallel Old
-
java9默认为G1
-
收集器的搭配非常重要,各有优势,目前主流PS搭配常见的搭配有
- Parallel Scavenge + Parallel Old,可控制吞吐量
- ParNew + CMS,对CPU敏感,用mark-sweep算法,大对象多时会严重影响GC频率
- G1目前不稳定
-
编译器
-
前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)
将*.java文件编译为*.class文件(二进制)
-
JIT即时编译器:HotSpot VM的C1、C2编译器
将*.class文件编译为机器码文件
-
AOT编译器:GUN Compiler for the Java(GCJ)、Excelsior JET
直接把*.java编译为机器码文件
-
代码优化:
- 代码简化:高内聚、底耦合
- 早期优化(前端编译器):
- 泛型擦除 : 编译期将泛型擦除,替换为泛型替代后的类型,在JIT编译器中不识别泛型;
- 基本类型自动装箱、拆箱 : String编译期优化,short,int,long,double,float编译器自动优化;
- 条件编译自动优化
- 晚期优化(运行期)
- 热点代码探测
- 内存位置变换
- 循环变换
运行期优化是我们几乎无法感知的,所以笔者在这儿就止步了