关于java:JVM性能监控与调优之概述命令行篇

13次阅读

共计 10661 个字符,预计需要花费 27 分钟才能阅读完成。

背景阐明

生产环境中呈现的问题

  • 生产环境产生了内存溢出该如何解决?
  • 生产环境应该给服务器调配多少内存适合?
  • 如何对垃圾回收器的性能进行调优?
  • 生产环境 CPU 负载期飙高该如何解决?
  • 生产环境应该给利用调配多少线程适合?
  • 不加 log,如何确定申请是否执行了某一行代码?
  • 不加 log,如何实时查看某个办法的入参加返回值?

为什么要调优

  • 防止出现 OOM
  • 解决 OOM
  • 缩小 Full GC 呈现的频率

不同阶段的思考

  • 上线前
  • 我的项目运行阶段
  • 线上呈现 OOM

调优概述

监控的根据

  • 运行日志
  • 异样堆栈
  • GC 日志
  • 线程快照
  • 堆转储快照

调优的大方向

  • 正当的编写代码
  • 充沛并正当应用硬件资源
  • 正当地进行 JVM 调优

性能优化的步骤

第一步:性能监控(发现问题)

一种以非强行或者入侵形式 收集或查看 利用经营性能数据的流动。

监控通常是指一种在生产、品质评估或者开发环境下施行的带有 预防或被动 性的流动。

当利用相干干系人提出性能问题却 没有提供足够 多的线索时,首先咱们须要进行性能监控,随后是性能剖析。

  • GC 频繁
  • CPU load 过高
  • OOM
  • 内存透露
  • 死锁
  • 程序响应工夫较长

第二步:性能剖析(排查问题)

一种以 侵入形式 收集运行性能数据的流动,它会影响利用的吞吐量或响应性。

性能剖析是针对性能问题的回答后果,关注的范畴通常比性能监控更加集中。

性能剖析很少在生产环境下进行,通常是在品质评估、零碎测试 或者 开发环境 下进行,是性能监控之后的步骤。

  • 打印 GC 日志,通过 GCview 或者 http://gceasy.io来剖析日志信息
  • 灵活运用,命令行工具,jstackjmapjinfo
  • dump出堆文件,应用内存剖析工具剖析文件
  • 应用阿里 Arthas, 或 jconsole, JVisualVM 来实时查看 JVM 状态
  • jstack查看堆栈信息

第三步:性能调优(解决问题)

一种为改善利用响应性或吞吐量而更改参数、源代码、属性配置的流动,性能调优是在性能监控、性能剖析之后的流动。

  • 适当减少内存,依据业务背景抉择垃圾回收器
  • 优化代码,管制内存应用
  • 减少机器,扩散节点压力
  • 正当设置线程池线程数量
  • 应用中间件进步程序效率,比方缓存,音讯队列等

性能评估 / 测试指标

进展工夫(响应工夫)

提交申请和返回该申请的响应之间应用的工夫,个别比拟关注均匀响应工夫。

罕用操作的响应工夫列表:

操作 响应工夫
关上一个站点 几秒
数据库查问一条记录(有索引) 十几毫秒
机械磁盘一次寻址定位 4 毫秒
从机械磁盘程序读取 1M 数据 2 毫秒
从 SSD 磁盘程序读取 1M 数据 0.3 毫秒
从近程分布式换成 Redis 读取一个数据 0.5 毫秒
从内存读取 1M 数据 十几奥妙
Java 程序本地办法调用 几奥妙
网络传输 2Kb 数据 1 奥妙

在垃圾回收环节中:

暂停工夫:执行垃圾收集时,程序的工作线程被暂停的工夫

-XX:MaxGCPauseMillis

吞吐量

对单位工夫内实现的工作量(申请)的量度

在 GC 中:运行用户代码的工夫占总运行工夫的比例(总运行工夫:程序的运行工夫 + 内存回收的工夫)

吞吐量为:1-1/(1+n)

-XX:GCTimeRatio=n

并发数

同一时刻,对服务器有理论交互的申请数。

1000 集体同时在线,预计并发数在 5%-15% 之间,也就是同时并发量:50-150 之间。

