JVM理论部分
理论部分
JVM的发展
JDK 1.0在推出时自带JVM Classic vm,只能以纯解释器的方法来运行Java代码(和Python一样,像C/C++是编译运行),如果想使用JIT来进行优化还需要外挂,如果使用JIT,那么JVM就无法以纯解释的方法来运行代码,即二者只能存一。
JDK 1.2时,Sun 推出了Exact vm,尝试解决Classic vm的问题,不仅解决了两种编译方式无法同时运行的问题,优化了虚拟机的对象查找方式,还具备了一些现代高性能处理器的功能,如:二级即时编译。但是仍未成为主虚拟机,在推出HotSpot之后作为备用虚拟机存在。
JDK 1.3时,使用HotSpot作为JVM的虚拟机,HotSpot不仅拥有之前两个虚拟机的优点,还有新的技术,热点探测技术,这个技术是指用计数器统计最具有优化价值的代码,然后使用JIT对其进行深度优化,当时和ExactVM存在争议但是还是选择HotSpot作为主虚拟机。
其实HotSpot也并非Sun公司自研的虚拟机而是收购其他公司,例如BEA JRockit和IBM J9 JVM也是其他公司研发的虚拟机。还存在性能更加强悍的虚拟机Azul VM 和 BEA Liquid VM。这些虚拟机需要运行在特定硬件平台上,要求较高,但是性能远超其他虚拟机。
在Oracle收购Sun之后,将JRockit的一些优点整合到HotSpot上,例如JRockit的垃圾回收机制和MissionControle服务。
什么是JVM
一个使用C语言编译的.exe文件可以在Win上运行,但是在Linux和Mac上是无法正常运行的,此时如果要代码在其他系统上运行,还需要写两份代码,分别给Linux系统用户和Mac系统用户。
对于Java代码,通过javac的编译会得到一个.class文件,然后再丢到JVM中进行运行,此时和JVM在什么系统上没什么关系,具体的代码运行由JVM来执行,JVM再去向系统平台请求资源运行即可。
只要是按照Java规范和约束写出来的代码并且编译成.class文件,都可以再JVM上运行。
其实 Java 虚拟机就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行。
JVM内存结构
Java虚拟机的内存部分通常分为公有部分和私有部分,公有部分通常为Java堆,常量区,方法区,线程私有部分PC寄存器,Java虚拟机栈,本地方法栈
公有部分
Java堆存放Java实例对象的内存分配,方法区是指Java类字节码数据的一块区域,存储了每一个类的结构信息,例如运行时常量池,字段和方法数据和构造方法等 常量区是存在方法区中的,实际上他俩同级。
Java堆分为两个区域年轻代和老年代,年轻代还分为Eden,From Survive 0, To Survive 1三个区域,在JVM中有一个MaxTenuringThreshold 的参数专门设置晋升到老年代的GC次数,当一个年轻代到达一定的GC次数时,在下一次GC时会进入老年代。
IBM根据统计将年轻代的Eden,From,To区域设置为8:1:1,80%的对象存活时间很短,减少内存的浪费
私有部分
PC寄存器保存线程当前正在执行的方法,如果当前方法不是native方法,PC寄存器会保存JVM正在运行的方法的字节码指令地址,如果时native方法,则会存储undefined。任意时刻,一条JVM虚拟机线程只会运行一个方法的代码,这个方法被称为该线程的当前方法,其地址存在PC寄存器中。
JVM栈,该栈与线程同时创建,不论何时,其栈顶永远是正在执行的方法。其中存储的是局部变量和一些过程结果,栈存储的数据包括局部变量表,操作数栈。
当Java使用其他方法时(例如native调用底层c语言),也会使用的本地方法栈,如果java虚拟机不支持native且自己也不依赖传统栈,也无需支持本地栈
小结
Java 虚拟机的内存结构是学习虚拟机所必须掌握的地方,其中以 Java 堆的内存模型最为重要,因为线上问题很多时候都是 Java 堆出现问题。因此掌握 Java 堆的划分以及常用参数的调整最为关键。
除了上述所说的六大部分之外,其实在 Java 中还有直接内存、栈帧等数据结构。但因为直接内存、栈帧的使用场景还比较少,所以这里并不做介绍,以免让初学者一时间混淆。
学到这里,一个 Java 文件就加载到内存中了,并且 Java 类信息就会存储在我们的方法区中。如果创建对象,那么对象数据就会存放在 Java 堆中。如果调用方法,就会用到 PC 寄存器、Java 虚拟机栈、本地方法栈等结构。
Java类加载机制
从字节码文件到JVM虚拟机有以下几个过程:加载,验证,准备,解析,初始化,使用,卸载
加载
官方对加载的解释是:
加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。
总的来说就是将字节码文件加载到JVM中
验证
当JVM完成字节码文件的加载后并且在方法区加载好对应的Class对象之后,JVM会启动对该字节码流的校验,只有符合JVM字节码规范的文件才能被JVM正确执行
- JVM规范校验:JVM会对当前字节码文件进行校验,查看是否符合规范,以及能否被当前版本的虚拟机运行,例如校验
ox cofe bene
的开头,主次版本号是否在当前虚拟机处理的范围内 - 代码逻辑校验:JVM会对当前代码组成的数据流和控制流进行校验,确保JVM运行该代码时不会出现致命错误,例如需要传入int的方法被传入了一个String参数,一个要求返回String的方法却没有返回值,引入了Car类,这个类却并不能找到
当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。
准备
JVM开始为类变量分配内存并初始化
- 内存分配的对象:java的变量有类变量和类成员变量,类变量是指被static修饰的变量,其他均为类成员变量,在准备阶段只会为类变量分配内存,类成员变量会在初始化阶段开始
- 初始化的类型:static字段修饰的变量会赋值成对应对象的零值,如果是static final字段,会直接复制成程序员想要的结果,因为使用final字段后,就不会被在修改
解析
通过准备阶段之后,JVM针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类引用进行解析,这个阶段的任务是将常量池中的值替换为内存的直接引用
初始化
一定要记住,初始化是初始化,实例化是实例化,初始化是初始的类模板,实例化是根据类模板生成一个实例对象
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
使用
JVM从main方法进入整个程序
卸载
程序执行完成后,java开始销毁class对象,最后JVM也退出内存
Tip
在Java代码变成class文件之后,没有构造方法的概念的,只有类的初始化方法和对象初始化方法
- 类的初始化方法会在初始化类的时候从上到下依次执行,收集类变量的赋值语句、静态代码块
- 对象初始化方法会在实例化对象的时候完成,从上到下依次执行,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码
小结
- 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
- 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
- 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
- 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。
垃圾回收
啥是垃圾
如果一个类可能被引用,那么他就是垃圾
最简单的如何确定一个类是否是垃圾,就是加一个计数器,使用加一,未被使用减一,这种方法就是引用计数法,但是如果有几个类形成闭环,他们之间互相调用,并未对整个程序作出贡献,那么引用计数法就无效了
当前的Java垃圾判断机制是GC Root Tracing算法,从GC Root出发,无法到达的类就是垃圾,无法到达的类就是未被引用的类
GC Root中包含如下
- 所有当前被加载的 Java 类
- Java 类的引用类型静态变量
- Java类的运行时常量池里的引用类型常量
- VM的一些静态数据结构里指向GC堆里的对象的引用
- 等等
GC Root中都是被精心挑选的类,那么引申过来的类自然不归为垃圾
垃圾清除算法
垃圾清除算法有三种,标记清除算法,复制算法,标记压缩算法
标记清除算法
分为标记和清除阶段,从GC Root开始标记所有可达的对象,剩下的未被标记的就是需要被清除的对象,但是使用标记清除算法可能会导致内存空间碎片化严重
复制算法
将内存分为两块,正在使用的对象复制到未被使用的内存块中,之后清除掉所有正在使用的内存块中对象,之后交换两个内存块的角色,虽然不容易导致碎片化,但是极大浪费了内存地址
标记清除算法
是将标记清除算法和复制算法融合,有三个阶段标记,清除,压缩,在经过标记和清除之后,将存活的对象压缩到对象的一边
分代思想
对于不同的地域需要采取不同的治理,在之前我们就知道Java堆中分为了年轻代和老年代,对于存活数量较少的年轻代使用复制算法,对于老年代这种存活较多的区域适合使用标记清除算法和标记压缩算法,在极端情况下,老年代的存活率可能达到100%,如果使用复制算法工作量极为庞大。
在JVM年轻代划分中,Eden:From:To区域被分为8:1:1,如果不将年轻代划分直接进行复制算法,那么内存使用率只能到达50%,但是如果将年轻代划分为三个区域,且根据IBM公司的研究98%的年轻代都是朝生夕死,在回收时,将eden和survive中的还存活的对象复制到另一块survive上,节约了90%的内存空间
分层思想
JVM还有一个分层思想,分代思想按照对象的生命周期长短将其分为了两个部分(新生代、老年代),但 JVM 中其实还有一个分区思想,即将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个区间,可以较好地控制 GC 时间。
JVM垃圾回收器
JVM垃圾回收器分别为四个类别:串行回收器,并行回收器,CMS回收器,G1回收器。
串行回收器
串行回收器是指使用单线程进行垃圾回收,每次垃圾回收时,只有一个线程,因此串行回收器运行在并发能力较弱的计算机上
串行回收器可以在年轻代和老年代使用,根据作用于不同的堆空间,分为年轻代串行回收器和老年代串行回收器。
年轻代串行回收器
串行收集器是所有垃圾回收器中最古老的一种,也是JDK中最基础的回收器之一
在新生代串行回收器使用的是复制算法,在串行回收器进行垃圾回收时,会触发Stop-The-World现象,即其他线程都需要暂停,等垃圾回收完成,在某些情况下,会造成较为糟糕的用户体验。
使用-XX:+UseSerialGC
参数可以指定使用串行回收器,当虚拟机在Client模式运行下,默认使用该垃圾回收器
老年代串行回收器
在老年代串行回收器是使用标价压缩算法,与新生代串行收集器一样,只能串行,独占式也会有较长的Stop-The-World现象
使用老年代串行回收器的好处,就是可以与多种新生代回收器配合使用,若要启用老年代回收器,尝试以下参数:
-XX:UseSerialGC
:新生代、老年代都使用串行回收器。-XX:UseParNewGC
:新生代使用 ParNew 回收器,老年代使用串行回收器。-XX:UseParallelGC
:新生代使用 ParallelGC 回收器,老年代使用串行回收器。
并行回收器
并行回收器在串行回收器上进行了优化,使用多线程进行垃圾回收,适用于并行能力较强的机器,可以有效缩减垃圾回收的时间
根据作用的内存区域的不同,并行回收器也有三个不同的回收器:新生代PreNew回收器,新生代PreallelGC回收器,老年代PreallelGC回收器。
新生代PreNew回收器
新生代PreNew回收器工作在新生代,只是简单的将新生代串行回收器多线程化,其回收策略,算法和参数和新生代串行回收器一样。
新生代PreNew回收器使用同样的复制算法,但是同样也会出发Stop-The-World现象,但因为使用多线程进行回收,因此在并发能力强的CPU上,停顿的时间远低于新生代串行回收器。
在并行能力弱的系统中,因为切换线程的原因,其实际效果反而不如串行回收器。
要开启新生地PreNew回收器可以使用以下参数:
-XX:+UseParNewGC
:新生代使用 ParNew 回收器,老年代使用串行回收器。-XX:UseConcMarkSweepGC
:新生代使用 ParNew 回收器,老年代使用 CMS。-XX:ParallelGCThreads
:指定 ParNew 回收器的工作线程数量。
新生代ParallelGC回收器
新生代PreallelGC回收器和新生代PreNew回收器非常相似,使用复制算法,都是多线程,独占式的收集器,也会导致Stop-The-World,但有不同点就是注重系统的吞吐量
可以通过参数调节一个自适应的GC调节策略-XX:+UseAdaptiveSizePolicy
,打开这个策略新生代的大小,Eden,Survice的比例,晋升老年代的对象的年龄都会被自动调节,到达堆大小,吞吐量,停顿时间的平衡点。
Preallel GC回收器提供了两个重要的参数用于控制操作系统的吞吐量
-XX:MaxGCPauseMillis
:设置最大垃圾收集停顿时间。在 ParallelGC 工作时,其会自动调整响应参数,将停顿时间控制在设置范围内。为了达到目的,其可能会使用较小的堆,但这会导致 GC 较为频繁。-XX:GCTimeRatio
:设置吞吐量大小,其实一个 0 - 100 的整数。假设 GCTimeRatio 的值为 n,那么系统将不花费超过 1/(1+n) 的时间用于垃圾手机。比如 GCTimeRatio 值为 19,那么系统用于垃圾收集的时间不超过 1 /(1+19) = 5%。默认情况下,它的取值是 99,即不超过 1% 的时间用于垃圾收集。
新生代 Parallel GC 回收器可以使用以下参数启用:
-XX:+UseParallelGC
:新生代使用 Parallel 回收器,老年代使用串行回收器。-XX:+UseParallelOldGC
:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。
老年代ParallelOld GC 回收器
老年代ParallelOld GC回收器也是一种多线程并发的回收器,和新生代ParNew回收器一样,也是注重吞吐量的回收器,只不过是作用与老年代。
ParallelOld GC回收器采用标记压缩算法,只有在JDK 1.6中才能使用。我们可以使用-XX:UseParallelOldGC
参数在新生代中使用 ParallelGC 收集器,在老年代中使用 ParallelOldGC 收集器。参数 -XX:ParallelGCThreads
也可以用于设置垃圾回收时的线程数量。
CMS回收器
与ParNew,Parallel回收器不同,CMS回收器更注重于系统停顿的时间,全称叫做Concurrent Mark Sweep,意为标记清除算法,是一个多线程的垃圾回收器。
工作步骤
CMS的主要步骤有初始标记,并发标记,预清理,重新标记,并发清除和并发充值,其中初始标记和重新标记是单线程的,其他阶段可以和用户线程一起执行。
新生代 GC 就是清空 Eden 区,将存活对象移动到 Survivor 区,部分年龄到了就移动到老年代。
在CMS中可以关闭预处理的操作,我们可以关闭开关 -XX:-CMSPrecleaningEnabled
不进行预清理。因为重新标记是需要独占CPU的,因此新生代GC发生之后,立刻发出一个新的GC导致停顿时间过长,为了避免这种情况,预处理会等待一次新生代GC之后再执行。
主要参数
使用-XX:+UseConcMarkSweepGC
,启动CMS回收器,线程并发数量使用-XX:ConcGCThreads
或 -XX:ParallelCMSThreads
参数设定。
此外,我们还可以设置 -XX:CMSInitiatingOccupancyFraction
来指定老年代空间使用阈值。当老年代空间使用率达到这个阈值时,会执行一次 CMS 回收,而不像其他回收器一样等到内存不够用的时候才进行 GC。
CMS使用的算法会造成许多内存碎片,我们可以使用 XX:+UseCMSCompactAtFullCollection
参数让 CMS 在完成垃圾回收后,进行一次内存碎片整理。使用 -XX:CMSFullGCsBeforeCompaction
参数设置进行多少次 CMS 回收后,进行一次内存压缩。
此外,如果希望使用 CMS 回收 Perm 区,那么则可以打开 -XX:+CMSClassUnloadingEnabled
开关。打开该开关后,如果条件允许,那么系统会使用 CMS 的机制回收 Perm 区 Class 数据。
G1回收器
从分代来看,G1仍属于分代垃圾回收器,但他的改变在于使用了分区算法,从而使得年轻代和老年代的区域不必连续。G1之前的回收器的内存分配都是一整块的内存,而G1的内存分配不必是一整块的内存。
从上图可以看到,每个Region被标记了 E、S、O 和 H,说明每个 Region 在运行时都充当了一种角色。所有标记为 E 的都是 Eden 区的内存,它们散落在内存的各个角落,并不要求内存连续。同理,Survivor 区、老年代(Old)也是如此。
从上图我们还可以看到 H 是以往算法中没有的,它代表 Humongous。这表示这些 Region 存储的是巨型对象(humongous object,H-obj),当新建对象大小超过 Region 大小一半时,直接在新的一个或多个连续 Region 中分配,并标记为 H。
堆内存中一个 Region 的大小可以通过 -XX:G1HeapRegionSize
参数指定,大小区间只能是1M、2M、4M、8M、16M 和 32M,总之是2的幂次方。如果G1HeapRegionSize 为默认值,即把设置的最小堆内存按照2048份均分,最后得到一个合理的大小。
工作步骤
主要分为四个步骤
- 新生代 GC
- 并发标记周期
- 混合收集
- 如果需要,可能进行 FullGC
并发标记周期则分为:初始标记、根区域扫描、并发标记、重新标记、独占清理、并发清理阶段。其中初始标记、重新标记、独占清理是独占式的,会引起停顿。并且初始标记会引发一次新生代 GC。在这个阶段,所有将要被回收的区域会被 G1 记录在一个称之为 Collection Set 的集合中。
混合回收阶段会首先针对 Collection Set 中的内存进行回收,因为这些垃圾比例较高。G1 回收器的名字 Garbage First 就是这个意思,垃圾优先处理的意思。在混合回收的时候,也会执行多次新生代 GC 和 混合 GC,从而来进行内存的回收。
必要时进行 Full GC。当在回收阶段遇到内存不足时,G1 会停止垃圾回收并进行一次 Full GC,从而腾出更多空间进行垃圾回收。
相关参数
打开 G1 收集器,我们可以使用参数:`-XX:+UseG1GC。
设置目标最大停顿时间,可以使用参数:-XX:MaxGCPauseMillis
。
设置 GC 工作线程数量,可以使用参数:-XX:ParallelGCThreads
。
设置堆使用率触发并发标记周期的执行,可以使用参数:-XX:InitiatingHeapOccupancyPercent
。
垃圾回收的几种类型
Minor GC
从年轻代空间回收内存被称为 Minor GC,有时候也称之为 Young GC。对于 Minor GC,你需要知道的一些点:
- 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以 Eden 区越小,越频繁执行 Minor GC。
- 当年轻代中的 Eden 区分配满的时候,年轻代中的部分对象会晋升到老年代,所以 Minor GC 后老年代的占用量通常会有所升高。
- 质疑常规的认知,所有的 Minor GC 都会触发 Stop-The-World,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的,因为大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果情况相反,即 Eden 区大部分新生对象不符合 GC 条件(即他们不被垃圾回收器收集),那么 Minor GC 执行时暂停的时间将会长很多(因为他们要JVM要将他们复制到 Survivor 区或老年代)。
Major GC
从老年代空间回收内存被称为 Major GC,有时候也称之为 Old GC。
许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。
Minor GC 作用于年轻代,Major GC 作用于老年代。 分配对象内存时发现内存不够,触发 Minor GC。Minor GC 会将对象移到老年代中,如果此时老年代空间不够,那么触发 Major GC。因此才会说,许多 Major GC 是由 Minor GC 引起的。
Full GC
Full GC 是清理整个堆空间 —— 包括年轻代、老年代和永久代(如果有的话)。因此 Full GC 可以说是 Minor GC 和 Major GC 的结合。
当准备要触发一次 Minor GC 时,如果发现年轻代的剩余空间比以往晋升的空间小,则不会触发 Minor GC 而是转为触发 Full GC。因为JVM此时认为:之前这么大空间的时候已经发生对象晋升了,那现在剩余空间更小了,那么很大概率上也会发生对象晋升。既然如此,那么我就直接帮你把事情给做了吧,直接来一次 Full GC,整理一下老年代和年轻代的空间。
另外,即在永久代分配空间但已经没有足够空间时,也会触发 Full GC
Stop-The-World
Stop-The-World,中文一般翻译为全世界暂停,是指在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔。
在 Stop-The-World 这段时间里,所有非垃圾回收线程都无法工作,都暂停下来。只有等到垃圾回收线程工作完成才可以继续工作。可以看出,Stop-The-World 时间的长短将关系到应用程序的响应时间,因此在 GC 过程中,Stop-The-World 的时间是一个非常重要的指标。
JDK性能监视
JPS查看所有的Java进程
使用jsp
,不加参数查看所有的Java进程
1 |
|
参数 | 含义 |
---|---|
-q | 指定jps只输出进程ID |
-m | 输出传递给Java进程的参数 |
-l | 输出主函数的完整路径 |
-v | 显示传递给Java虚拟机的参数 |
虚拟机统计信息
jstat是观察Java堆的详情
1 |
|
其中 option 可以由以下值构成。
参数 | 含义 |
---|---|
-class | 监视类装载、卸载数量、总空间以及类装载所耗费的时间 |
-gc | 监视Java堆状况,包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息 |
-gccapacity | 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间 |
-gcutil | 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比 |
-gccause | 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因 |
-gcnew | 监视新生代GC状况 |
-gcnewcapacity | 监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间 |
-gcold | 监视老年代GC状况 |
-gcoldcapacity | 监视内容与-gcold基本相同,输出主要关注使用到的最大、最小空间 |
-gcpermcapacity | 输出永久代使用到的最大、最小空间 |
-compiler | 输出JIT编译器编译过的方法、耗时信息 |
-printcompilation | 输出已经被JIT编译的方法 |
例如:我们用jstat命令来监视一个LVMID为25656的JVM进程。
1 |
|
参数 | 含义 |
---|---|
S0、S1 | 表示Survivor0、Survivor1,还未使用。 |
E | 表示Eden区使用了12.05%的空间。 |
O | 表示老年代还未使用。 |
P | 表示永久代使用了14.17%的空间 |
YUC、YGCT | 表示从程序运行以来一共发生了0次Minor GC(YGC,Young GC),总共耗时0秒。 |
FGC、FGCT | 表示从程序运行以来一共发生了0次Full GC(FGC,Full GC),总共耗时0秒。 |
查看虚拟机参数
jinfo 可以用来查看正在运行的 Java 应用程序的扩展参数,甚至支持在运行时,修改部分参数。它的基本语法是:
1 |
|
执行例子,查询 CMSInitiatingOccupancyFraction 参数值
导出堆到文件
jmap 是一个多功能命令,可以生成 Java 程序的 Dump 文件,也可以查看堆内对象实例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。
1 |
|
执行样例,使用 jmap 生成一个正在运行的 IDEA的 dump 快照文件的例子。例子中的25656是通过jps名称查询到的LVMID。
堆分析工具
jhat 命令用于分析 Java 应用的对快照内存。Sun JDK 提供了 jhat 命令与 jmap 搭配使用,来分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,可以在浏览器中查看。下面我们用 jhat 来分析上面生成的 dump.bin 文件:
查看线程堆栈
jstack 命令用于导出 Java 应用程序的线程堆栈。jstack命令格式:
1 |
|
下面使用jstack查看一个线程对战的例子:
远程主机信息收集
jstad 命令用于收集远程主机信息。
多功能命令行
jcmd 命令可以针对给定的 Java 虚拟机执行一条命令。
JVM常见参数配置
代码
1 |
|
堆栈空间配置
因为Stack的特性,JVM中99%的问题都出在了堆和方法区中,所以需要对此调优,均基于JDK 1.8虚拟机
堆配置
我们使用-Xms来设置堆的初始化大小,-Xmx来设置堆的最大空间大小,需要先编译成class文件
1 |
|
1 |
|
年轻代配置
使用-Xmn来指定年轻代内存的大小,老年代的大小等于堆的内存大小监狱年轻代的大小,加上-XX:+PrintGCDetails
,来查看具体的的内存大小。
1 |
|
1 |
|
1 |
|
Eden区域
在年轻代中,分为Eden,From,To三个区域,使用-XX:SurvivorRatio
这个参数,-XX:SurvivorRatio = eden/from = eden/to
,如果设置参数为2,Eden占50%,剩下两个各占25%,
1 |
|
其中total=eden+from,因为from和to区域只会有一个区域正在使用
元空间
元空间是在使用本机的内存大小,作用和永久代类似,都是对JVM规范中方法区的实现,需要使用参数来调制其大小,但在 JDK1.8 之时,永久代被移除,取而代之的是元空间(Metaspace)。在元空间这块内存中,有两个参数很相似,它们是: -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize。
当metaspace大小到达MetaspaceSize时会执行GC
1 |
|
栈大小
使用-Xss指定最大栈空间为2m
1 |
|
直接内存
在 JVM 中还有一块内存,它独立于 JVM 的堆内存,它就是:直接内存。我们可以使用 -XX:MaxDirectMemorySize 设置最大直接内存。如果不设置,默认为最大堆空间,即 -Xmx。
1 |
|
上面的启动命令设置直接内存最大值为 50M。
当直接内存使用达到设置值时,就会触发垃圾回收。如果不能有效释放足够空间,就会引发直接内存溢出导致系统的 OOM。
JVM参数查看命令
打印显式参数
1 |
|
打印隐式参数
1 |
|
打印所有的系统参数
1 |
|
只是其中一部分
类追踪信息配置
跟踪类的加载和卸载
JVM会在启动时去加载类信息,如何得知它加载了哪些,卸去哪些。
1 |
|
加载类的加载信息
1 |
|
类的卸载信息基本上没有
加载类的卸载信息
1 |
|
打印实例柱状信息
该参数表示遇到 Ctrl-Break 后打印类实例的柱状信息,与 jmap -histo 功能相同。
了解了这些参数,能够让我们更好地了解哪些类已经被加载到 JVM 中,从而更好地排查问题。最后来个总结,加强记忆:
1 |
|
GC日志配置
代码
1 |
|
打印GC日志
1 |
|
打印详细的GC日志
1 |
|
从下面的日志可以看出,该参数能打印出更加详细的 GC 信息,包括:年轻代的信息、永久代的信息。
1 |
|
GC前后打印堆的信息
1 |
|
1 |
|
打印GC发生的时间
表示从JVM启动到GC发生的时间
1 |
|
1 |
|
打印程序执行的时间
-XX:+PrintGCApplicationConcurrentTime
1 |
|
1 |
|
打印停顿时间
-XX:+PrintGCApplicationStoppedTime
打印由于GC造成的时间停顿
1 |
|
1 |
|
保存GC日志
-Xloggc
,输出目录在本目录下
1 |
|
1 |
|
Java历程
JDK JRE
JRE (Java Runtime Envrionment),Java运行时环境,仅包含Java程序的必须组件,例如Java虚拟机和Java核心库
JDK(Java Development Kit),除了包含JRE以外,还包含其他的开发和诊断工具
如果只需要运行JRE就可以了,如果需要开发则需要JDK
JDK J2SE
其实 JDK 是指包含了开发、诊断工具的一个组件,在JDK 1.2发布时,就分成了J2SE,J2EE,J2ME,这三个分别是三种不同的技术体系,在不同的开发环境中使用
SE是指 Standard Environment,在标准开发环境的技术,包含的许多核心类,例如:数据库连接、网络编程、接口定义等。J2SE 技术体系主要用于桌面应用软件的编程。正因为 J2SE 包含了 JDK 核心类,所以在我们下载 JDK 时,你会看到其实下载处的文字说明是「Java SE 11.0.1 is the latest release for JDK 11。
EE是指Enterpise Environment,是企业级应用的技术,包含很多JavaWeb开发的类,例如 Servlet,JSP等,J2EE 技术体系主要用于分布式的网络程序的开发,如电子商务网站等。
ME 是指(Java 2 Micro Edition),是嵌入式技术体系,它包含 J2SE 中的一部分类。J2ME 技术体系主要用于消费类电子产品的软件开发,例如:手机、PDA、寻呼机等。
Java SE J2SE
其实是同一样东西,在 JDK 1.7发布后更名为 Java SE 版本号,比起之前的命名方式,其增加了 JDK 的版本号,能够更加清晰地表明技术体系所属的 JDK 版本。
JDK 7 和 JDK 1.7也类似