JVM

官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
JVM参数配置参考https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html


Java Virtual Machine(Java虚拟机),是C语言开发的虚拟出来的一个“操作系统”,在这个“操作系统”里面运行java程序的class字节码文件;不同的电脑操作系统有不同的JVM版本。
JVM启动之后,在我们的电脑上或者服务器上表现出来的就是一个java进程。
能在JVM上运行的编程语言都是把编写好的源代码编译成JVM能识别的.class字节码文件,然后再JVM上运行;(javac编译器将.java文件编译为.class文件)

JDK、JRE、JVM

类加载器

通过一个类的全限定名来获取描述此类的二进制字节流;
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器,只有父类无法完成加载时子类加载器才会尝试加载;
双亲委派模型(jdk1.2+)

  • Bootstrap ClassLoader启动类加载器(C++ 实现,是虚拟机的一部分);
  • Extension ClassLoader 扩展类加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
  • Application ClassLoader 应用程序加载器(Java实现)
  • Custom ClassLoader 自定义类加载器(java实现,继承 jdk 的 ClassLoader)

除了启动类加载器之外,其他类加载器都有自己的父类加载器;

// null(bootstrapClassLoader c++语言实现,没有名称)
String.class.getClassLoader();
//sun.misc.Launcher$ExtClassLoader@4f7c7a2d
MyTest.class.getClassLoader().getParent();
// sun.misc.Launcher$AppClassLoader@18ab4ac2
MyTest.class.getClassLoader();

JVM内存部分(运行时数据区)

程序计数器

  1. 记录程序执行位置、行号;
  2. 一块很小的区域;
  3. 线程私有;
  4. 不存在OutOfMemoryError;
  5. 无GC回收;

虚拟机栈

虚拟机栈是采用了一种栈的数据结构,入口和出口只有一个,分为入栈和出栈,先进后出;虚拟机栈主要是执行方法;
A方法调用B方法,B方法调用C方法,(A–> B–> C),方法执行就是压栈,方法执行结束就出栈。

  • 局部变量表:是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量;
  • 操作数栈:也叫操作栈,它是一个先进后出的栈 (FILO),当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是入栈和出栈操作,一个完整的方法执行期间往往包含多个这样入栈/出栈的过程;
  • 动态链接:一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池,所以需要在运行时动态将这些符号引用转化为直接引用;
  • 返回地址:方法不管是正常执行结束还是异常退出,需要返回方法被调用的位置;

虚拟机栈的特点

  1. 线程私有;
  2. 方法执行会创建栈帧,存储局部变量表等信息;
  3. 方法执行入虚拟机栈,方法执行完出虚拟机栈;
  4. 栈深度大于虚拟机所允许StackOverflowError;
  5. 栈需扩展而无法申请空间OutOfMemoryError;
  6. 栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;
  7. 栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;
  8. 该区域不会有GC回收;

本地方法栈

  1. 与虚拟机栈基本类似;
  2. 区别在于本地方法栈为Native方法服务;
  3. Sun HotSpot将虚拟机栈和本地方法栈合并;
  4. 有StackOverflowError和OutOfMemoryError;
  5. GC不会回收该区域;

栈的OutOfMemoryError溢出一般是在多线程条件下可能会产生,建立过多的线程,每个线程的运行时间又比较长,可能产生栈的OutOfMemoryError溢出;
单线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配时,虚拟机抛出StackOverFlowError的错误异常;
线程私有部分的整体特征:
是线程私有的,随着线程执行结束而结束(JVM就销毁了虚拟机栈里面的栈帧),是比较有规律的,问题会少一些,出问题比较多的是线程共享的部分,也就是堆和方法区(元空间);

方法区(元空间)

方法区(jdk 1.7后合并到了堆)
方法区在JDK1.8称为元空间(Metaspace),元空间与堆不相连,但与堆共享物理内存;
方法区(元空间)的特点

  1. 线程共享;
  2. 存储 类信息、常量、运行时常量池、静态变量、即时编译器编译后的代码等数据;
  3. HotSpot虚拟机上将方法区叫永久代;(1.7及之前的版本)
  4. 垃圾收集很少光顾该区域(无GC回收);
  5. 方法区通过-XX:MaxPermSize设置最大值;(1.7及之前的版本)
    (元空间:1.8是-XX:MaxMetaspaceSize=48m)
  6. 空间不够分配时OutOfMemoryError;

  • -XX:+PrintGCDetails用于打印GC日志;
  • -XX:+PrintGCDateStamps用于打印对应的时间戳;
  • -XX:-UseCompressedClassPointer表示在Metaspace中不要开辟出一块新的空间(Compressed Class Space),如果开辟这块空间的话,默认大小是1G,所以我们关闭该功能;
  • -XX:MetaspaceSize=20M
  • -XX:MaxMetaspaceSize=20m

