抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

类加载

加载过程

java文件编译class字节码文件,通过类加载器写入到JVM虚拟机运行时数据区中。

类加载流程

​ 类加载需要经历五个阶段,分别是加载,验证,准备,解析,初始化。其中加载,验证,准备,初始化这四个阶段发生的顺序时确定的,而解析阶段时不一定的。它在某些情况下可以在初始化阶段之后开始,这是为了支持java的运行时绑定(也称做动态绑定)。另外,这些阶段都是按顺序开始,而非按顺序进行或者完成。

类加载器

双亲委派机制

​ 当某个类加载器需要加载某个class文件时,它首先会把这个任务委派给它的父级加载器,递归到顶级加载器。随后进行加载,倘若加载失败,由子类加载器进行加载。可以保证同一个class只被加载一次。

运行时数据区

绿色线程共享,蓝色线程私有




  • JDK6:有永久代,静态变量存放在永久代上
  • JDK7:有永久代,但是已经把运行时常量池,常量池存放在方法区中了,逐渐的减少永久代的使用。
  • JDK8:无永久代,运行是常量池、类常量池都保存在元数据区,也就是元空间 。但是字符串常量池任然放在堆上。需要注意的是:元空间占用的是本地内存

内存模型

程序计数器

线程私有。Java虚拟机执行多线程时,由于CPU时间片限制,线程执行具有随机性,操作系统会一直切换执行不同指令,我们需要把切换时候线程执行的位置存入到PC寄存器中,等切回来的时候能够回到原来的位置继续执行。 任何时候,每个Java虚拟机都在执行单个方法的代码,即该线程的当前方法。如果该方法不是Native方法,即PC寄存器会记录当前正在执行的java虚拟机指令的地址,如果线程当前执行的方法是本地的,那么java虚拟机的PC寄存器的值就是Undefined程序计数器是唯一一个不会出现OOM的区域。

本地方法栈

线程私有。提供虚拟机使用到本地Native方法,本地方法栈和虚拟机栈在有的虚拟机是合在一起的,例如Hot Spot虚拟机。

线程私有,每个线程都有一个与线程同时创建的私有的虚拟机栈。虚拟机栈中存储栈帧,每个被线程调用的方法都会产生一个栈帧,栈帧中保存一个方法的状态信息,例如局部变量(存储方法内的局部变量)、操作数帧(用来计算临时存放变量)、方法出口(记录哪个方法的本方法)、动态链接(存放方法的元信息)等。调用一个方法就是执行一个栈帧的过程,一个方法调用完成,对应的栈帧就会出栈。

  • 如果线程执行所需栈深度大于Java虚拟机栈深度,就会抛出StackOverFlowError,其实方法调用的过程就是入栈和出栈的过程,如果一致入栈不出栈,就容易发生异常(递归调用)
  • 如果Java虚拟机栈可以动态扩展,但是扩展大小的时候无法申请到足够的内存,则会抛出OutOfMemoryError

线程共享。堆是java虚拟机管理内存最大的一块,在虚拟机启动时创建,所有线程共享,堆中的对象永远不会被显式释放,必须由GC回收,所以GC也主要回收堆中的对象实例,我们平常讨论的垃圾回收就是回收堆内存。堆可以处于物理上不连续的空间,可以固定大小,也可以动态扩展,通过参数-Xms-Xmx两个参数控制堆的最小值和最大值。这里是**OOM异常的易发区**

分成新生代和老年代。

新生代又分成1个Eden区和2个Survivor区,默认比例8:1:1

绝大多数对象在Eden区生成,**当Eden区填满,或者不满足新建一个对象时,触发Young GC。**将存活的对象放进SurvivorSurvivor分成两块,用于每次Young GC时,将存活的对象统统复制进空的幸存区,将有数据的那块清空。

当一个对象在两个survivor区之间交换了一定次数之后,将对象拷贝到老年代。这个数量用-XX:MaxTenuringThreshold配置,默认值是15

方法区

