深入了解JVM内核

深入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.符号引用验证
      • 常量池中描述类是否存在
      • 访问的方法或字段是否存在且有足够的权限
  • 准备:在方法区中分配内存,并为类赋初始值

  • 解析:符号引用替换为直接引用

    • 符号引用,字符串引用对象不一定被加载
    • 直接引用,指针或地址偏移量引用对象一定在内存中
    • 在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)]

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务