方法区/元空间溢出(OutOfMemoryError:Metaspace)
7. 方法区也称永久代(1.7及之前的版本);
8. 方法区存放class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,比如通过反射大量生成动态类(Class<?>)填充该区域即会发生内存溢出

JDK1.6及之前:有永久代,常量池在方法区;
JDK1.7:从某个版本开始已去除永久代,常量池1.7放入堆中;
JDK1.8及之后:无永久代,常量池1.8在元空间;
在jdk1.7及jdk1.8中不会报OutOfMemoryError:PermGen space;
jdk1.8元空间设置大小:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

  1. 线程共享;
  2. 内存中最大的区域;
  3. 虚拟机启动时创建;
  4. 存放所有实例对象或数组
  5. GC垃圾收集器的主要管理区域;
  6. 可分为新生代(1/3)、老年代(2/3);
  7. 新生代更细化可分为Eden(8/10)、From Survivor(1/10)、To Survivor(1/10);
  8. 可通过-Xmx、-Xms调节堆大小;
  9. 无法再扩展java.lang.OutOfMemoryError: Java heap space

堆溢出
不断创建对象又不释放,当对象到达一定数量,无堆空间将产生堆内存溢出;
被持有引用无法释放就溢出,未被持有引用,GC满了就回收
运行jdk的bin目录下jvisualvm.exe图形工具,可以查看当前系统中java进程的运行情况(方法、GC、堆、元空间等)

  • 内存泄漏:GC Roots到对象之间有可达路径而无法收集;
  • 内存溢出:GC Roots到对象之间无可达路径,可以被收集,但对象还需要存活着,此时可根据物理机内存适当调大虚拟机堆参数-Xms、-Xmx,分析代码是否对象生命周期是否过长、对象是否持有状态时间过长;

堆中对象的创建过程

  • 当通过 new 创建对象时,首先检查这个new指令的参数是否能在元空间中定位到一个类的符号引用(Class信息),并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,执行相应的类加载;
  • 类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确定),在堆的空闲内存中划分一块区域(‘指针碰撞《内存规整》’(内存顺序排列,指针后移)或‘空闲列表《内存交错》’(内存随机排列,记录空闲地址)的分配方式);
  • 由于堆中分配内存非常频繁,为了避免多个线程同时分配堆内存时的冲突,虚拟机采用CAS和失败重试方式保证操作的线程安全,同时虚拟机还有另一套设计就是把每个线程分配堆内存的动作隔离开,即每个线程预先在堆中分配一块内存,称为线程分配缓冲(TLAB->Thread Local Allocation Buffer),线程先在自己的TLAB上分配,分配完了再CAS同步,这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全;
  • 内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
  • 执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成;

对象的内存布局

在 HotSpot 虚拟机中,分为 3 块区域:

  • 对象头(Header):包含两部分
    • 第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’;
    • 第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;
存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记
偏向线程ID、偏向时间戳,对象分代年龄01可偏向
  • 实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);
  • 对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍;

对象的访问
使用对象时,我们是通过栈上的 reference 引用来操作堆上的具体对象;
Sun Hotspot虚拟机使用直接指针访问具体对象;

JVM垃圾回收

垃圾收集Garbage Collection 简称GC;
线程私有部分:程序计数器、虚拟机栈、本地方法栈;随着线程创建和结束,不需要回收。
线程共享部分:堆、方法区;需要考虑垃圾回收问题。

可达性分析算法

JVM是通过可达性分析算法来判断一个对象是否需要被回收,其实现思想是通过一个GC Root对象为起始根节点,然后逐个往下去搜索各个对象,看GC Root到对象之间有没有可达路径,如果没有可达路径则表示该对象是不可用的,可以被回收;
JVM中如下几种对象可以作为GC Root

  • 虚拟机栈本地变量表中引用类型所引用的对象;
  • 方法区/元空间中类的静态变量所引用的对象;
  • 方法区/元空间中类的常量所引用的对象;
  • 本地方法栈中Native方法里所引用的对象;

判断是否有可达路径主要是通过 引用 去分析,Java中的引用分为4种:(强度依次减弱)

  1. 强引用(Strong Reference):是最普遍的引用,比如 User user = new User();垃圾收集器不会回收强引用;
  2. 软引用(Soft Reference):表示一些对象还有用,但是也不是必须要用的,在系统内存不足时,可以被垃圾回收器回收;
  3. 弱引用(Weak Reference):表示一些不是必须的对象,当开始垃圾收集时,无论内存是否足够,都会回收弱引用对象;
  4. 虚引用(Phantom Reference):是一种特殊的引用,也是最弱的引用关系,用来实现Object.finalize功能,在开发中很少使用;

方法区/元空间的回收

