JVM底层原理分析 + 性能调优

基本概念

——JVM位置

JVM运行在操作系统之上的,与硬件没有直接的交互
在这里插入图片描述

——体系结构

在这里插入图片描述

1. 类装载器ClassLoader

  • 负责加载class文件,class文件在文件开头有特定的文件标示(cafe babe),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定

  • 并不是所有后缀为.class的文件都能被加载,只有在文件开头为cafe babe的文件才是唯一认证

  • 虚拟机自带的加载器

    • 启动类加载器(BootStrap):C++编写,出厂自带,java环境变量配置中的jre的bin目录中的所有.class文件,默认加载最早的类文件(ArrayList、Object)等,导包前缀为java
    • 扩展类加载器(Extension):java编写,后续版本迭代的类加载,导包前缀为javax
    • 应用程序类加载器(AppClassLoader):java也叫系统类加载器,加载当前应用的classpath的所有类,自己创建定义的类,应用时需要被加载
    • 启动类加载器 > 扩展类加载器 > 应用程序加载器(可通过.getclass().getClassLoader().getParent()验证 )
  • jre / bin / rt.jar(runtime.jar)/ sun.misc.Launcher是java虚拟机的入口

双亲委派机制

  • 有事往上桶(沙箱安全机制)
  • 当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
  • 采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。
  • 类加载从自顶向下加载,当加载一个用户定义的对象时(应用程序加载器),先找启动类加载器,没找到再找扩展类加载器,没找到再找应用程序加载器,还没有的话报ClassNotFound异常

2. 运行时数据区

Native Interface本地接口(本地方法栈)

  • 本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies
  • native关键字:存在于(native)本地方法栈,调用底层的C语言函数库

PC寄存器

  • 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也就是将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,可以忽略不计
  • 这块内存区域很小,它是当前线程所要执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来读取下一条需要执行的字节码指令
  • 如果执行的是一个Native方法,那这个计数器就为空
  • 用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生OOM(内存溢出)错误

方法区

  • 供每个线程共享的运行时内存区域
  • 存储了每个类的结构信息,运行时的常量池,字段,方法数据,构造方法,普通方法
  • 实例变量存储在堆内存中,和方法区无关

  • 包含 局部变量表操作数栈动态链接方法出口
  • 栈也叫栈内存,栈管运行,堆管内存,先进后出
  • 在创建线程时创建,它的生命周期跟随线程的生命周期,线程结束栈内存释放
  • 对于栈来说不存在垃圾回收,线程私有,只要线程结束该栈就结束
  • 存储:8种基本数据类型的变量+对象的引用变量+实例方法都是在栈内存中分配
  • 每个方法执行的过程中都会创建一个栈帧(在JVM中叫栈帧,JVM外叫方法),用于存储局部变量表,操作数栈,动态链接,方法出口信息;栈的大小通常在256k~756k左右

堆(重点)

  • 一个JVM只存在一个堆内存,堆内存的大小是可以调节的;类加载器读取了类文件后,需要把类、方法、常量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行

  • 堆内存逻辑上分为三部分(物理上只有新生代+老年代两部分),新生代+老年代+元空间(java1.7称为永久代,1.8后称为元空间)

  • 元空间和永久代最本质的区别为:永久代使用的是JVM的堆内存,元空间使用的是本机的物理内存;因此,元空间的大小仅受本地内存限制
    在这里插入图片描述

  • 当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC,轻量GC也叫YGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁

  • 若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”(堆内存溢出)

  • 问题(重点):如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够,原因以下:

    • Java虚拟机的堆内存设置的不够,可通过-Xms、-Xmx来调整
    • 代码中创建了大量的大对象,并且长时间不能被垃圾收集器收集(大对象一直处于被引用的状态)
  • 从GC角度细分
    在这里插入图片描述

    • YGC(轻GC)的过程:复制——》清空——》互换
    1. eden、SurvivorFrom 复制到 SurvivorTo,年龄+1
      首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到S0urvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1
    2. 清空 eden、SurvivorFrom
      然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to区
    3. SurvivorTo和 SurvivorFrom 互换
      最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

内存管理

在这里插入图片描述

  • 不同对象的生命周期不同,98%的对象都是临时对象
  • 实际上,方法区和堆一样,是各个线程共享的内存区域,用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码;JVM规范将方法区描述为堆的一个逻辑部分,但它有个别名叫非堆,目的就是要和堆分开
  • 大多习惯称方法区就是永久代,严格本质上两者有所不同,方法区是一个规范、概念,而永久代是方法区的一个实现
  • 永久存储区是一个常驻内存区域,存放JDK自身携带的Class,Interface的元数据,存储的是运行环境必须的类信息,加载的就是rt.jar包所有的对象,被装载进此区域的数据是不会被垃圾回收器回收掉,关闭JVM才会释放此区域所占用的内存

GC Roots

  • GC Root 表示当前存活的对象(包括 局部变量表静态变量常量本地方法栈 中的对象)
  • JVM 中判断一个对象是否标记为可回收的对象是根据 可达性分析算法(以 GC Roots 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象)

3.执行引擎

主要由三部分组成

  • 解释器(Interpreter) :默认情况下HotSpot采用的是 JIT编译器 和 解释器 并存的架构
  • 即时编译器(Just-In-Time Compiler):在HotSpot中内嵌了两个JIT编译器,分别为Client Compiler和Server Compiler,一般简称为C1和C2编译器
  • 垃圾收集器(Garbage Collection):Serial(GC)、Serial Old、ParNew、【Parallel Scavenge(清除)、Parallel Old】(JDK8 默认)、CMS、G1(JDK9 默认)

