JVM虚拟机

JVM规范官方文档

JVM是一个跨语言的平台,加载字节码文件,翻译成不同系统的机器指令进行运行

JVM.jpg

字节码文件可视化,可以在 idea-plugins 安装【jclasslib bytecode viewer】
通过idea-view-show bytecode with jclasslib 查看

class文件结构:
ClassFile {
    u4             magic;    (魔数,识别class文件格式 0xCAFEBABE) 4字节 
    u2             minor_version; (小版本) 2字节 
    u2             major_version; (大版本) 2字节
    u2             constant_pool_count; (常量池计数器) 2字节
    cp_info        constant_pool[constant_pool_count-1]; (常量池表) n字节
    u2             access_flags; (访问标识) 2字节
    u2             this_class; (类索引) 2字节
    u2             super_class; (父类索引) 2字节
    u2             interfaces_count; (接口计数器) 2字节
    u2             interfaces[interfaces_count]; (接口索引集合) 2字节
    u2             fields_count; (字段计数器) 2字节
    field_info     fields[fields_count]; (字段表) n字节
    u2             methods_count;  (方法计数器) 2字节
    method_info    methods[methods_count]; (方法表) n字节
    u2             attributes_count; (属性计数器) 2字节
    attribute_info attributes[attributes_count]; (属性表) n字节
}

包装类对象缓存范围

包装类 缓存范围
Byte -128~127
Short -128~127
Integer -128~127
Long -128~127
Float 没有
Double 没有
Character 0~127
Boolean true、false
类的加载过程:
类的加载过程.jpg

加载(Loading):

  1. 通过本地磁盘/网络获取class文件
  2. 在【运行时数据区-元空间】生成这个Class类信息 (会在验证通过后执行)

链接(Linking):

  • 验证(Verification)
    验证class文件是否合法,例如魔数检查,开头0xCAFEBABE;语义校验等。
  • 准备(Preparation)
    给类中的静态变量分配内存,并赋值默认值 (如果是static final修饰的,就不会赋值默认值,直接赋值定义的值)
  • 解析(Resolution)
    把类、接口、字段和方法的符号引用转为直接引用。 (例如:常量池中的字面量引用,将字面量的对象加载到内存后,就有真实的内存地址了,就可以引用真实的内容地址)

初始化(Initialization):
给类的静态变量赋予定义的值。就是执行<clinit>()方法 (如果有static修饰的类变量或者static静态代码块,jvm才会自动生成一个<clinit>()方法 父类的static块优先级高于子类;如果[静态变量没有显示赋值],或者[final修饰并且是常量]就不会生成<clinit>())


Class.forName("")和Class.getClassLoader().loadClass("")区别是?
Class.forName会Loading和Initialization。 Class.getClassLoader().loadClass只会Loading

类加载器的分类:
类加载器关系.png

启动类加载器(Bootstrap ClassLoader):

  1. C/C++实现,用来加载java核心库:JAVA_HOME/jre/lib/rt.jar
  2. 只加载java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader):

  1. java实现,继承ClassLoader类
  2. 负责加载JAVA_HOME/jre/lib/ext或java.ext.dirs路径类库

应用类加载器(Application ClassLoader):

  1. java实现,继承ClassLoader类
  2. 负责加载java.class.path路径类库
  3. 它是应用中默认的类加载器
双亲委派机制:
双亲委派.png

避免了类的重复加载,防止核心类库被篡改。

子类加载器可以访问父类加载器的Class,反之不行; 只有类路径和类名一致并且类加载器一致,才能保证类唯一

运行时内存结构:
运行时数据区.png