在JVM的垃圾回收中,堆内存是回收最频繁也是最多的,方法区/元空间的垃圾收集效率非常低,所以JVM规范中并没有要求一定要回收方法区/元空间,如果方法区/元空间有无用的类信息、常量池,JVM不是必须要回收的;
Hotspot 虚拟机默认会进行类的卸载,如果不想卸载无用的类,可以加上参数-Xnoclassgc;
查看类的加载和卸载参数:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -verbose:class -XX:+TraceClassLoading -XX:+TraceClassUnloading

方法区/元空间垃圾回收主要两部分内容:

  • 废弃常量:一般是判断没有任何对象引用该常量;
  • 无用的类:要满足以下三个条件
    1. 该类所有的实例都已经回收;
    2. 加载该类的 ClassLoader 已经被回收;
    3. 该类对应的 Class 对象没有任何地方被引用;

JVM回收对象的两次标记过程

  • 第一次标记
    如果对象进行可达性分析算法之后没有发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选;
    • 筛选条件:判断此对象是否有必要执行finalize()方法;
    • 筛选结果:当对象没有覆盖finalize()方法或者finalize()方法已经被JVM执行过,则判定为可回收对象,如果对象有必要执行finalize()方法,则被放入F-Queue队列中,稍后在JVM自动建立低优先级的Finalizer线程(可能多个线程)中触发这个方法;
  • 第二次标记
    GC对F-Queue队列中的对象进行二次标记;
    如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么第二次标记时则会将它移出“即将回收”集合,如果此时对象还没成功逃脱,那么只能被回收了;

finalize() 是Object类的一个空方法、该方法是被垃圾收集器所调用,一个对象的finalize()方法只会被垃圾收集器自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;
不提倡在程序中调用finalize()来进行对象的自救,因为该方法执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),无法保证各个对象的调用顺序(甚至有不同线程中调用);

JVM垃圾回收器回收算法

标记-清除算法

最基础的收集算法;
标记:标记出所有需要回收的对象,经过两次标记的对象就可以判定可以回收了;
清除:两次标记后,对还在“ 即将回收 ”集合的对象进行回收;

优点:基于最基础的可达性分析算法,实现简单,后续的收集算法都是基于这种思想实现的;
缺点:标记和清除效率不高,产生大量不连续的内存碎片,导致创建大对象时找不到连续的空间,不得不提前触发另一次的垃圾回收;

复制算法

将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉;

优点:实现简单,效率高,解决了标记-清除算法导致的内存碎片问题;
缺点:代价太大,将可分配内存缩小了一半,效率随对象的存活率升高而降低,一般虚拟机都会采用该算法来回收新生代;

标记-整理算法

标记-整理算法是根据老年代的特点而产生的;
标记:标记过程与上面的标记-清理算法一致,也是基于可达性分析算法,也是两次标记;
整理:和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理。让存活对象都向一端移动,然后直接清理掉边界以外的内存;

优点:不会像复制算法那样划分两个区域,提高了空间利用率,不会产生不连续的内存碎片;
缺点:效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率变低;

分代收集算法

现在一般虚拟机的垃圾收集都是采用“ 分代收集 ”算法;
根据对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,JVM根据各个年代的特点采用不同的收集算法;
新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;
老年代中,因为对象存活率较高,采用标记-清理、标记-整理算法来进行回收;

JVM垃圾收集器

一共有7种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用,垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器;
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1

Serial收集器

-XX:+UseSerialGC
新生代收集器,最早的收集器,单线程的,收集时需暂停用户线程的工作。所以有卡顿现象,效率不高。但Serial收集器简单,不会有线程交互的开销,是client模式下默认的垃圾收集器。
java -version查看默认模式: -client, -server(jdk1.8);

ParNew收集器

-XX:+UseParNewGC
它是新生代收集器,就是Serial收集器的多线程版本,大部分基本一样,配置参数也一致,单CPU下,ParNew还需要切换线程,可能还不如Serial。
Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,后者CMS收集老年代。
“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;
“-XX:ParallelGCThreads=2”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

Parallel Scavenge收集器

-XX:+UseParallelGC
它是新生代收集器,基于复制算法,并行的多线程收集器(与ParNew收集器类似),侧重于达到一个可控的吞吐量,虚拟机运行100分钟,垃圾收集花1分钟,则吞吐量为99%。
它提供两个参数设置吞吐量:
-XX:MaxGCPauseMillis 设置大于0的毫秒数,每次GC的时间将保持不超过设置的值。
–XX:GCTimeRatio 垃圾收集时间占整个虚拟机时间的比率

Serial Old收集器

它是Serial收集器的老年代版本,同Serial一样,单线程,可在Client模式下使用,也可在Server模式下使用,采用标记-整理算法;

Parallel Old收集器

