运行时内存模型

image.png
image.png
image.pngimage.png

方法区

  • 7常量池在方法区,8移动至元空间
  • 方法区只是一种规范或标准。永久代或者元空间是方法区的实现。

    常量池

  • Java 中常量池包括运行时常量池、字符串常量池和类文件常量池,字符串常量池在堆内部,运行时常量池在元数据

  • 主管程序的允许,生命周期和线程同步。

  • 压栈、栈帧。栈帧=局部变量表+操作数栈+父、子指针。
  • 不存在垃圾回收过程,线程结束栈内存就释放。
  • 基本数据类型+对象 +实例方法索引。

image.png

  • 调优和垃圾回收的主要区域。
  • 一个JVM只有一个堆内存。
  • 类、实例、方法、常量、变量。
  • 分为新生代/伊甸园(又分幸村区/Suriver0、1)、老年代old、永久区。

image.png
image.png

新生代、老年代

  • 新生代—>幸存区(过度)—>老年代(默认15次回收)
  • 新生区
    • 类:诞生、成长、死亡。
    • 伊甸园:所有的对象都是在这ne出来的。
    • 幸存区(0,1)
    • 默认伊甸园:from:to = 8:1:1
    • 默认新生代:老年代=2:1
  • 经研究,99%对象都是临时对象XD。

    永久区

  • 几乎不存在垃圾回收,常驻内存,用来存放JDK自身携带的Class对象信息、接口元数据,存储的是java运行时的一些环境或类信息。关闭JVM会释放。

  • JDK6:永久代,字符常量池在方法区。
  • JDK7:永久代,字符常量池在堆,运行时常量池在方法区。
  • JDK8:永久代被元空间代替,元空间不在堆中,不与堆相连与堆共享本地内存,默认无大小限制,可参数控制大小。

    JMM(java线程内存模型)

    CPU样例图

    image.png

  • volatile 保证内存可见性(不同线程的内存副本可见),防止指令重排,不是原子性。

  • 主内存只有一个,每个线程有一个拷贝,数据一致性问题。
    • 新的变量必须在主内存中诞生,一个变量同时只有一个线程能对其lock ,多次lock必须指向相同次数的unlock才能解锁。

image.png
JAVA基础--虚拟机 - 图10

八种jvm原子操作

  • read(读取):从主内存读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • use(使用):从工作内存读取数据来计算
  • assign(赋值):将计算好的值重新赋值到工作内存中
  • store(存储):将工作内存数据写入主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定):将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
    • 数据不一致解决:
  • 早期:总线加锁,违背多核多线程初衷。
  • MESI缓存一致性协议。当线程修改通过总线写回主内存,其他线程的cpu总线嗅探机制见识到,使线的工作内存对应数据失效,若线程使用该数据,重新读取主内存。


类加载过程

  • 加载—>链接(验证—>准备—>解析)—>初始化
  1. 加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的Class对象,作为方法去这个类的各种数据的访问入口
  2. 验证:验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟自身的安全。
  3. 准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法去中进行分配。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
  4. 解析:解析阶段是虚拟机将常量池内的符号(Class文件内的符号)引用替换为直接引用(指针)的过程。
  5. 初始化:初始化阶段是类加载过程的最后一步,开始执行类中定义的Java程序代码(字节码)。

JAVA基础--虚拟机 - 图11
image.png