java -XX:+PrintFlagsFinal -version:查看所有JVM的最终值 HotSpot VM参数文档

  • 程序计数器:存储将要执行下一条指令的地址,防止CPU时间片切换后重复执行。它是线程私有的
  • 栈:先进后出的特点,栈里面存放着一个个栈帧,一个栈帧对应一个java的方法。由于栈分配的内存比较小,所以它主要负责运行指令集,存放一些基本数据类型,通过引用进行创建/修改堆空间的数据。
    栈不存在GC,但存在OOM
    -Xss1m:设置栈大小
    栈帧:对应一个java的方法
    1.局部变量表:定义为一个数组,用于存储方法参数和方法体内的局部变量;如果是非static修饰的方法,索引[0]下标保存的就是this;double和long占用两个slot,也就是索引下标占两个
    2.操作数栈:用于保存计算过程的中间结果以及计算过程中变量的临时存放
  • 堆:负责对象的存储,所有的对象实例以及数组都应当在堆上。 可以处在物理上不连续的内存空间中;堆中Eden区会有一块TLAB区域,它是线程私有的
    堆存在GC,存在OOM
    -Xms512m:设置堆空间起始大小(默认物理内存的1/64)
    -Xmx512m:设置堆空间最大大小(默认物理内存的1/4)
    通常-Xms和-Xmx配置相同的值,避免清理完堆后不需要重新计算堆区大小。
    年轻代:它又分为Eden区和S0(from)、S1(to)区;存放一些生命周期比较短的对象,创建和消亡都非常迅速。
    -Xmn512m:设置年轻代大小
    老年代:生命周期非常长的对象
    -XX:NewRatio=2:设置新生代与老年代的占比
    -XX:SurvivorRatio=8:设置Eden与S0、S1区占比
  • 元空间(1.8):使用堆外直接内存,存放.Class类结构信息、方法信息、常量、JIT热点代码缓存
    -XX:MetaspaceSize=1024m:设置元空间初始大小
    -XX:MaxMetaspaceSize=-1:设置元空间最大大小

为什么要分老年代年轻代?
年轻代对象特点是生命周期很短,老年代对象特点是生命周期很长,如果混到一起,每次触发GC,大对象寻找又很慢,对象又很多,每次都会浪费一部分不需要GC的对象寻找时间。GC会暂停用户线程,造成响应慢

对象分配过程:
对象分配过程.jpg

1.new对象会先放到年轻代的Eden区,如果Eden满了,放不下了,会触发YGC进行垃圾回收。通过可达性分析,找出还是引用的对象,放到to区,并且年龄=1,如果from区还有对象,也会找出还在引用的对象,放到to区,并且年龄+1;
2.当S区的存活对象反复计数,达到设定的年龄阈值(默认是15),就会将对象放到老年代中;或者对象太大,Eden区YGC后还是放不下,就会直接放到老年代;如果老年代也放不下,就会触发FGC,最后还是放不下,直接抛出OOM异常

-XX:MaxTenuringThreshold=15:设置对象S区到老年代的年龄阈值【最大就15,因为对象头中分代年龄就4bit,最大就1111,转换十进制就是15】

对象实例创建过程:
例如通过代码:Object obj = new Object();
对应的字节码指令:
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init> : ()V>
7 astore_1
8 return
对象实例创建过程.jpg
对象实例结构组成:
对象结构.png
MarkWord.png

HotSpot JVM默认使用混合模式(解释器+JIT及时编译器)
解释器:逐行解释字节码指令成为可执行的机器指令
JIT编译器:寻找热点代码,将对应的机器指令缓存到元空间中,减少重复的解释耗时,提升性能;
哪些代码才符合热点代码?
当调用某个方法的次数超过阈值
-XX:CompileThreshold=10000:设置JIT缓存方法调用次数阈值(-client模式默认1500次,-server模式默认10000次)

垃圾回收器(GC):

哪些对象需要回收?怎么判断这个对象是垃圾?
就是在运行中没有任何引用指向这个对象,没有人用这个对象,就应该被回收掉。

  • 可达性分析:通过根Roots节点对象寻找引用的对象,避免了循环引用,内存泄漏。为了保证一致性,GC进行中会STW,暂停用户线程,防止产生新的Root和引用
    Roots节点对象有哪些?
    1.栈中引用的对象
    2.类的静态属性引用的对象
    3.元空间中常量引用的对象
    4.被同步锁synchronized持有的对象
    5.jvm内部的引用对象
    可达性分析.jpg