-XX:+UseParallelOldGC
是Parallel Scavenge的老年代版本,多线程,标记整理算法,它是在1.6开始才提供。
在注重吞吐量和CPU资源的情况, Parallel Scavenge新生代+ Parallel Old老年代是一个很好的搭配。

CMS收集器

-XX:+UseConcMarkSweepGC
全称Concurrent Mark Sweep,是追求最短回收停顿时间为目标的收集器,互联网B/S结构的服务器端特别适合此收集器。
基于标记清除算法,它的运作过程分为4个阶段,初始标记、并发标记、重新标记、并发清除,其中初始标记和重新标记需要暂停用户线程,其他是并发执行,所以总体上暂停时间更短。
CMS收集器的缺点:

  • 并发收集会占用CPU资源,特别是cpu数量小的服务器下,会占用用户线程,导致性能下降;
  • 会产生浮动垃圾,因为你并发清除的时候用户线程可能还在产生垃圾,这些垃圾没有清除,而且你不能让老年代填满了再清除,你要给用户线程留一定空间,所以jdk1.5默认是老年代65%了就触发回收,jdk1.6则提升到92%,如果预留老年代不够用户线程使用,则启用Serial Old收集,这就会暂停用户线程,导致性能下降。
  • 这个标记整理算法清理后会产生碎片空间,如果此时要分配一个大对象,则无连续空间,需要一次full gc,性能下降;
## G1收集器 -XX:+UseG1GC 目前最前沿最先进的垃圾收集器,从JDK1.7u4开始可以使用; G1收集器的特点: - 并发和并行,充分利用多核cpu,让用户线程不暂停; - 保留了分代收集的特性,不需要其他收集器配合完成各代的回收; - 空间整合,G1不会产生碎片空间,不会触发full gc去回收碎片; - 可预测的停顿,G1追求了停顿预测模型,在一段时间内停顿不超过多少毫秒等;

GC日志

  • -XX:+PrintGC
    允许在每个GC上打印消息,默认禁用。
  • -XX:+PrintGCApplicationConcurrentTime
    启用打印自上次暂停(例如GC暂停)以来经过的时间,默认禁用。
  • -XX:+PrintGCApplicationStoppedTime
    允许打印暂停(例如GC暂停)持续的时间,默认禁用。
  • -XX:+PrintGCDateStamps
    启用在每个GC上打印日期戳,默认禁用。
  • -XX:+PrintGCDetails
    允许在每个GC上打印详细消息,默认禁用。
  • -XX:+PrintGCTaskTimeStamps
    启用为每个GC工作线程任务打印时间戳,默认禁用。
  • -XX:+PrintGCTimeStamps
    启用在每个GC上打印时间戳,默认禁用。
  • -Xloggc:filename
    设置要将详细GC事件信息重定向到其中进行日志记录的文件。写入此文件的信息类似于-verbose:gc的输出,其中包含自每个记录的事件之前的第一个gc事件以来经过的时间。-Xloggc选项重写-verbose:gc,如果这两个选项都是用同一个java命令给出的。
  • -XX:+HeapDumpOnOutOfMemoryError
    启用在引发Java.lang.OutOfMemoryError异常时使用堆探查器(HPROF)将Java堆转储到当前目录中的文件。可以使用-XX:heap dump path选项显式设置堆转储文件的路径和名称,默认禁用。
  • -XX:HeapDumpPath=path
    设置在设置-XX:+HeapDumpOnOutOfMemoryError选项时用于写入堆分析器(HPROF)提供的堆转储的路径和文件名。默认是在当前工作目录中创建的,名为java_pidpid.hprof,其中pid是导致错误的进程的标识符。
    显式设置默认文件(%p表示当前进程标识符):-XX:HeapDumpPath=./java_pid%p.hprof

分析阅读GC日志

JDK命令行监控工具