加载

  • 启动类加载器 Boot Strap ClassLoader
  • 扩展类加载器 Extense
  • 应用程序类加载器 Application
  • 加载器JAVA基础--虚拟机 - 图13

    连接

    初始化

    双亲委派机制

  • 保证安全

  • 初始化对象时,类加载时向上委托APP—>EXC—>BOOT依次寻找Boot—>EXC—>APP,能加载就加载,使用当前加载器,否则就交由子类加载器加载,最终执行,若都没有class not found异常。
  • 若新建java.lang.String,BOOT加载器会在tr.jar找到,使用rt包的类。新建无效。

    垃圾回收

    image.png

    可达性分析法(对象存活判断)

    在进行垃圾回收是,首先会对对象进行可达性分析,若果发现对象没有与 GC Roots 相连接的链路,将会对对象进行第一次标记。然后看标记的对象是否覆盖了 finalize 方法或者 finalize 方法已经被虚拟机调用过了,如果没有覆盖或者已经被调用过了,则直接回收,如果没有则将对象放入到 F-Queue 中主备通过 Finalizer 线程去执行这些方法,在执行完之后,垃圾收集器会对对象进行二次标记,如果对象依旧北邮被 GC Roots 引用,则对象会被回收。
    finalize 方法只能被 jvm 调用一次,且对象被标记后只有通过 finalize 方法来逃脱,且只有一次逃离机会。

    复制算法

    image.png

    标记清除算法

    image.png

    GC
  • 轻量级(Minor GC针对新生代)、重量级(默认达到老年代80%,针对老年代Full GC,全局GC)。

  • 主要针对新生代和老年代。

    • 四大算法
      • 标记清除
      • 标记整理
      • 分代收集算法
      • 复制算法
      • 此外还有可达性分析法、引用计数法。
    • 引用计数(太LOW):对象被引用计数器加一,引用结束减一,计数器为0可回收。
    • 复制算法:保证有一个Survive为空,叫survive to, 每次GC伊甸园移动至Survive to,from也会移动至to,然后from空,名称互换。若15次(默认)还没有被回收在幸存区,就会移动至老年区。
      • 优点:没有内存碎片
      • 缺点:浪费内存空间,有一半Survive永远为空。
      • 最佳场景:对象存活的较低(新生代)。
      • 极端场景:100%对象存活度。
    • 标记清除法:回收:扫描,标记没有引用的对象;清除:清除没有标记的对象。
      • 优点:不需要额外空间。
      • 缺点:两次扫描,浪费时间;内存碎片。
      • 极端:所有都被标记,内存爆炸。
      • 优化:标记压缩/整理:整理碎片,多了移动成本。再优化:标记—>清除—>压缩/整理。
        总结
  • 算法

    • 内存效率:复制 > 标记清除 > 标记整理
    • 内存整齐:复制 = 标记压缩 > 标记清除
    • 内存利用率:标记压缩 = 标记清除> 复制
  • 没有最优,只有最合适
  • GC分代收集算法
    • 新生代:
      • 存活率低:复制算法。
    • 老年代:
      • 存活率高:标记清楚+标记整理混合。

JIT

JIT概念JIT:Just In Time Compiler,一般翻译为即时编译器,这是是针对解释型语言而言的,而且并非虚拟机必须,是一种优化手段,Java的商用虚拟机HotSpot就有这种技术手段,Java虚拟机标准对JIT的存在没有作出任何规范,所以这是虚拟机实现的自定义优化技术。 HotSpot虚拟机的执行引擎在执行Java代码是可以采用【解释执行】和【编译执行】两种方式的,如果采用的是编译执行方式,那么就会使用到JIT,而解释执行就不会使用到JIT,所以,早期说Java是解释型语言,是没有任何问题的,而在拥有JIT的Java虚拟机环境下,说Java是解释型语言严格意义上已经不正确了。 HotSpot中的编译器是javac,他的工作是将源代码编译成字节码,这部分工作是完全独立的,完全不需要运行时参与,所以Java程序的编译是半独立的实现。有了字节码,就有解释器来进行解释执行,这是早期虚拟机的工作流程,后来,虚拟机会将执行频率高的方法或语句块通过JIT编译成本地机器码,提高了代码执行的效率,至此你已经了解了JIT在Java虚拟机中所处的地位和工作的主要内容。 1.JIT的工作原理图
JAVA基础--虚拟机 - 图17
image.png
工作原理
当JIT编译启用时(默认是启用的),JVM读入.class文件解释后,将其发给JIT编译器。JIT编译器将字节码编译成本机机器代码。
通常javac将程序源码编译,转换成java字节码,JVM通过解释字节码将其翻译成相应的机器指令,逐条读入,逐条解释翻译。非常显然,经过解释运行,其运行速度必定会比可运行的二进制字节码程序慢。为了提高运行速度,引入了JIT技术。
在执行时JIT会把翻译过的机器码保存起来,已备下次使用,因此从理论上来说,採用该JIT技术能够,能够接近曾经纯编译技术。 2.相关知识
JIT是just in time,即时编译技术。使用该技术,可以加速java程序的运行速度。
JIT并不总是奏效,不能期望JIT一定可以加速你代码运行的速度,更糟糕的是她有可能减少代码的运行速度。这取决于你的代码结构,当然非常多情况下我们还是可以如愿以偿的。
从上面我们知道了之所以要关闭JITjava.lang.Compiler.disable(); 是由于加快运行的速度。由于JIT对每条字节码都进行编译,造成了编译过程负担过重。为了避免这样的情况,当前的JIT仅仅对常常运行的字节码进行编译,如循环等