线程共享,在虚拟机启动时创建,存储已经被虚拟机加载的类信息、常量、静态变量,即经编译器之后的数据(运行时常量池[存放class文件元信息描述,编译后的代码数据,引用类型数据,类文件常量池]、属性和方法数据),以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化使用的特殊方法,方法区在逻辑上是堆的一部分,但是它又有另一个别名叫非堆,目的是与堆区分开。方法区可以是固定大小,也可以根据计算需要进行扩展。如果方法区的内存无法满足分配请求时也会抛出OutOfMemoryError

永久代

元空间

JDK8使用元空间代替永久代(类元信息,字段,静态属性,方法,常量),同时运行时常量池等都移动到了元空间,字符串常量池在堆中。

元空间在本地内存中,它独立于运行时数据区,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,因此不受堆大小影响。但是会受到本机总内存大小以及处理器寻址空间的限制,因此它也可能抛出OOM异常。

垃圾回收

找到垃圾

​ 两种方法判断对象是否需要销毁:

  • 引用计数法

    ​ 为对象添加一个引用计数器,每当对象被引用,引用计数器加1。当引用失效,减一。计数器为零,代表该对象可以被回收。

    ​ **问题:**循环引用问题

  • 可达性分析

    ​ 通过一系列"GC Roots"的根节点开始,沿用引用链进行搜索,凡是在引用链上的,都不会被回收。

    ​ 解决了循环依赖问题。

垃圾回收算法-GC

标记-清除算法

  1. 首先在堆内存扫描一遍,然后把灰色区域的对象标记
  2. 继续扫描,扫描的同时将被标记的对象统一回收回收后:

  • 回收之后,产生大量不连续的内存碎片。
  • 需要进行标记,清除两个过程,效率不高。

复制算法

​ 将内存区域分成大小一样的两块,每次只是用一块,gc之后将对象复制到另一块,然后一次性清理掉这块内存。

回收前:

回收后:

复制算法的缺点是牺牲了一半的内存空间,有点浪费。复制算法在JVM中的体现就是:java堆内存做了几次划分,Hot Spot虚拟机中EdenSurvivor的比例是Eden:S0:S1=8:1:1,将Survivor分成了两个区域S0S1来进行赋值,这种做法是为了弥补原始复制算法直接将一半空间作为空闲空间的浪费。IBM公司表示:Young区有98%的对象都是朝生夕死的,生命周期极短,所以说一次GC下来存活的对象很少,所以没必要用一半空间来复制。

标记-整理算法

​ 标记-整理算法就是为了老年代而设计的算法,标记-整理算法和标记-清除算法的区别在最后一步,标记-整理不会直接对对象清理,而是进行移动,将存活对象移动到一端,然后清理掉边界以外的对象。

回收前:

回收后:

分代收集算法

​ 目前主流的商业虚拟机都是采用分代收集算法,这种算法就是上面三种算法的结合。新生代采用复制算法,老年代采用标记-整理或标记-清除算法。

垃圾收集器

Serial 和Serial Old 收集器

Serial 单线程,GC时Stop The World(STW),暂停所有用户线程。

Serial Old 单线程,作用与老年代。标记整理算法

优点:单线程收集效率高

缺点:需要STW,暂停所有用户线程

算法: Serial采用复制算法 ;Serial Old采用标记-整理算法

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,实现并行收集, 原理跟Serial一致(并行指的是多个GC线程并行,但是用户线程还是暂停,并发指的是用户线程和GC线程同时执行)。ParNew默认开启和CPU个数相同的线程数进行回收。

优点:在多CPU时,比Serial的效率高。

缺点:还是需要STW,单CPU时比Serial效率低

算法:复制算法

Parallel Scavenge 收集器

新生代收集器,也是复制算法,和ParNew一样并行的多线程收集器,更关注系统的吞吐量
[吞吐量=(运行用户代码的时间) / (运行用户代码的时间+ GC 时间) ]
Parallel Scavenge 提供了两个参数用于精确控制吞吐量:

1
2
3
4
5
6
7
8
9
-XX:MaxGCPauseMillis //GC最大停顿毫秒数,必须大于0
-XX:GCTimeRation //设置吞吐量大小,大于0小于100,默认值为99
-XX:+UseAdaptiveSizePolicy //开启自适应策略

//你会不会觉得把MaxGCPauseMillis设置小点就会让GC速度变快?
//答案是否定的,如果设置时间过小,Parallel Scavenge 会牺牲吞吐量和新生代空间来交换,
//比如新生代400Mb需要GC时间为100ms,设置成50ms了,那么就会把新生代调小为200Mb,这样肯定时间就降下来了,
//然而这种操作可能会降低吞吐量,原先10s触发一次GC,每次100ms,
// 修改时间后变成5s触发一次GC,每次70ms,那么10ms触发两次GC的时间变成了140ms,吞吐量反而降低。

Parallel Old 收集器

是Parallel Scavenge 的老年代版本,使用标记-整理算法,因为Parallel Scavenge 无法和CMS搭配使用,所以只能和Serial Old。自从Parallel Old出现,就有了Parallel Scavenge +Parallel Old的组合,这是JDK8使用的,注重吞吐量的一组收集器。

CMS收集器

这是优化GC停顿时间为目标的收集器,并发回收(仍然需要STW,但是时间很短)。通过-XX:+UseMarkSweepGC启用。CMS基于标记-清除算法实现。整个过程分为四步:

  • 初始标记:需要STW,标记GC Roots对象。
  • 并发标记:这个阶段可以和用户线程一起进行,分为三步:
    1. 根据第一步找到的GC Roots开始搜索跟GC Roots相连的对象。
    2. 预清理:处理并发标记之后发生变化的对象。
    3. 可被终止的预清理:有一个abort 触发条件,该阶段存在的目的是希望能发生一次Young GC,来减少Young区对象数量,降低重新标记的工作量,因为重新标记会扫描整个堆内空间,可以通过参数
      -XX:+CMSScavangeBeforeRemark 控制在重新标记前发生一次Young GC,默认为false
  • 重新标记:需要STW,这个阶段是为了修正在阶段2标记之后产生变化的对象。
  • 并发清除:和用户线程同时进行,开始正式清理垃圾,此阶段产生的垃圾留待下次清除。

优点:并发收集,低停顿

缺点:产生大量碎片,并发阶段会降低吞吐量

G1收集器

G1是以优化GC停顿时间为目标的收集器,它尝试以高概率满足GC停顿时间为目标,同时实现高吞吐量。在G1中,将堆的整个内存布局做了修改,在G1中整个堆划分为多个大小相等的独立区域Region,虽然在逻辑上还保留了新生代和老年代,但是物理上已经隔离了,G1的堆内存布局如下

上图智能柜被划分成一组大小相同的Region,每个Region都是连续的虚拟内存范围,G1可以知道哪个Region区域内大部分是空的,这样就可以在每次允许的收集时间内优先回收价值最大的Region区域(根据回收所获得的空间大小以及回收需要的时间综合考虑)所以这就是G1叫做Garbage-First的原因。G1JDK8默认的垃圾收集器G1的工作流程和CMS很相似,区别在最后的步骤。也有四步:

  • 初始标记:需要STW,标记下GC Roots关联的对象,并且修改TAMS(Next Top at Mark Start)的值,使得下一阶段并发运行时,能在正确可用的Region中创建对象。
  • 并发标记:和CMS一样,主要是进行GC Roots的向下搜索,找出存活对象进行标记。
  • 最终标记:需要STW,和CMS一样,这个阶段是修正并发标记期间因用户程序运行而导致变动的对象。
  • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间制定回收计划。

G1的第一个重点是为运行需要大堆且GC延迟有限的应用程序的用户提供解决方案,这就意味着堆大小约为6G或更大,并且稳定且可预测的暂停时间低于0.5秒。如果应用程序具备以下特性,可以考虑切换到G1收集器:

  • 超过50%的Java堆被实时数据占用
  • 对象分配率或提升率差异很大
  • 当前应用程序GC停顿时间超过0.5秒,又想缩短停顿时间