官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/index.html

  • jinfo
    查看正在运行的Java程序的扩展参数信息 jinfo pid
    查看JVM的参数 jinfo -flags pid
    查看java系统属性 jinfo -sysprops pid
  • jps
    显示所有的HotSpot虚拟机进程,可选参数:
    -q 只输出LVMID,LVMID与linux的PID一致
    -m 输出虚拟机进程启动时传给主类main()的参数
    -l 输出主类的全名或jar包的路径
    -v 输出虚拟机进程启动时JVM参数
  • jstat
    虚拟机监控命令,收集虚拟机运行时的各方面数据,可选参数:
    -class 监视类装载、卸载及耗时,
    -gc 监视java堆各个区的情况
    -gccapacity 与gc基本相同,但更关注java堆个区域使用到的最大、最小空间
    -gcutil 与gc基本相同,但更关注已使用空间占总空间的百分比
    -gccause 与gcutil功能一样,但会额外输出导致上一次GC产生的原因
    -gcnew 监视新生代GC状况
    -gcnewcapacity 与gcnew一致,但更关注使用到的最大、最小空间
    -gcold 监视老年代GC状况
    -gcoldcapacity 与gcold一致,但更关注使用到的最大、最小空间
    -gcpermcapacity 输出永久代使用到的最大、最小空间
    -gcmetacapacity 元空间GC情况
    -compiler 输出JIT编译器编译过的方法、耗时等信息
    -printcompilation 输出已经被JIT编译的方法
    其他
    S0C:年轻代中第一个survivor(幸存区)的容量 (字节)
    S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
    S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
    S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
    EC:年轻代中Eden(伊甸园)的容量 (字节)
    EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)
    OC:Old代的容量 (字节)
    OU:Old代目前已使用空间 (字节)
    PC:Perm(持久代)的容量 (字节)
    PU:Perm(持久代)目前已使用空间 (字节)
    YGC:从应用程序启动到采样时年轻代中gc次数
    YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
    FGC:从应用程序启动到采样时old代(全gc)gc次数
    FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
    GCT:从应用程序启动到采样时gc用的总时间(s)
    NGCMN:年轻代(young)中初始化(最小)的大小 (字节)
    NGCMX:年轻代(young)的最大容量 (字节)
    NGC:年轻代(young)中当前的容量 (字节)
    OGCMN:old代中初始化(最小)的大小 (字节)
    OGCMX:old代的最大容量 (字节)
    OGC:old代当前新生成的容量 (字节)
    PGCMN:perm代中初始化(最小)的大小 (字节)
    PGCMX:perm代的最大容量 (字节)
    PGC:perm代当前新生成的容量 (字节)
    S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
    S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
    E:年轻代中Eden(伊甸园)已使用的占当前容量百分比
    O:old代已使用的占当前容量百分比
    P:perm代已使用的占当前容量百分比
    S0CMX:年轻代中第一个survivor(幸存区)的最大容量 (字节)
    S1CMX :年轻代中第二个survivor(幸存区)的最大容量 (字节)
    ECMX:年轻代中Eden(伊甸园)的最大容量 (字节)
    DSS:当前需要survivor(幸存区)的容量 (字节)(Eden区已满)
    TT: 持有次数限制
    MTT:最大持有次数限制
  • jmap
    可用于生成java堆转储快照文件
    可选参数:(有些参数在window下无效)
    -dump,如:jmap -dump:format=b,file=/opt/dump.bin pid
    -finalizerinfo 等待执行finalizer的对象
    -heap 显示java堆栈信息,如jmap -heap pid
    -histo 显示堆中对象统计信息,如jmap -histo pid
    -permstat 显示永久代的内存情况
    -F 当-dump生成堆栈无响应,可以使用-F强制生成堆栈信息
  • jhat
    用于分析虚拟机生成的堆转储快照文件
  • jstack
    生成虚拟机中当前执行的线程快照信息,目的是查出当前线程有没有卡顿、长时间没有响应的情况,卡顿等待的原因可能为等待外部资源、数据库连接、网络资源、设备资源、死循环、锁等,可选参数:
    -F 当正常输出无响应时强制输出线程堆栈;
    -l 除显示堆栈外,显示锁的附加信息;
    -m 如果调用到本地方法,显示C/C++的堆栈信息;

JVM可视化监视

JCnsole(jdk1.5+)

Java监控与管理控制台
jdk安装目录bin下双击jconsole.exe启动;
概要是总体信息
内存标签页相当于jstat命令
线程标签页相当于jstack命令

VisualVM(jdk1.6 u7+)

java虚拟机监控工具
在jdk安装目标bin下双击jvisualvm.exe启动;
文档:https://visualvm.github.io/index.html

注意:使用图形化工具,要开启JMX;JDK1.5及之前的版本要手动开启JMX功能,需在程序启动前加上参数-Dcom.sun.management.jmxremote。

JVM内存溢出分析

MAT(Memory Analysis Tool):分析dump文件(堆转储快照文件)
官网下载:https://www.eclipse.org/mat/

JVM参数类型

官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  • 标准参数
    Standard Options
    在各个版本中都有,是比较稳定的,以-开头;
    -help、-server、-client、-version、-classpath…
  • 非标准化X参数
    Non-Standard Options
    是不稳定的,在各个jvm版本中不一样,以-X开头,可以使用 java -X 查看可选参数;
  • 非标准化XX参数
    Advanced Runtime Options
    Advanced JIT Compiler Options
    Advanced Serviceability Options
    Advanced Garbage Collection Options
    也是不稳定的,在各个jvm版本中不一样,主要用于jvm调优和debug,以-XX开头;
    非标准化XX参数分两类
    • boolean类型
      -XX:[+-]<name>,+表示启用,-表示禁用
      比如:-XX:+UseG1GC
    • 非boolean类型
      -XX:<key>=<value>
      -XX:MaxGCPauseMillis=200