JDK命令行工具

  • jps(虚拟机进程状况工具):jps可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称 以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。
  • jstat(虚拟机统计信息监视工具):jstat是用于监视虚拟机各种运行状态信息的命令行工 具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jinfo(Java配置信息工具):jinfo的作用是实时地查看和调整虚拟机各项参数。
  • jmap(Java内存映像工具):命令用于生成堆转储快照(一般称为heapdump或dump文 件)。如果不使用jmap命令,要想获取Java堆转储快照,还有一些比较“暴力”的手段:譬如 在第2章中用过的-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出 现之后自动生成dump文件。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永 久代的详细信息,如空间使用率、当前用的是哪种收集器等。
  • jhat(虚拟机堆转储快照分析工具):jhat命令与jmap搭配使用,来分析jmap生成的堆 转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在 浏览器中查看。
  • jstack(Java堆栈跟踪工具):jstack命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈 的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循 环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿 的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些 什么事情,或者等待着什么资源。

    调优参数

  • 调优参数———-

  • Xms2g:初始化推大小为 2g;#-Xmx2g:堆最大内存为 2g;
  • XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • XX:+PrintGC:开启打印 gc 信息;#-XX:+PrintGCDetails:打印 gc 详细信息。
  • XX:+HeapDumpOnOutIfMemoryError:

    JVM调优实战

  1. 外部命令导致系统缓慢
    一个数字校园应用系统,发现请求响应时间比较慢,通过操作系统的mpstat工具发现CPU使用率很高,并且系统占用绝大多数的CPU资 源的程序并不是应用系统本身。每个用户请求的处理都需要执行一个外部shell脚本来获得系统的一些信息,执行这个shell脚本是通过Java的 Runtime.getRuntime().exec()方法来调用的。这种调用方式可以达到目的,但是它在Java 虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时创建进程 的开销也非常可观。Java虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一 样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执 行这个操作,系统的消耗会很大,不仅是CPU,内存负担也很重。用户根据建议去掉这个Shell脚本执行的语句,改为使用Java的API去获取这些信息后, 系统很快恢复了正常。
  2. 由Windows虚拟内存导致的长时间停顿
    一个带心跳检测功能的GUI桌面程序,每15秒会发送一次心跳检测信号,如果 对方30秒以内都没有信号返回,那就认为和对方程序的连接已经断开。程序上线后发现心跳 检测有误报的概率,查询日志发现误报的原因是程序会偶尔出现间隔约一分钟左右的时间完 全无日志输出,处于停顿状态。
    因为是桌面程序,所需的内存并不大(-Xmx256m),所以开始并没有想到是GC导致的 程序停顿,但是加入参数-XX:+PrintGCApplicationStoppedTime-XX:+PrintGCDateStamps- Xloggc:gclog.log后,从GC日志文件中确认了停顿确实是由GC导致的,大部分GC时间都控 制在100毫秒以内,但偶尔就会出现一次接近1分钟的GC。
    从GC日志中找到长时间停顿的具体日志信息(添加了-XX:+PrintReferenceGC参数), 找到的日志片段如下所示。从日志中可以看出,真正执行GC动作的时间不是很长,但从准 备开始GC,到真正开始GC之间所消耗的时间却占了绝大部分。
    除GC日志之外,还观察到这个GUI程序内存变化的一个特点,当它最小化的时候,资源 管理中显示的占用内存大幅度减小,但是虚拟内存则没有变化,因此怀疑程序在最小化时它 的工作内存被自动交换到磁盘的页面文件之中了,这样发生GC时就有可能因为恢复页面文 件的操作而导致不正常的GC停顿。在Java的GUI程序中要避免这种现象,可以 加入参数“-Dsun.awt.keepWorkingSetOnMinimize=true”来解决。

    OOM排查

  • 先调大堆内存
  • 内存分析工具
    • MAT(eclipse集成)、Jprofiler
    • 分析Dump内存文件,快速定位内存泄露。
    • 获得堆中的数据。
    • 获得大的对象。

脑图链接