深入JVM内核-原理,诊断和优化
JVM初识
- JVM,是Java Virtul Machine的简称,称为Java虚拟机
- Java中比较重要的两个规范
- JAVA语言规范:定义了什么是JAVA语言
- JVM规范:主要定义JVM的内部实现,二进制class文件和JVM指令集等
JVM的运行机制
1.JVM的启动流程
[外链图片转存失败(img-Yuh4yqOu-1563543065680)(https://raw.githubusercontent.com/WaldeinCheng/ImgRepo/master/JVM启动流程图.png)]
2.JVM基本结构
[外链图片转存失败(img-4k9eAe6h-1563543065682)(https://raw.githubusercontent.com/WaldeinCheng/ImgRepo/master/JVM基本结构.png)]
- PC寄存器(程序计数器):线程私有,每一个线程拥有一个PC寄存器,指向下一条指令的地址
- 字节码解释器通过改变PC寄存器的值,来实现代码的流程控制:顺序执行,选择,循环,异常处理
- 多线程时,PC寄存器记录当前线程执行的位置,用于线程切换回来时能够知道线程上次执行的位置
- 方法区:所有线程共享,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。被称为“永久代”,是因为 HotSpot 虚拟机的设计团队把 GC 分代收集扩展到方法区,即使用永久代来实现方法区,像 GC 管理 Java 堆一样管理方法区,从而省去专门为方法区编写内存管理代码,内存回收目标是针对常量池的回收和堆类型的卸载
- JAVA堆:所有线程共享,在虚拟机启动时创建,唯一目的是存放对象实例,是垃圾收集器管理的主要区域——” GC 堆“,可以细分为新生代和老年代,新生代又可以细分为 Eden 空间、 From Survivor 空间和 To Survivor 空间;物理上可以不连续,但逻辑上连续,可以选择固定大小或者扩展;
- JAVA栈:线程私有,栈由一系列帧组成,然而每一帧保存的都是一个方法的局部变量,操作数栈,常量池指针
- 操作数栈:Java中没有寄存器,所有参数传递使用操作数栈
- 局部变量表:存放了编译器可知的各种数据类型,对象引用(reference类型)
- 栈异常:StackOverFlowError和OutOfMemeoryError
- 本地方法栈: 为JVM使用的 Native 方法服务,也是线程私有
3.JAVA内存模型
- 定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节
- JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
- happens-before原则:
Java内存模型中定义的两项操作之间的次序关系,如果说操作A先行发生于操作B,操作A产生的影响能***作B观察到,“影响”包含了修改了内存***享变量的值、发送了消息、调用了方法等。
GC算法和种类
1.GC的概念
- Garbage Collection:垃圾收集,Java中用来自动回收已经不使用的对象资源,以节约空间和提升效率,java中,GC的对象是堆空间和永久区
- 垃圾回收的主要问题就是怎么判断,对象已经没有使用了,常用的方法
- 引用计数法:给对象添加一个引用计数器,标记此对象是不是垃圾,有引用此对象,计数器就加1,引用失效就减1,当计数器为0时,表示此对象没有用了,可以回收,老牌的垃圾回收算法,java中没有使用
- 根搜索算法:通过被称为“GC Roots”的对象作为起点,从这些起点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不再被使用。在Java中可以当作GCRoots的对象包括:
- 栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
2.GC算法
-
标记-清除算法:标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。
- 标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。
- 清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
- 缺点:
- 效率低下(标记和清除都是遍历),标记和清除的条件都是在线程停止的情况下,效率低的话,交互性就很差
- 空闲内存不连续,因为清除的时候,清除对象哪里都是,清除后内存比较乱,JVM维护这种状态比较耗费内存
-
复制算法:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
- 与标记-清除算法相比,复制算法是一种相对高效的回收方法
- 不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC)
-
标记-整理算法: 如果在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选中这种算法。
- 标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
- 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
3.可触及性
- 从根节点可以触及到的对象,就是可触及对象。
- 一旦所有引用被释放,该对象也不是一定会收,而是处于可复活状态,因为在finalize()中可能复活该对象。
- finalize()方法是对象逃脱死亡的最后机会,只要在finalize()方法中重新与引用链上的任何对象建立关联即可。注意finalize()方法只会被虚拟机调用一次,如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过了,那么虚拟机将不会执行finalize()方法。
- 在finalize()后,如果仍然是从根节点不可触及的,则进入不可触及状态,不可触及的对象不可能复活,将被虚拟机回收。
注意:应避免使用finalize(),操作不慎可能导致错误。
4.Stop-the-World
- 由GC引起了一个关键性的问题,Stop-The-World,简称STW。它是Java中一种全局暂停的现象。全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互。这种现象多半由于GC引起的,此外Dump线程、死锁检查、堆Dump都有可能引起STW,但这几种情况一般都是人为触发。新生代的GC,也就是minorGC会比较短,一般在毫秒级。老年代的GC有时候也在零点几秒以内完成,但有时会很长,达到几秒、几十秒甚至更长,这主要取决于当时堆的实际情况。
类装载器(ClassLoader)
1.class装载验证流程
[外链图片转存失败(img-HfRkR5aQ-1563543065683)(https://raw.githubusercontent.com/FreeLonChan/ImgRepo/master/class装载验证流程.png)]
-
加载:读入字节码文件,取得类的二进制流,转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象
-
验证:验证字节码文件的正确性
- 1.文件格式的验证
- 是否以0xCAFEBABE开头
- 版本号是否合理
- 2.元数据验证
- 是否有父类
- 是否继承了final类
- 非抽象类实现了所有抽象方法
- 3.字节码验证
- 运行检查
- 跳转指令是否到合适的位置
- 4.符号引用验证
- 常量池中描述类是否存在
- 访问的方法或字段是否存在且有足够的权限
- 1.文件格式的验证
-
准备:在方法区中分配内存,并为类赋初始值
-
解析:符号引用替换为直接引用
- 符号引用,字符串引用对象不一定被加载
- 直接引用,指针或地址偏移量引用对象一定在内存中
- 在java中,一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类 的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,即直接引用地址
- 直接引用
public class StringAndStringBuilder{ public static void main(String[] args){ System.out.println ("s=" + "asdfa"); } }
- 符号引用
public class StringAndStringBuilder{ public static void main(String[] args){ String s="asdfa"; System.out.println ("s=" + s); } }
-
初始化:执行类构造器<clinit>,初始化static变量和方法
- <clinit>是线程安全的
- 子类的<clinit>调用前保证父类的<clinit>先调用
-
使用:
-
卸载:
2.什么是类加载器ClassLoader
- ClassLoader是一个抽象类
- ClassLoader的实例读入Java字节码将类加载到JVM中
- ClassLoader可以定制,满足不同字节码流的获取
- Classloader负责类装载阶段的加载阶段
3.JDK中ClassLoader默认设计模式
ClassLoader常用的几个方法:
defineClass(String name,java.nio.ByteBuffer b,ProtectionDomain protectionDomain)
//指定保护域(ProtectionDomain),把ByteBuffer的内容转化为Java类,这个方法是final的
defineClass(String name,byte[] b,int off,int len)
//把字节数组 b中的内容转换成 Java 类,其开始偏移为off,这个方法被声明为final的。
findClass(String name)
//查找指定名称的类
loadClass(String name)
//加载指定名称的类
resolveClass(Class<?>)
//链接指定的类
其中defineClass把字节流解析成能够识别的Class对象,通常和findClass一起使用,通过覆盖父类的findClass方法来实现类的加载规则,获取要加载类的字节码,然后调用defineClass方法来生成类的Class对象
ClassLoader的等级加载机制
BootStrapClassLoader:启动类加载器
加载层次中最顶层的类加载器,负责加载JDK中的核心类库,eg:rt.jar,resources.jar,charsets.jar等.需要加载什么都是由JVM自己控制
ExtClassLoader:扩展类加载器
负责加载Java的扩展库,JVM的实现会提供一个扩展库,根据目录查找所需加载出来,默认加载JAVA_HOME/jre/lib/ext的所有jar
AppClassLoader:系统类加载器
负责加载应用程序classpath下所有jar和class文件,一般Java应用的类都是它加载完成的
性能监控工具
1.系统性能监控
介绍一些常用的Linux命令
-
uptime:
- 查看当前系统时间
- 运行时间
- 连接数:终端连接个数
- 1,10,15分钟内的平均负载量
-
top:动态查看系统的运行情况
- -b:以批处理模式操作;
- -c:显示完整的治命令;
- -d:屏幕刷新间隔时间;
- -I:忽略失效过程;
- -s:保密模式;
- -S:累积模式;
- -i<时间>:设置间隔时间;
- -u<用户名>:指定用户名;
- -p<进程号>:指定进程;
- -n<次数>:循环显示的次数。
-
vmstat:操作系统的虚拟内存、进程、IO读写、CPU活动等进行监视
- -a:显示活动内页;
- -f:显示启动后创建的进程总数;
- -m:显示slab信息;
- -n:头信息仅显示一次;
- -s:以表格方式显示事件计数器和内存状态;
- -d:报告磁盘状态;
- -p:显示指定的硬盘分区状态;
- -S:输出信息的单位。
-
pidstat:用于监控全部或指定进程的cpu、内存、线程、设备IO等系统资源的占用情况(需要安装)
- -u:默认的参数,显示各个进程的cpu使用统计
- -r:显示各个进程的内存使用统计
- -d:显示各个进程的IO使用情况
- -p:指定进程号
- -w:显示每个进程的上下文切换情况
- -t:显示选择任务的线程的统计信息外的额外信息
- -T { TASK | CHILD | ALL }
- 这个选项指定了pidstat监控的。TASK表示报告独立的task,CHILD关键字表示报告进程下所有线程统计信息。ALL表示报告独立的task和task下面的所有线程。
- 注意:task和子线程的全局的统计信息和pidstat选项无关。这些统计信息不会对应到当前的统计间隔,这些统计信息只有在子线程kill或者完成的时候才会被收集。
- -V:版本号
- -h:在一行上显示了所有活动,这样其他程序可以容易解析。
- -I:在SMP环境,表示任务的CPU使用率/内核数量
- -l:显示命令名和所有参数
2.Java自带工具
-
jps:列出当前java进程
- -q 可以指定jps只输出进程ID,不输出类的短名称
- -m 可以输出传递给Java进程的参数
- -l 可以用于输出主函数的完整路径
- -v 可以显示传递给JVM的参数
-
jinfo:查看正在运行的Java程序的扩展参数,也可以修改部分参数
- -flag <name> 打印指定JVM的参数值
- -flag[+|-] <name> 设置指定JVM参数的布尔值
- -flag <name>=<value> 设置指定JVM的参数值
-
jamp:生成Java应用程序的堆快照和对象的统计信息
- -histo
- -dump:format=b,file=<address>
-
jstack:打印线程信息
- -l 打印所信息
- -m 打印java和native的帧信息
- -F 强制打印
-
Visual VM
Java堆分析
1.内存溢出(OOM)的原因
-
堆溢出:占用大量空间,直接溢出
- 关键字:java.lang.OutOfMemoryError:java heap space
- 解决方法:增大堆空间,直接释放内存
-
永久区溢出:生成的类过多,GC处理不过来
- 关键字:java.lang.OutOfMemoryError:PermGen space
- 解决方法:增大Perm区,允许class回收
-
栈溢出:创建线程的时候,给线程分配的空间请求不到所导致的
- 关键字:java.lang.OutOfMemoryError:unable to create new NativeSpread
- 解决方法:减小堆空间,减小线程栈大小
-
直接内存溢出:ByteBuffer.allocateDirect()无法从OS获取足够的空间
- 解决方法:减小堆内存,有意出发GC
2.MAT的使用
3.使用Visual VM分析堆
锁
1.线程安全
- 使用锁机制,只能同一时间只能让某一个线程访问数据,保证数据的一致性和不被污染
2.对象头Mark
- 对象头的标记,32位
- 描述对象的hash,锁信息,垃圾回收标记,年龄
- 指向锁记录的指针
- 指向moniter的指针
- GC标记
- 偏向锁线程ID
3.偏向锁
- 没有竞争的情况下,偏向锁可以提高性能
- 偏向就是偏心,偏向锁会偏向于当前已经占有锁的线程
- 将对象头Mark标记为偏向,线程ID存放在对象头中
- 只有没有竞争,获得偏向锁的线程,进入同步块中时,不需要做同步
- 默认开启
- 竞争激烈的时候,反而会增加系统负担
4.轻量级锁
- 轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗
5.自旋锁
- 当竞争存在时,如果线程能很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋)
- 如果同步块很长,自旋失败,降低系统性能
总结
- 偏向锁,轻量级锁,自旋锁都不是Java语言层面的锁优化方法
- 内置于JVM中获取锁的方法和步骤
- 偏向锁可用会先尝试偏向锁
- 轻量级锁可用先尝试轻量级锁
- 都失败时尝试自旋锁
- 再失败尝试普通锁,使用OS互斥量在OS中挂起
Java中提高锁性能的方法
1.减少锁持有时间
简单来就是说,把锁的放的范围缩小
public synchronized void syncMethod()
{
method1();
mutextMethod();
method2();
}
这里method1()和method2()并不需要加锁,锁加在方法上,会耗费很多时间。可改进为以下
public void syncMethod()
{
method1();
synchronized(this)
{
mutextMethod();
}
method2();
}
2.减小锁粒度
- 大对象拆分为小对象,大大增加并行度,降低竞争度,竞争度降低后,偏向锁和轻量级锁成功率会提高
- ConcurrentHashMap的底层实现就是基于这个减小锁粒度,把数据分成若干个Segment(段):Segment<K,V> [] segments,每一个segment对应一个锁,维护一个HashEntry<K,V>
- put操作时,先定位到Segment,锁定这个Segment,进行操作
- -减小颗粒度后,ConcurrentHashMap允许若干个线程同时进入
- 详情参考了ConcurrentHashMap原理
3.锁分离
- 根据功能进行锁分离
- 例如:ReadWriteLock
- 读多写少的情况下,可以提高性能
4.锁粗化
- 如果线程中对一个锁不断的请求,同步和释放,本身会很消耗系统资源,这时候不必减少锁持有时间
for(int i=0;i<n;i++) { synchronized(lock); } 反复请求,同步和释放,可改进为 synchronized(lock){ for(int i=0;i<n;i++) { } }
5.锁消除
- 在确定不可能被加锁的情况下,可以把锁取消掉
6.无锁
- 无锁是一种乐观的操作
- 无锁的一种实现方式
- CAS(compare and Swap)
class文件结构
1.语言无关性
java语言跟JVM没有直接的联系,是通过class文件实现关联的,但是能够生成class文件的不只java语言
2.文件结构
[外链图片转存失败(img-OVps7waL-1563543065684)(https://raw.githubusercontent.com/FreeLonChan/ImgRepo/master/class文件结构.jpg)]