内存溢出问题分析

  • 自动导出内存溢出堆栈转存文件
    添加JVM参数启动java程序
    -Xmx10m
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -Xloggc:d:/logs/gc.log
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=d:/logs/heapdump.hprof
  • 手动导出内存溢出堆栈转存文件
    jmap -dump:format=b,file=d:/logs/heapdump.hprof pid

使用 MAT 工具打开 heapdump.hprof 查看溢出怀疑

线程问题监控

官方文档:https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr034.html#sthref31
线程状态

线程状态描述
NEW线程尚未启动
RUNNABLE线程在JVM中执行
BLOCKED线程在等待监视器锁定时被阻塞
WAITING线程正在无限期地等待另一个线程来执行特定的操作
TIMED WAITING线程等待另一个线程执行一个操作,等待时间最长为指定的等待时间
TERMINATED线程已退出
  • jstack pid > xxx.txt
    生成进程(pid)的线程运行信息到指定文件
  • top
    查看 cpu 进程使用情况;
  • top -p pid -H
    查看进程中线程的使用情况

监控本地及远程进程

visualvm,文档:https://visualvm.github.io/index.html
Arthas:https://arthas.aliyun.com/

连接远程tomcat(JXM连接)

开启远程访问:

tomcat bin目录下编辑 catalina.sh,在JAVA_OPTS位置加入这行,修改ip和端口

JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8080 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=192.168.10.110"

远程tomcat启动后在 visualvm 中添加远程ip和端口即可监控。

开启远程访问并开启认证:

JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8080 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access"

JAVA_HOME/jre/lib/management下

  1. 将jmxremote.access和jmxremote.password.template 复制到CATALINA_BASE/conf目录下
  2. 重命名jmxremote.password.template为jmxremote.password
  3. 修改jmxremote.access 添加权限:
    monitorRole readonly
    controlRole readwrite
  4. 修改jmxremote.password 添加密码:
    monitorRole 123456
    controlRole 123456
  5. 给文件赋读写权限
    chmod 600 jmxremote.password
    chmod 命令:https://linuxize.com/post/chmod-command-in-linux/

也可以在/root/.bash_profile 最后追加如下配置:

export JAVA_OPTS="-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=8080
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-Djava.rmi.server.hostname=192.168.10.110"

或者在 /etc/profile 文件中配置;
使上述配置生效:source ~/.bash_profile

jstatd连接配置

需要jdk支持;
新建文件jstatd.all.policy(可自定义文件名),内容如下:

grant codebase "file:/usr/local/jdk1.8.0_181/lib/tools.jar" {
    permission java.security.AllPermission;
};

执行命令:&后台启动

jstatd -J-Djava.security.policy=jstatd.all.policy &

GC日志分析

GCeasy

GC日志在线分析工具:https://gceasy.io
打印出GC日志:

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:d:/logs/jvmgc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/logs/heapdump.hprof

将生成的gc日志上传gceasy生成分析报告
堆栈分析:https://heaphero.io
线程分析:https://fastthread.io

GCViewer

https://github.com/chewiebug/GCViewer
下载jar包,通过 java -jar gcviewer-1.36.jar 运行;

GC调优

如何选择垃圾收集器

  • 优先调整堆的大小(-Xms、-Xmx)然后让JVM自己去选择;
  • 如果内存小于100M,使用串行垃圾收集器;
  • 如果是单核CPU,并且没有停顿时间上的要求,采用串行垃圾收集或者让JVM自己选择;
  • 如果允许停顿时间超过1秒,选择并行垃圾收集或者JVM自己选;
  • 如果响应时间最重要,并且不能超过1秒,采用并发垃圾收集器;

并行垃圾收集器

  • -XX:+UseParallelGC 手动指定开启并行垃圾收集,Server模式下默认开启;
  • -XX:ParallelGCThreads=4 指定多少个GC线程,默认CPU>8 n=5/8,CPU<8 n=cpu数
  • -XX:MaxGCPauseMillis 最大GC停顿时间
  • -XX:GCTimeRatio GC时间比例
  • 它的动态内存调整可以通过参数控制
    • -XX:YoungGenerationSizeIncrement
    • -XX:TenuredGenerationSizeIncrement
    • -XX:AdaptiveSizeDecrementScaleFactor
  • 并行垃圾收集有一个自适应的策略,会不断地扩容或缩容来满足停顿时间和吞吐量的要求;

CMS垃圾收集器

  • 并发收集
  • 低延迟、低停顿
  • 老年代垃圾收集