4. 四大引用类型

  • 强引用:当内存不存,JVM 开始垃圾回收,对于强引用的对象,就算出现了 OOM 也不会对该对象进行回收;最常见的普通对象引用,只要还有强引用指向一个对象,就表明对象活着,除非为null。因此强引用是造成内存泄露的主要原因之一
  • 软引用:相对强引用弱化,用java.lang.ref.SoftReference实现,通常用在对内存敏感的程序中,比如高速缓存就需要软引用;JVM 内存充足时它不会被回收,不足时会被回收
  • 弱引用:用java.lang.ref.WeakReference实现,比软引用的生存期更短,只要垃圾回收机制一运行, 不管JVM 的内存空间是否足够,都会被回收
    • 软引用 和 弱引用的适用场景:读取大量的本地图片
      • 如果每次读取图片从硬盘读取影响性能;一次性全部加载到内存中可能造成内存溢出
      • 软引用可解决该问题:用 HashMap<String,SoftReference<BitMap>> 来保存图片的路径和响应的图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效避免 OOM 的问题
  • 虚引用:用java.lang.ref.PhantomReference实现
    • 必须和引用队列(ReferenceQueue)联合使用,形同虚设;一个对象仅持有虚引用,那么它和没有任何引用一样,在任何时候都可能被垃圾回收器回收
    • PhantomReference 的get方法总是返回 null,因此无法访问对象的引用对象
    • 设置虚引用的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理

——性能调优

STW:Stop-The-World

  • 是在垃圾回收算法的执行过程中,将 JVM内存冻结、应用程序停顿 的一种状态
  • 在STW 状态下,JAVA的所有线程都是停⽌执⾏的,GC线程除外
  • STW是不可避免的,垃圾回收算法执⾏一定会出现STW,我们要做的只是减少停顿的时间
  • 减少STW(暂停)降低GC垃圾回收的频率 是调优的重点
  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
  • 如果系统卡顿很明显,大概率就是频繁执行GC垃圾回收,频繁进入STW状态产生停顿的缘故

常见诊断工具

  • Java Visual VM(JVisual VM),可安装插件(Visual GC)
  • 阿里调优工具 Arthas

调优命令:可在运行前的VM -options中进行调整JVM的内存大小
(-Xms1024m -Xmx1024m -XX:+PrintGCDetails)

  • -Xms:设置初始分配大小,默认为物理内存的1/64
  • -Xmx:最大分配内存,默认为物理内存的1/4
  • -XX:+PrintGCDetails:输出详细的GC日志

基本原则

  • 设置的初始值应该和最大值一致,避免内存忽高忽低,产生停顿,而且GC与最大值无关
  • 减少GC回收次数,因为GC回收本身会影响程序的效率

GC

次数上频繁收集新生代,较少收集老年代,基本不动元空间

四大算法

  1. 引用计数法:
    缺点:每次对对象赋值时要维护计数器,较难处理循环利用,一般不采用该方式
  2. 复制算法(复制——交换——清空,发生在新生代)
    Eden:From:To为8:1:1的原因,就是From到To的内存大小一致才可实现完全复制,不产生内存碎片
    缺点:当对象的存活率较高时,极大的占用内存,存活率较低使用
  3. 标记清除(发生在老年代):
    为了弥补复制算法浪费空间的缺点,先标记,再清除,节约空间;
    缺点:效率低,清除的地方会产生内存碎片
  4. 标记整理(发生在老年代,与标记清除混合使用):
    弥补标记清除所产生的内存碎片,标记的对象将会被整理,未标记的将会被清除,减少产生的内存碎片
    缺点:耗时长,效率低

在这里插入图片描述

补充

  • 内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)
  • 内存整齐度:复制算法=标记整理算法>标记清除算法
  • 内存利用率:标记整理算法=标记清除算法>复制算法
  • 没有最好的算法,只有最合适的算法,需要通过分代来选择对应的回收算法

——常用参数

  • -Xms:初始堆内存大小,默认物理内存64/1
    -Xms = -XX:InitialHeapSize
  • -Xmx:最大堆内存,默认物理内存4/1
    -Xmx = -XX:MaxHeapSize
  • -Xss:栈内存大小
    -Xss = -XX:ThreadStackSize
    设置单个线程栈大小,一般默认512~1024kb,单个线程栈大小跟操作系统和JDK版本都有关系
  • -Xmn:年轻代大小
  • -XX:MetaspaceSize:元空间大小
    元空间本质跟永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间并不在虚拟机中,而是使用本机内存。因此,元空间大小仅受本地内存限制。
  • -XX:+PrintGCDetails:打印GC详细日志信息
  • -XX:SurvivorRatio:新生代比例设置(默认Edem : from : to = 8 : 1 : 1 )
  • -XX:NewRatio:新生代和老年代比例设置(默认1:2)
  • -XX:MaxTenuringThreshold:进入老年代阈值设置

一台 java 服务器可以跑多少个线程?

  • 公式:线程数量=(机器本身可用内存 - JVM分配的堆内存)/ Xss的值
  • (不考虑系统限制)假设我们的容器本身大小是8G,堆大小是4096M,走 -Xss 默认值,可以得出最大线程数量:4096个
  • 结论1:jvm堆越大,系统创建的线程数量越小
    结论2:当-Xss的值越小,可生成线程数量越多
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序少年不秃头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值