存活对象找到后,怎么回收垃圾对象呢?

  • 标记-清除算法:通过可达性分析,将存活对象进行标记,再次全堆遍历未标记的垃圾对象记录到空闲内存地址列表中
    缺点:效率低,遍历两次;内存空间不连续,碎片化

    标记-清理.png

  • 复制算法:通过可达性分析,将存活对象按序直接复制到另一块内存空间,不会出现碎片化问题
    缺点:需要两倍内存空间,引用地址的变动也要联动,对象不能太多,越多越耗时。年轻代S0和S1区就是用的复制算法

    复制算法.png

  • 标记-整理算法:通过可达性分析进行标记,将所有存活的对象从内存的一端按顺序移动,之后清理另一端所有;避免了两倍内存的开销。通常老年代用的就是此算法,所以要尽可能减少FGC
    缺点:效率低于复制算法

    标记-整理.png

对象的引用类型:

  • 强引用(普通对象默认强引用):只要还被引用着,对象GC不回收,直到OOM
  • 软引用(SoftReference):内存不足,发生OOM之前,会将软引用对象回收
  • 弱引用(WeakReference):只要触发GC就会回收掉
  • 虚引用:对象回收跟踪
  • 终结器引用:用于实现对象的finalize();

内存溢出(OOM):分配一个超大对象,JVM判定GC后也不可能放下,就直接OOM或者GC回收后,还是没有可分配的内存放置创建的对象,也会出现内存溢出。
内存泄漏:这里的泄漏并不是说可达性分析没找到而导致的,而是本身应该视为垃圾,但在逻辑中引用着,无法释放

有哪些垃圾回收器?特点是什么?

GC分类.jpg

JDK8默认使用Parallel Scavenge GC和Parallel Old GC
JDK9默认使用G1 GC

  • Serial GC:使用复制算法,串行回收,暂停所有用户线程。

  • Serial Old GC:使用标记-整理算法,串行回收,暂停所有用户线程。
    适用场景:比如运行在客户端模式client,配置不算高,单个CPU还省去了线程交互的开销
    -XX:+UseSerialGC:设置年轻代老年代串行垃圾回收器

  • ParNew GC:使用复制算法,并行回收,暂停所有用户线程。在多核配置下,大大提升了吞吐量。
    -XX:+UseParNewGC:设置年轻代并行垃圾回收器
    -XX:ParallelGCThreads:设置并行线程数量

  • Paraller Scavenge GC:使用复制算法,并行回收,暂停所有用户线程。在多核配置下,大大提升了吞吐量。

  • Paraller Old GC:使用标记-整理算法,并行回收,暂停所有用户线程。
    -XX:+UseParallerGC:设置年轻代并行垃圾回收器
    -XX:+UseParallerOldGC:设置老年代并行垃圾回收器
    -XX:GCTimeRatio:垃圾收集时间占总时间的比例 (1/(N+1)) ;默认99,垃圾回收的时间不超过1%。
    -XX:+UseAdaptiveSizePolicy:开启自适应调节,年轻代Eden/S0/S1比例自动调节,以及S区年龄阈值自动调节等。

  • CMS GC:使用标记-清除算法,并发回收,与用户线程交替执行,主打低延迟。
    初始标记(STW):只标记与GC Roots直接关联的对象。会暂停用户线程
    并发标记:从GC Roots直接关联的对象寻找间接关联的对象
    重新标记(STW):修正并发标记阶段,用户线程并发执行,导致数据不一致性
    并发清理:清理未标记的对象;因为并发标记和并发清理阶段,所以无法清理浮动垃圾.
    -XX:+UseConcMarkSweepGC:设置老年代代并发垃圾回收器
    -XX:CMSInitiatingOccupanyFraction:设置堆内存使用率阈值,CMS并不是放不下了对象的时候才触发垃圾回收。
    -XX:+UseCMSCompactAtFullCollection:CMS GC后,开启内存碎片整理,但是会STW。
    -XX:CMSFullGCsBeforeCompaction:多少次FGC后,才对内存碎片进行整理。

  • G1 GC:将堆空间分成若干个区域(Region),逻辑上区分年轻代和老年代。侧重点回收垃圾最大量的区域(Region),后台维护一个区域(Region)优先级列表;Region之间采用复制算法,对整堆进行位置整理。
    -XX:+UseG1GC:设置年轻代和老年代代垃圾回收器。
    -XX:G1HeapRegionSize:设置每个Region大小。2的幂
    -XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间,但不能完全保证,默认200ms
    -XX:InitiatingHeapOccupancyPercent:堆的使用率超过比例,触发GC,默认45
    -XX:ConcGCThreads:并发标记的线程数