JDK1.8之前大部分web应用都是采用该垃圾收集器;
CMS的调优参数:
-XX:ConcGCThreads 并发的GC线程数
-XX:UseCMSCompactAtFullCollection FullGC后做压缩
-XX:CMSFullGCsBeforeCompaction 多少次FullGC后压缩一次
-XX:CMSInitiatingOccupancyFraction 触发FullGC
-XX:UseCMSInitiatingOccupancyOnly 是否动态调整
-XX:CMSScavengeBeforeRemark FullGC前先YGC

G1垃圾收集器

新生代、老年代收集器;

官方文档:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html

Young GC

新对象进入Eden区
存活对象拷贝到Survivor区
存活对象年龄达到阈值后晋升到Old区;(默认15岁,15次GC)
以上几点在 并行垃圾收集、cms垃圾收集 上都是一样的;

Mixed GC

不是Full GC,回收Young区和部分Old区;
Mixed GC时会进行一次全局标记,当
-XX:InitiatingHeapOccupancyPercent=45时触发;

垃圾收集器追求的两项指标

  • -XX:MaxGCPauseMillis
    停顿时间:垃圾收集时候,应用程序被停顿了多少时间;
  • -XX:GCTimeRatio
    吞吐量:垃圾收集花费的时间与应用程序运行的时间的比例;

JVM调优最好的情况的是 吞吐量最大,而停顿时间最小;

GC调优目标

  • 缩短停顿时间
  • 增大吞吐量

JVM调优的步骤

  1. 打印GC日志;
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:d:/jvm/jvmgc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/jvm/heapdump.hprof -XX:+DisableExplicitGC

-XX:+DisableExplicitGC 禁止在代码中使用System.gc();

  1. 分析GC日志得出与性能相关的问题;
  2. 调优JVM参数提升性能;

此过程在实际生产中是反复调试的过程,通过不断地调整实现最优化,不是一步到位的。

GC调优指南:

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html

首先给调优定下目标
1、最大暂停时间目标
2、吞吐量目标
3、微调直达上述目标达到

  • maximum pause time goal -XX:MaxGCPauseMillis=n:每次GC时程序暂停最多多少毫秒;
  • throughput goal -XX:GCTimeRatio=n: 表示花费总时间百分之多少的CPU时间去运行程序;
  • footprint goal:如果其他目标都达到了,那么首先减少heap size,直到前两个goal不再满足,然后再慢慢增加,直到满足前面两个goal;

调优前的初始参数
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/logs/gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/logs/heapdump.hprof

吞吐量最小停顿时间最大停顿时间平均停顿时间Young GCFull GC
92.75%0.010160.308170.0601583
98.38%0.004180.032180.0161340

G1调优建议
1、年轻代不要使用 -Xmn,-XX:NewRatio 显式设置大小,会覆盖暂停时间的目标;
2、暂停时间不要太严格,吞吐量目标是90%及以上;

GC常用参数

堆、栈、元空间设置

-Xss:每个线程的栈大小
-Xms:初始堆大小,默认物理内存的1/64
-Xmx:最大堆大小,默认物理内存的1/4
-Xmn:新生代大小-XX:NewSize:设置新生代初始大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:YoungGenerationSizeIncrement=30 年轻代动态扩容增量
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
-XX:MetaspaceSize:设置元空间大小
-XX:MaxMetaspaceSize:设置元空间最大允许大小,默认不受限制,JVM Metaspace会进行动态扩展。

垃圾回收日志信息打印

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

垃圾收集器设置

-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParalledlOldGC:设置并行老年代收集器
-XX:+UseConcMarkSweepGC:设置CMS并发收集器
-XX:+UseG1GC:设置G1收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数

并行收集器设置

-XX:ParallelGCThreads:设置并行收集器收集时使用的CPU数。并行收集线程数;
-XX:MaxGCPauseMillis:设置并行收集最大暂停时间
-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

CMS收集器设置

-XX:+UseConcMarkSweepGC:设置CMS并发收集器
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。
-XX:CMSFullGCsBeforeCompaction: 设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled: 允许对类元数据进行回收
-XX:UseCMSInitiatingOccupancyOnly: 表示只在到达阀值的时候,才进行CMS回收;
-XX:+CMSIncrementalMode: 设置为增量模式。适用于单CPU情况
-XX:ParallelCMSThreads: 设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction: 设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理

G1收集器设置

-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
-XX:GCTimeRatio:吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor填充容量(默认50%)
-XX:MaxTenuringThreshold:最大任期阈值(默认15)
-XX:InitiatingHeapOccupancyPercen:老年代占用空间超过整堆比IHOP阈值(默认45%),超过则执行混合收集
-XX:G1HeapWastePercent:堆废物百分比(默认5%)
-XX:G1MixedGCCountTarget:参数混合周期的最大总次数(默认8)