内存占用

Java 堆区所占的内存大小。

相互间的关系

以高速公路通行情况为例

  • 吞吐量:每天通过高速公路收费站的车辆的数据
  • 并发数:高速公路上正在行驶的车辆的数
  • 响应工夫:车速

随着并发数越来越多,响应工夫也就是车速会缓缓升高,吞吐量也可能会反而升高。

JVM 监控及诊断工具 - 命令行

概述

性能诊断 是软件工程师在日常工作中须要常常面对和解决的问题,在用户体验至上的明天,解决好利用的性能问题能带来十分大的收益。

Java 作为最风行的编程语言之一,其利用性能诊断始终受到业界宽泛关注。可能造成 Java 利用呈现性能问题的因素十分多,例如 线程管制 磁盘读写 数据库拜访 网络 I/O垃圾收集 等。想要定位这些问题,一款优良的性能诊断工具必不可少。

简略命令行工具

刚接触 java 学习的时候,大家必定最先理解的两个命令就是javac , java

那么 , 除此之外,还有没有其余的命令能够供咱们应用呢?

咱们进入到装置 jdk 中的 bin 目录, 发现还有一系列辅助工具。这些辅助工具用来获取指标 JVM 不同方面、不同档次的信息,帮忙开发人员很好地解决 Java 应用程序的一些疑难杂症。

mac 零碎:

jps:查看正在运行的 Java 过程

JPS(Java Process Staflus):显示指定零碎内所有的 HotSpot 虚拟机过程, 可用于查问正在运行的虚拟机过程。

测试

/**
 * @author 又坏又迷人
 * 公众号: Java 菜鸟程序员
 * @date 2021/2/7
 * @Description: 线程休眠, 查看 jps 命令
 */
public class ThreadSleep {public static void main(String[] args) throws InterruptedException {TimeUnit.HOURS.sleep(1);
    }

}

运行起来之后,咱们在命令行输出 jps 能够查看到该过程 id 以及名称。

对于本地虚拟机过程来说,过程的 本地虚拟机 ID与操作系统的 过程 ID是统一的,是惟一的。

根本用法

语法格局:

jps [options] [hostid]

options 参数

-q:仅仅显示本地虚拟机惟一 id。不显示名称。

jps -q

-l:输出应用程序朱磊的全类名或如果执行的是 jar 包,则输入 jar 残缺门路。

jps -l

-m:输入虚拟机过程启动时传递给主类 main()的参数

jps -m

-v:列出虚拟机过程启动时的 JVM 参数。比方:-Xms20m -Xmx50m

jps -v

如果 Java 过程敞开了默认开启的 UsePerfData 参数(即应用参数-XX:-UsePerfData) , 那么 jps 命令以及 jstat 将无奈获取该 Java 过程。

hostid 参数

RMI 注册表中注册的主机名。

如果想要近程监控主机上的 java 程序,须要装置 jstatd。

对于具备更严格的平安实际的网络场合而言,可能应用一个自定义的策略文件来显示对特定的可信主机或网络的拜访,只管这种技术容易受到 IP 地址欺诈攻打。

如果平安问题无奈应用一个定制的策略文件来解决,那么最平安的操作是不运行 jstatd 服务器,而是在本地应用 jstat 和 jps 工具。

jstat:查看 JVM 统计信息

jstat(JVM Statistics Monitoring Tool): 用于监督虚拟机 各种运行状态信息 的命令行工具。

它能够显示本地或者近程虚拟机过程中的 类装载 内存 垃圾收集 JIT 编译 等运行数据。

在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期间定位虚拟机性能问题的首选工具。
罕用于 检测垃圾回收 问题以及 内存透露 问题。

根本语法

根本语法为:

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

查看命令相干参数:

jstat -h 或者 jstat -help

option 参数

选项 option 能够由以下值形成 :

类装载相干的:

-class:显示 ClassLoader 的相干信息:类的装载 卸载数量 总空间 类装载所耗费的工夫

jstat -class PID

JIT 相干的:

-compiler:显示 JIT 编译器编译过的办法、耗时等信息。

jstat -compiler PID

-printcompilation:输入曾经被 JIT 编译的办法。