各收集对比

收集器 串行/并行/并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制 响应速度优先 单CPU环境下的Client模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式,CMS的后备预案
ParNew 并行 新生代 复制 响应速度优先 多CPU环境时在Server模式下与CMS配合使用
Parallel Scavenge 并行 新生代 复制 吞吐量优先 在后台运算且不需要太多交互的任务
Parallel 并行 老年代 标记-整理 吞吐量优先 在后台运算且不需要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网网站 或 B/S系统服务端中的Java应用
G1 并发 兼顾 标记-整理+复制 响应速度优先 面向服务端应用
  • 串行收集器:Serial和Serial Old单线程收集,适用于内存较小的嵌入式设备。
  • 并行收集器【吞吐量优先】:Parallel Scavenge + Parallel Old,适用于科学计算、后台处理等场景。
  • 并发收集器【GC停顿时间优先】:CMS和G1,适用于对时间有要求的场景,例如Web应用。

调优

所谓调优就是设置一个合理的JVM参数,适配当前系统运行。

参数

可以分成三类:标准参数,-X参数,-XX参数

  • 标准参数

    ​ 以"-"开头的参数称为标准参数,是任何一个JDK版本都支持的,比较稳定,不会随版本更新和改变。例如-version,-help,-server。

  • -X参数

    ​ 以-X开头的参数是在特定版本HotSpot支持的命令。JDK版本变化之后,参数可能变化,这个参数用的较少。

  • -XX参数

    -XX是不稳定的参数,也是主要参数,分为Boolean类型和非Boolean类型。

    • Boolean类型

      -XX:[+-]<name>:+或-表示启用或者禁用name属性

      1
      2
      3
      4
      5
      6
      表示启用CMS垃圾收集器
      -XX:+UseConcMarkSweepGC
      表示启用G1垃圾收集器
      -XX:+UseG1GC
      表示打印出所有的JVM参数信息
      -XX:+PrintFlagsFinal
    • 非Boolean类型

      -XX<name>=<value>:name表示属性,value表示属性对应的值

      1
      2
      设置最大永久代空间大小为5M
      -XX:MaxMetaspaceSize=5M
    • 其他

      1
      2
      3
      -Xms1000 等价于 -XX:InitialHeapSize=1000
      -Xmx1000 等价于 -XX:MaxHeapSize=1000
      -Xss1000 等价于 -XX:ThreadStackSize=1000

常用参数

设置 说明
-XX:ClCompilerCount=3 最大并行编译数,大于1时可以提高编译速度,但会影响系统稳定性
-XX:InitialHeapSize=100m 初始堆大小,可以简写为-Xms100
-XX:MaxHeapSize 最大堆大小,可以简写为-Xmx100
-XX:NewSize=20m 设置年轻代大小
-XX:MaxNewSize 设置年轻代最大值
-XX:OldSize=50m 设置老年代大小
-XX:MetaspaceSize=50m 设置方法区大小,JDK8才有,用元空间代替方法区
-XX:+UseParallelGC 设置Parallel Scavenge作为新生代收集器
系统默认会选择Parallel Old作为老年代收集器
-XX:NewRatio 新生代和老年代的比值
比如 -XX:NewRatio=4表示新生代:老年代=1:4
-XX:SurvivorRatio 表示Survivor区和Eden区的比值
比如-XX:SurvivorRatio=8表示(S0或S1):Eden=1:8

命令监控工具

待续


参考:

Java SE Specifications (oracle.com)

JVM万字总结 - 掘金 (juejin.cn)

【JVM系列8】JVM知识点总览 - 掘金 (juejin.cn)

JVM8内存模型_jdk8内存模型_余生之君的博客-CSDN博客

JVM内存模型总结,有各版本JDK对比、有元空间OOM监控案例、有Java版虚拟机,综合实践学习! - 小傅哥 - 博客园 (cnblogs.com)

https://zhuanlan.zhihu.com/p/297001119

评论