1. JVM大事记
1996年 SUN JDK 1.0 Classic VM,纯解释运行
1997年 JDK1.1 发布AWT、内部类、JDBC、RMI、反射
1998年 JDK1.2 Solaris Exact VM,数据类型敏感,提升的GC性能JDK1.2开始 称为Java 2,发布Swing Collections
2000年 JDK 1.3 Hotspot 作为默认虚拟机发布,发布JavaSound
2002年 JDK 1.4 Classic VM退出历史舞台,添加assert、正则表达式、NIO、IPV6、日志API、加密类库
2004年 JDK1.5 发布泛型、注解、装箱、枚举、可变长的参数、Foreach循环
2006年JDK1.6 发布脚本语言支持、JDBC 4.0、Java编译器 API
2011年 JDK7 发布G1、动态语言、增强64位系统中的压缩指针、NIO 2.0
2014年 JDK8发布Lambda表达式、Stream API、语法增强、Java类型注解
2016年JDK9 发布模块化
2. JVM基本结构
JVM运行机制:根据当前路径和系统版本寻找jvm.cfg,装载配置,寻找jvm.dll初始化JVM,获得JNIEnv接口(JNIEnv为JVM接口,findClass等操作通过它实现),找到类中main方法并运行
JVM基本结构:
①PC寄存器,线程私有,指向下一条指令的地址,在线程创建时创建
②方法区,保存装载的类信息(类型的常量池,字段,方法信息,方法字节码),JDK6时,String等常量信息置于方法区,JDK7时,已经移动到了堆
③堆,线程共享,应用系统对象都保存在Java堆中,GC的主要工作区间
④栈,线程私有,栈由一系列帧组成,帧保存一个方法的局部变量、操作数栈、返回地址、常量池指针
栈上分配一般指小对象在没有逃逸的情况下,可以直接分配在栈上,这样可以自动回收,减轻GC压力
注意:版本更迭对运行时数据区的改变
Java7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。方法区的内存回收目标主要是针对常量池的回收和对类型的卸载。而之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。Java7中已经将运行时常量池从永久代移除,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。在Java8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域被叫做元空间。
⑤本地方法栈,和java栈非常类似,最大的不同在于java栈用于方法的调用,而本地方法栈则用于本地方法的调用,作为对java虚拟机的重要扩展,java虚拟机允许java直接调用本地方法(通常使用C编写)
⑥直接内存,在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
3. JVM常见参数配置
打印GC详细信息:-XX:+PrintGCDetails
打印CG发生的时间戳:-XX:+PrintGCTimeStamps
指定最大堆和最小堆:-Xmx –Xms
设置新生代大小:-Xmn
新生代(eden+2*survivor)和老年代(不包含永久区)的比值:-XX:NewRatio
例子:4表示新生代:老年代=1:4,即年轻代占堆的1/5
设置两个Survivor区和eden的比:-XX:SurvivorRatio
例子:8表示两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10
注意:官方推荐新生代占堆的3/8,幸存代占新生代的1/10,在OOM时,记得Dump出堆,确保可以排查现场问题
设置永久区的初始空间和最大空间:-XX:PermSize -XX:MaxPermSize(使用CGLIB等库的时候,可能会产生大量的类,这些类,有可能撑爆永久区导致OOM)
栈空间分配:-Xss 通常只有几百K
4. GC算法及垃圾收集器
首先了解什么情况GC可以回收对象,先从对象的状态说起:
可触及的,从根节点可以到达这个对象;
可复活的,一旦所有引用被释放,就是可复活状态,在finalize()中可能复活该对象,只有一次复活;
不可触及的,在finalize()后,可能会进入不可触及状态,不可触及的对象不可能复活,可以回收。
注意:避免使用finalize(),操作不慎可能导致错误。
再细说GC算法:
GC算法(Java中,GC的对象是堆空间和永久区)
(1)引用计数法(Java没有采用)
问题:引用和去引用伴随加法和减法,影响性能;很难处理循环引用
(2)标记清除
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。未被标记的对象就是未被引用的垃圾对象。在清除阶段,清除所有未被标记的对象。
(3)标记压缩
标记-压缩算法适合用于存活对象较多的场合,如老年代。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。然后将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
(4)复制算法(空间浪费)
适用于存活对象较少的场合如新生代,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
再讲解下GC垃圾回收器:
①串行收集器(新生代、老年代使用串行回收)
特点:最古老,最稳定,效率高,可能会产生较长的停顿,新生代采用复制算法,老年代采用标记-压缩算法
参数配置:
-XX:+UseSerialGC
②并行收集器
(1)ParNew(Serial收集器新生代的并行版本)
特点:多线程,需要多核支持
参数配置:
-XX:+UseParNewGC 新生代并行,老年代串行
-XX:ParallelGCThreads 限制线程数量
(2)Parallel收集器(类似ParNew)
特点:新生代采用复制算法,老年采用标记-压缩算法,更加关注吞吐量
参数设置:
-XX:+UseParallelGC 使用Parallel收集器,老年代串行
-XX:+UseParallelOldGC 使用Parallel收集器,老年代并行
-XX:MaxGCPauseMills 最大停顿时间,单位毫秒
-XX:GCTimeRatio 垃圾收集时间占总时间的比,默认99,即最大允许1%时间做GC
③CMS并发收集器(Concurrent Mark Sweep 并发标记清除)
特点:老年代收集器(新生代使用ParNew),采用的是标记-清除算法,此CMS收集器尽可能降低停顿,清理不彻底,在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
参数配置:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理
整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量
④G1并发收集器(收集尽可能多的垃圾(Garbage First)的设计原则)
特点:采用了启发式算法,G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中,G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。
参数配置:
-XX:G1HeapRegionSize可指定分区大小
-XX:+UseG1GC 开启选项
5. 逃逸技术
在Java中每一个对象都有一定的作用域,理论上,一个对象在一块代码中构造,那么也应该在这块代码中被回收,但实际上,我们经常会让一个对象存活更长的时间,超过定义它的代码块,我们将这种现象称为逃逸。
逃逸按照行为不同有可以分为方法逃逸和线程逃逸。产生方法逃逸一般是由于返回值返回,或者是将对象的引用设置到传入的参数中,线程逃逸则由于同一个对象被多个线程使用,产生资源占用而导致。
Java虚拟机在确定对象不发生逃逸的情况下,所进行的一些高效的优化。
①栈上分配
明确对象不会发生逃逸时,就可以对这个对象做一个优化,直接分配到栈上,这样这个对象就会随着方法的出栈而销毁,这样就可以将少垃圾回收的压力。
②同步消除
确定一个对象不会发生逃逸时,就没有必要对这个对象进行同步操作,VM将会取消这种变量操作的同步操作,从而提升性能。
③标量替换
标量指的是Java中的基本类型,标量替换即是将一个聚合量拆成多个标量来替换,即用一些基本类型来代替一个对象。明确对象不会发生逃逸,并且可以进行标量替换的话,就可以不创建这个对象,就可以节省创建和销毁对象的开销。
6. 类加载
①class装载验证流程
(1)加载(装载类的第一个阶段):取得类的二进制流,转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象
(2)链接(验证、准备、解析)
验证:保证Class流的格式是正确的
•文件格式的验证
•元数据验证
•字节码验证
•符号引用验证
准备:分配内存,并为类设置初始值
解析:符号引用替换为直接引用
(3)初始化:执行类构造器,子类的构造器调用前保证父类的构造器被调用
②类装载器
ClassLoader负责类装载过程中的加载阶段,ClassLoader的实例将读入Java字节码将类装载到JVM中,可以定制,满足不同的字节码流获取方式
注意:
(1)类装载的机制的层次结构
BootStrapClassLoader (启动ClassLoader):负责加载rt.jar /-Xbootclasspath目录下的类库
ExtensionClassLoader (扩展ClassLoader):负责加载%JAVA_HOME%/lib/ext/*.jar
AppClassLoader (应用ClassLoader/系统ClassLoader):负责加Classpath下的类
(2)工作模式:自底向上检查类是否存在,自顶向下尝试加载类
这种双亲模式的问题:顶层ClassLoader,无法加载底层ClassLoader的类
例如Java框架(rt.jar)如何加载应用的类?javax.xml.parsers包中定义了xml解析的类接口Service Provider Interface SPI 位于rt.jar即接口在启动ClassLoader中。而SPI的实现类在AppLoader。
Thread.setContextClassLoader():上下文加载器,用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题,基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例,上下文ClassLoader可以突破双亲模式的局限性。
注意:双亲模式的破坏:Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent,OSGi的ClassLoader形成网状结构,根据需要自由加载Class
7. Class文件结构
文件结构(魔数、版本、常量池、访问符、类、超类、接口、字段、方法、属性)
①魔数magic u4(0xCAFEBABE)标识是否为一个class文件
②版本minor_version u2、major_version u2 不同JDK编译器版本,值不同
③常量池constant_pool_count u2 常量池数量
constant_pool cp_info 常量池信息
类型 | 标识标签 | 说明 |
CONSTANT_Utf8 | 1 | UTF-8编码的Unicode字符串 |
CONSTANT_Integer | 3 | int类型的字面值 |
CONSTANT_Float | 4 | float类型的字面值 |
CONSTANT_Long | 5 | long类型的字面值 |
CONSTANT_Double | 6 | double类型的字面值 |
CONSTANT_Class | 7 | 对一个类或接口的符号引用 |
CONSTANT_String | 8 | String类型字面值的引用 |
CONSTANT_Fieldref | 9 | 对一个字段的符号引用 |
CONSTANT_Methodref | 10 | 对一个类中方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 对一个接口中方法的符号引用 |
CONSTANT_NameAndType | 12 | 对一个字段或方法的部分符号引用 |
④访问符 access flag u2 类的标示符
⑤类 this_class u2 指向常量池的Class
⑥超类 super_class u2 指向常量池的Class
⑦接口 interface_count u2 接口数量
interface u2 接口
⑧字段 field_count字段数量
field 字段信息
access_flags u2 访问类型
name_index u2 字段的名字
descriptor_index u2 字段的类型
attributes_count u2 额外信息,包括修饰常量等
attribute_info attributes[attributes_count];
⑨方法 methods_count方法数量
method_info 方法信息
access_flags u2 访问类型
name_index u2 方法名字
descriptor_index u2 描述符
attributes_count u2 额外信息,包括代码等
attribute_info attributes[attributes_count];
⑩属性
在field和method中,可以有若干个attribute,类文件也有attribute,用于描述一些额外的信息
8. 锁
线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
Jvm的锁机制:
首先介绍一下,对象头Mark
Mark Word,对象头的标记,32位,描述对象的hash、锁信息(指向锁记录的指针,指向monitor的指针,偏向锁线程ID),垃圾回收标记,年龄
②偏向锁(锁会偏向于当前已经占有锁的线程)
适合大部分情况没有竞争的场景,可以通过偏向来提高性能。将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark,没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步,当其他线程请求相同的锁时,偏向模式结束,在竞争激烈的场合,偏向锁会增加系统负担。
参数配置:
-XX:+UseBiasedLocking 默认启用
-XX:BiasedLockingStartupDelay 延时开启时长
③轻量级锁(BasicObjectLock,嵌入在线程栈中的对象锁)
轻量级锁是一种快速的锁定方法,将对象头的Mark指针保存到锁对象中,将对象头设置为指向锁的指针(在线程栈空间中)。如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁),在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降。
④自旋锁
当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋),JDK1.7中,已内置实现,自旋成功时,节省线程挂起切换时间,提升系统性能。
注意:内置于JVM中的获取锁的优化方法和获取锁的步骤
偏向锁可用会先尝试偏向锁,再轻量级锁可用会先尝试轻量级锁,以上都失败,尝试自旋锁,再失败,尝试普通锁,使用OS互斥量在操作系统层挂起