jstat -printcompilation PID

垃圾回收相干的:

/**
 * @author 又坏又迷人
 * 公众号: Java 菜鸟程序员
 * @date 2021/2/7
 * @Description: 测试 jstat 垃圾回收参数相干
 *
 * -Xms60m -Xmx60m -XX:SurvivorRatio=8
 */
public class GCTest {public static void main(String[] args) {ArrayList<byte[]> list = new ArrayList<>();
        int num = 1000;

        for (int i = 0; i < num; i++) {
            //100KB
            byte[] arr = new byte[1024 * 100];
            list.add(arr);
            try {Thread.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }

}

-gc:显示与 GC 相干的堆信息。包含 Eden 区、两个 Survivor 区、老年代、永恒代等的容量、已用空间、GC 工夫共计等信息。

jstat -gc PID

参数细节

新生代相干

  • SOC 是第一个幸存者区的大小(字节)
  • S1C 是第二个幸存者区的大小(字节)
  • SOU 是第一个幸存者区已应用的大小(字节)
  • S1U 是第二个幸存者区已应用的大小(字节)
  • EC 是 Eden 空间的大小(字节)
  • EU 是 Eden 空间已应用大小(字节)

老年代相干

  • OC 是老年代的大小(字节)
  • OU 是老年代已应用的大小(字节)

办法区(元空间)相干

  • MC 是办法区的大小
  • MU 是办法区已应用的大小
  • CCSC 是压缩类空间的大小
  • CCSU 是压缩类空间已应用的大小

其它

  • YGC 是指从应用程序启动到采样时 young gc 次数
  • YGCT 是指从应用程序启动到采样时 young gc 耗费的工夫(秒)
  • FGC 是指从应用程序启动到采样时 full gc 次数
  • FGCT 是指从应用程序启动到采样时 full gc 耗费的工夫(秒)
  • GCT 是指从应用程序启动到采样时 gc 的总工夫

-gcutil:显示内容与 -gc 基本相同,但输入次要关注已应用空间占总空间的百分比

jstat -gcutil PID

-gccapacity:显示内容与 -gc 基本相同,但输入次要关注 Java 堆各个区域应用到的最大、最小空间

jstat -gccapacity PID

-gccause:与 -gcutil 性能一样,然而会额定输入导致最初一次或以后正在产生的 GC 产生的起因。

jstat -gccause PID

-gcnew:显示新生代 GC 情况。

jstat -gcnew PID

  • S0C:第一个 survivor 区大小
  • S1C:第二个 survivor 区的大小
  • S0U:第一个 survivor 区的应用大小
  • S1U:第二个 survivor 区的应用大小
  • TT:对象在新生代存活的次数
  • MTT:对象在新生代存活的最大次数
  • DSS:冀望的 survivor 区大小
  • EC:eden 区的大小
  • EU:eden 区的应用大小
  • YGC:年老代垃圾回收次数
  • YGCT:年老代垃圾回收耗费工夫

-gcnewcapacity:显示内容与 -gcnew 基本相同,输入次要关注应用到的最大、最小空间。

jstat -gcnewcapacity PID

  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:以后新生代容量
  • S0CMX:最大 survivor1 区大小
  • S0C:以后 survivor1 区大小
  • S1CMX:最大 survivor2 区大小
  • S1C:以后 survivor2 区大小
  • ECMX:最大 eden 区大小
  • EC:以后 eden 区大小
  • YGC:年老代垃圾回收次数
  • FGC:老年代回收次数

-gcold: 显示老年代 GC 情况。

jstat -gcold PID

  • MC:办法区大小
  • MU:办法区应用大小
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间应用大小
  • OC:老年代大小
  • OU:老年代应用大小
  • YGC:年老代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收耗费工夫
  • GCT:垃圾回收耗费总工夫

-gcoldcapacity:老年代内存统计,次要关注应用到的最大、最小空间。

jstat -gcoldcapacity PID

  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:以后老年代大小
  • OC:老年代大小
  • YGC:年老代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收耗费工夫
  • GCT:垃圾回收耗费总工夫

-gcmetacapacity:输入永恒代应用到的最大、最小空间。

jstat -gcmetacapacity PID