GC日志监控与分析:

-XX:+PrintGCDetails:打印GC回收时堆和元空间的详细信息
-XX:+PrintGCDateStamps:输出发生GC的时间
-Xloggc:/data/gc.log:把GC日志输出到指定文件中
-XX:+UseGCLogFileRotation:打开GC日志文件滚动覆盖
-XX:NumberOfGCLogFiles:设置滚动日志文件的个数
-XX:GCLogFileSize:设置滚动日志文件的大小
例如:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/data/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1m

jstat -gc <pid> 1000 20 (通过jdk自带命令工具每隔1秒打印一次gc情况,总共打印20次)

arthas在线分析 官网
java -jar arthas-boot.jar <pid>

arthas.png

还可以导出dump文件进行离线分析:

  • 手动导出方式:
    jmap -dump:live,format=b,file=<name.hprof> <pid>
  • 自动导出方式(出现OOM后):
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=<name.hprof>

使用工具进行分析dump和展示图表:

public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true){
            Object obj = new Object();
            list.add(obj);
        }
    }

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  • jvisualvm(jdk自带的):
jvisualvm概要.png

jvisualvm类实例统计.png
  • Eclipse Memory Analyzer


    eclipse疑似泄漏分析.png

    eclipse泄漏分析详情.png

    eclipse线程.png
如何合理的设置堆空间大小?

设置过大,就会堆积很多对象,GC时间就比较长;设置过小,就会频繁触发GC

空间 命令 推荐值
整堆(heap) -Xms和-Xmx 设置为老年代FGC后存活对象大小的3-4倍
元空间(metaSpace) -XX:MetaspaceSize和-XX:MaxMetaspaceSize 设置为老年代FGC后存活对象大小的1.2-1.5倍
年轻代(young) -Xmn 设置为老年代FGC后存活对象大小的1-1.5倍

获取老年代FGC后存活对象大小:打开GC日志,通过FGC后的空间大小,取多次的平均值作为参考值。
手动触发FGC:jmap -dump:live,format=b,file=<name.hprof> <pid>或者jmap -histo:live <pid>

CPU飙高如何排查?
  1. 通过top命令根据cpu排序,看看哪个java进程占用较高
    或通过ps aux | grep java 查找跟java相关的进程
    最终得到进程ID(pid)


    top命令.png

    psaux.png
  2. top -Hp <pid> :查看进程的飙高的线程id


    异常线程id.png
  3. 将飙高的线程id十进制——>>十六进制
    2482 -> 0x9b2
  4. jstack <进程的pid> | grep -A20 0x9b2


    jstack线程信息.png

    定位到可能存在问题的代码位置或者是垃圾回收线程占用的问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 上篇(JVM内存与垃圾回收篇)大纲 JVM与Java体系结构 类加载子系统 运行时数据区概述及线程 程序计数器 虚...
    Wjun阅读 418评论 0 1
  • 未完待续…… 帧数据区:JVM 类加载子系统 运行时数据区程序计数器虚拟机栈本地方法接口本地方法栈堆方法区直接内存...
    鸡龙阅读 507评论 0 0
  • 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。 前面我们学习了Clas...
    tracy_668阅读 264评论 0 0
  • 一、运行时数据区域 程序计数器Java 虚拟机栈本地方法栈堆方法区运行时常量池直接内存 二、垃圾收集 判断一个对象...
    Java机械师阅读 737评论 0 1
  • 来源:博客园 作者:bojiangzhou[https://www.cnblogs.com/chiangchou/...
    小郭子阅读 189评论 0 0