常用JVM参数

-Xms
-Xmx
-XX:NewSize -XX:MaxNewSize 新生代大小 新生代最大大小
-XX:NewRatio 新生代与老年代的比例 -XX:SurvivorRatio eden区与s区的比例
-XX:MetaspaceSize -XX:MaxMetaspaceSize 元空间的大小 元空间的最大大小
-XX:+UseCompressedClassPointers 元空间是否使用类指针压缩,使用的话该区域默认占用1G大小
-XX:+UseCompressedOops ,OOP =“ordinary object pointer”普通对象指针,压缩对象指针,起到节约内存占用的效果,原理,解释器在解释字节码时,植入压缩指令(不影响正常和JVM优化后的指令顺序)。具体逻辑是,当对象被读取时,解压,存入heap时,压缩;
-XX:CompressedClassSpaceSize 设置元空间的类指针压缩区域的大小;
-XX:InitialCodeCacheSize 设置初始CodeCache大小
-XX:ReservedCodeCacheSize 用于设置code cache的最大大小,通常默认是240M
对象分配优先在Eden区分配
大对象直接进入老年代:-XX:PretenureSizeThreshold 用来指定超过这个大小的对象直接放入老年代;
长期存活的对象进入老年代:-XX:MaxTenuringThreshold 设置的是年龄阈值,默认15;-XX:+PrintTenuringDistribution 打印一下对象的年龄信息;-XX:TargetSurvivorRatio 设定survivor区的目标使用率,默认50;
-XX:+UseG1GC 开启使用G1
-XX:G1HeapRegionSize=n region的大小,1-32M之间,最大2048个
-XX:MaxGCPauseMillis=200 最大停顿时间
-XX:G1NewSizePercent 新生代占比
-XX:G1MaxNewSizePercent 新生代最大占比
-XX:G1ReservePercent=10 保留的内存占比以避免to区的溢出
-XX:ParallelGCThreads=n 并行的线程数,会停止应用的
-XX:ConcGCThreads=n 并发线程数,和应用程序一起执行的,默认是1/4的-XX:ParallelGCThreads=n
样例,针对JDK1.6

-server -Xmx4g -Xms4g -Xmn256m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70

-server VM有两种运行模式Server与Client,两种模式的区别在于,Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多;
-Xmx4g 最大堆大小;
-Xms4g 初始堆大小;
-Xmn256m 堆中年轻代大小;
-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;
XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4;
-Xss 每个线程的Stack大小;
-XX:+DisableExplicitGC,这个参数作用是禁止代码中显示调用GC。
-XX:+UseConcMarkSweepGC 并发标记清除(CMS)收集器,CMS收集器也被称为短暂停顿并发收集器;
-XX:+CMSParallelRemarkEnabled 降低标记停顿;
-XX:+UseCMSCompactAtFullCollection: 使用并发收集器时,开启对年老代的压缩;
-XX:LargePageSizeInBytes 指定Java heap的分页页面大小;
-XX:+UseFastAccessorMethods 原始类型的快速优化;
-XX:+UseCMSInitiatingOccupancyOnly 使用手动定义的初始化大小开始CMS收集;
-XX:CMSInitiatingOccupancyFraction 使用cms作为垃圾回收使用70%后开始CMS收集;
对于Tomcat,在tomcat的bin目录下的catalina.sh中设置jvm参数:

JAVA_OPTS=”-server -Xmx4g -Xms4g -Xmn256m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70”

并行垃圾收集器:多个GC线程可以同时收集;
并发垃圾收集器:多个GC线程和应用线程可以同时工作;

代码优化建议

尽量重用对象,不要循环创建对象,比如for循环字符串拼接;
容器类初始化的时候建议指定长度;list、map -->扩容影响性能;
ArrayList随机访问快,添加删除慢,LinkedList添加删除快,随机访问慢;
集合遍历尽量减少重复计算集合size(),使用变量存储;
使用Entry遍历Map;for(Map.entry entry : map.entrySet()) {}
大数组复制采用System.arraycopy(); -->它是native方法
尽量使用基本类型而不使用包装类型;int a = 10; 拆箱使用了.valueOf();
不要手动调用System.gc();
及时释放对象的引用防止内存泄漏,user[i] = null;–>数组、map等要注意;
多用局部变量少用成员变量,减小变量的作用域;
减少同步锁的作用范围,synchronized;
使用ThreadLocal缓存线程不安全的对象;
尽量使用延迟加载;
减少使用反射;
多采用连接池、线程池、对象池 (commons-pool.jar)、缓存等;
及时释放连接资源,数据库连接、IO流、socket连接等;
日志输出生产环境注意级别(info以上),日志参数使用占位符而不是拼接;
logger.info(“user id = {}”, id);

  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值