  • MCMN: 最小元数据容量
  • MCMX:最大元数据容量
  • MC:以后元数据空间大小
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • CCSC:以后压缩类空间大小
  • YGC:年老代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收耗费工夫
  • GCT:垃圾回收耗费总工夫

-t 参数

能够在输入信息前加上一个 Timestamp 列,显示程序的运行工夫。单位:秒。

jstat -class -t PID

-h 参数

能够在周期性数据输入时,输入多少行数据后输入一个表头信息。

jstat -class -hx PID

interval 参数

用于指定输入统计数据的周期,单位为毫秒。即:查问距离。

jstat -class PID ms

count 参数

用于指定查问的总次数,n 为总次数

jstat -class PID ms n

jstat 还能够用来判断是否呈现内存透露 :

第 1 步:

在长时间运行的 Java 程序中,咱们能够运行 jstat 命令间断获取 多行性能数据,并取这几行数据中 OU 列(即已占用的老年代内存)的最小值。

第 2 步:

而后,咱们每隔一段较长的工夫反复一次上述操作,来取得 多组 OU 最小值
如果这些值呈上涨趋势,则阐明该 Java 程序的 老年代内存已使用量在一直上涨 ,这意味着 无奈回收的对象在一直减少 ,因而很有可能 存在内存透露

jinfo:实时查看和批改 JVM 配置参数

jinfo(Configuralion Info for Java) : 查看虚拟机 配置参数 信息,也可用于调整虚拟机的配置参数。

在很多状况下,Java 应用程序不会指定所有的 Java 虚拟机参数。而此时,开发人员可能不晓得某一个具体的 Java 虚拟机参数的默认值。

在这种状况下,可能须要通过查找文档获取某个参数的默认值。这个查找过程可能是十分艰巨的。但有了 jinfo 工具,开发人员能够很不便地找到 Java 虚拟机参数的以后值。

根本语法

jinfo [options] pid

[options]:

选项 选项阐明
no option 输入全副的参数和零碎属性
-flag name 输入对应名称的参数
-flag [+-]name 开启或者敞开对应名称的参数只有被标记为 manageable 的参数才能够被动静批改
-flag name=value 设定对应名称的参数
-flags 输入全副的参数
-sysprops 输入零碎属性

查看

-sysprops:能够查看由 System.getProperties()获得的参数。

jinfo -sysprops PID

-flags:查看已经赋过值的一些参数。

jinfo -flags PID

-flag:查看某个 java 过程具体参数。

 jinfo -flag 具体参数 PID

批改

jinfo 不仅能够查看运行时某一个 Java 虚拟机参数的理论取值,甚至能够在运行时批改局部参数,并使之立刻失效。

然而,并非所有参数都反对动静批改。参数只有被标记为 manageable 的 flag 能够被实时批改。其实,这个批改能力是极其无限的。

能够查看被标记为 manageable 的参数。

java -XX:+PrintFlagsFinal -version | grep manageable
intx CMSAbortablePrecleanWaitMillis =100 {manageable}
intx CMSWaitDuration =2000 {manageable}
bool HeapDumpAfterFullGC =false {manageable}
bool HeapDumpBeforeFullGC =false {manageable}
bool HeapDumpOnOutofMemoryError =false {manageable}
ccstr HeapDumpPath {manageable}
uintx MaxHeapFreeRatio =100 {manageable}
uintx MinHeapFreeRatio =0 {manageable}
bool PrintClassHistogram =false {manageable}
bool PrintClassHistogragAfterFullGC =false {manageable}
bool PrintClassHistogramBeforeFullGC =false {manageable}
bool PrintConcurrentLocks =false {manageable}
bool PrintGC =false {manageable}
bool PrintGCDateStamps =false {manageable}
bool PrintGCDetails =false [manageable}
bool PrintGCTimeStamps =false {manageable}

针对 boolean 类型:

jinfo -flag [+-]具体参数 PID

针对非 boolean 类型:

jinfo -flag 具体参数 = 具体参数值 PID

扩大

  • java -XX+PrintFlagslnitial PID:查看所有 JVM 参数启动的初始值。
  • java -XX:+PrintFlagsFinal PID: 查看所有 JVM 参数的最终值。
  • java -XX+PrintCommandLineFlags PID:查看曾经被用户或者 JVM 设置过的具体参数名称和值。

jmap:导出内存映像文件 / 内存应用状况

jmap(JVM Memory Map) : 作用一方面是获取 dump 文件(堆转储快照文件,二进制文件),它还能够获取指标 Java 过程的内存相干信息,包含 Java堆各区域的应用状况 堆中对象的统计信息 类加载信息 等。

根本语法

jmap [option] <pid>
jmap [option] <executable <core>
jmap [option] [server_id@]<remote server IP or hostname>

其中 option 包含:

选项 作用
-dump 生成 dump 文件,-dump:live 只保留堆中存活的对象
-heap 输入整个堆空间的详细信息,包含 GC 的应用、堆配置信息,以及内存的应用信息等
-histo 输入堆空间中对象的统计信息,包含类、实例数量和共计容量
-permstat 以 ClassLoader 为统计口径输入永恒代的内存状态信息(仅 linux/solaris 平台无效)
-finalizerinfo 显示在 F-Queue 中期待 Finalizer 线程执行 finalize 办法的对象(仅 linux/solaris 平台无效)
-F 当虚拟机过程对 -dump 选项没有任何响应时,强制执行生成 dump 文件(仅 linux/solaris 平台无效)

阐明:这些参数和 linux 下输出显示的命令多少会有不同,包含也受 jdk 版本的影响。

生成 Java 堆转储快照文件:dump

一般来说,应用 jmap 指令生成 dump 文件的操作算得上是最罕用的 jmap 命令之一,将堆中所有存活对象导出至一个文件之中。

Heap Dump 又叫做堆存储文件,指一个 Java 过程在某个工夫点的内存快照。Heap Dump 在触发内存快照的时候会保留此刻的信息如下:

  • All Object’s

Class, fields, primitive values and references

  • All Classes

ClassLoader, name, super class, static fields

  • Garbage Collection Roots

Objects defined to be reachable by the JVM

  • Thread Stacks and Local Variables

The call-stacks of threads at the moment of the snapshot, and per-frame information about local objects

阐明:

  1. 通常在写 Heap Dump 文件前会触发一次 Full GC , 所以 Heap Dump 文件里保留的都是 Full GC 后留下的对象信息。
  2. 因为生成 dump 文件比拟耗时,大家须要急躁期待,尤其是大内存镜像生成的 dump 文件则须要消耗更长的工夫来实现。

手动的形式

jmap -dump:format=b,file=<filename.hprof><pid>
jmap -dump:live,format=b,file=<filename.hprof><pid> // 存活对象

主动的形式

当程序产生 OOM 退出零碎时,一些刹时信息都随着程序的终止而隐没,而重现 OOM 问题往往比拟艰难或者耗时。
此时若能在 OOM 时,主动导出 dump 文件就显得十分迫切。

这里介绍一种比拟罕用的获得堆快照文件的办法,即应用:

在程序产生 OOM 时,导出应用程序的以后堆快照 :

-XX:+HeapDumpOnOutOfMemoryError

能够指定堆快照的保留地位

-XX:HeapDumpPath=<filename.hprof>

显示堆内存相干信息

查看各区大小

jmap -heap pid

所有类型应用的内存

jmap -histo pid

因为 jmap 将拜访堆中的所有对象,为了保障在此过程中不被利用线程烦扰,jmap 须要借助 平安点机制 ,让所有线程停留在不扭转堆中数据的状态。
也就是说,由 jmap 导出的堆快照必然是 平安点地位 的。这可能导致基于该堆快照的剖析后果存在偏差。

举个例子,假如在编译生成的机器码中,某些对象的生命周期在两个平安点之间,那么 :live 选项将无奈探知到这些对象。

另外,如果某个线程长时间无奈跑到平安点,jmap 将始终等上来。
与后面讲的 jstat 则不同 , 垃圾回收器会被动将 jstat 所须要的摘要数据保留至固定地位之中,jstat 只需间接读取即可。

jhat:JDK 堆剖析工具

jhat(JVM Heap Analysis Topl) :

Sun JDK 提供的 jhat 命令与 jmap 命令搭配应用,用于剖析 jmap 生成的 heap dump 文件(堆转储快照)。
jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的剖析后果后,用户能够在浏览器中查看剖析后果(剖析虚拟机转储快照信息)。

应用了 jhat 命令,就启动了一个 http 服务,端口是 7000 , 即 http://localhost:7000/ , 就能够在浏览器里剖析。

阐明:jhat 命令在 JDK9、JDK10 中曾经被删除,官网倡议用 VisualVM 代替。

根本语法

jhat [option] [dumpfile]

之后咱们拜访 localhost:7000

option 参数

参数 含意
-stack false、true 敞开、关上对象调配调用栈跟踪
-refs false、true 敞开、关上对象援用跟踪
-port port-number 设置 jhat http 端口号 默认 7000
-exclude exclude-file 执行对象查问时须要排除的数据成员列表文件
-baseline exclude-file 制订一个基准堆转储
-debug int 设置 debug 级别
-version 启动后显示版本信息后就退出
-J <flag> 传入启动参数,比方 -J -Xmx512m

jstack:打印 JVM 中线程快照

jstack(JVM stlack Trace):用于生成虚拟机指定过程 以后时刻的线程快照 (虚拟机堆栈跟踪)。 线程快照 : 以后虚拟机内指定过程的每一条线程正在执行的办法堆栈的汇合。

生成线程快照的作用:可用于定位线程呈现 长时间进展的起因 ,如 线程间死锁 死循环 申请内部资源 导致的长时间期待等问题。

这些都是导致线程长时间进展的常见起因。当线程呈现进展时,就能够用 jstack 显示各个线程调用的堆栈状况。

thread dump 中,要注意上面几种状态:

  • 死锁,Deadlock
  • 期待资源,Waiting on condition
  • 期待获取监视器,Waiting on monitor entry
  • 阻塞,Blocked
  • 执行中,Runnable
  • 暂停,Suspended
  • 对象期待中,Object.wait()TIMED_WAITING
  • 进行,Parked

根本语法

jstack [option] pid

jstack 治理近程过程的话,须要在近程程序的启动参数中减少:

  • Djava.rmi.server.hostname=……
  • Dcom.sun.management.jmxremote
  • Dcom.sun.management.jmxremote.port=8888
  • Dcom.sun.management.jmxremote.authenticate=false
  • Dcom.sun.management.jmxremote.ssl=false
/**
 * @author 又坏又迷人
 * 公众号: Java 菜鸟程序员
 * @date 2021/2/7
 * @Description: 死锁 demo
 */
public class DeadLock {private static Object firstMonitor = new Object();
    private static Object secondMonitor = new Object();

    public static void main(String[] args) {new Thread(() -> {while (true) {synchronized (firstMonitor) {synchronized (secondMonitor) {System.out.println("Thread1");
                    }
                }
            }
        }).start();

        new Thread(() -> {while (true) {synchronized (secondMonitor) {synchronized (firstMonitor) {System.out.println("Thread2");
                    }
                }
            }
        }).start();}

}

option 参数

参数 含意
-F 当失常输入的申请不被响应时,强制输入线程堆栈
-I 出堆栈外,显示对于锁的附加信息
-m 如果调用到本地办法,能够显示 C/C++ 的堆栈
-h 帮忙操作

jcmd:多功能命令行

在 JDK 1.7 当前,新增了一个命令行工具 jcmd
它是一个多功能的工具,能够用来实现后面除了 jstat 之外所有命令的性能。比方:用它来导出 内存应用 、查看Java 过程、导出 线程信息 、执行GCJVM 运行工夫 等。

jcmd 领有 jmap 的大部分性能,并且在 Oracle 的官方网站上也举荐应用 jcmd 命令代 jmap 命令。

根本语法

jcmd -help

jcmd -l:列出所有的 JVM 过程

jcmd pid help:针对指定的过程,列出反对的所有命令

jcmd pid 具体命令:显示指定过程的指令命令数据。

正文完
 0