https://blog.csdn.net/v123411739/article/details/79671697添加链接描述
https://blog.csdn.net/v123411739/article/details/79692477添加链接描述
一、Java虚拟机的内存管理
1.1、运行时数据区
-
PC程序计数器:线程私有,
1、JVM的pc程序计数器的功能是存放伪指令的,更确切的说是存放姜堰执行的指令的地址
2、当虚拟机正在执行的方法是一个本地方法的时候,jvm的pc寄存器存储的值是undefined
3、程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个
4、此内存区域是JVM规范中唯一一个没有规定任何OutOfError情况的区域。 -
本地方法栈:线程私有,为JVM使用到本地方法的服务
-
虚拟机栈:线程私有,Java虚拟机栈和线程同时创建,用于存储赵振。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基
本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了
支持方法调用过程中的动态链接(Dynamic Linking)。
方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
- 堆:
1、是JVM所管理的内存中最大的一块
2、堆是jvm所有线程共享的(堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB))
3、在JVM启动的时候创建
4、唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存
5、Java堆是垃圾收集器管理的主要区域
6、因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
7、java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
8、方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
9、如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
元空间:
在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。 HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。
它和永久代有什么不同的?
存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其
中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久
代内存溢出。
方法区:
方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载 的类型信息、
常量、 静态变量、 即时编译器编译后的代码缓存等数据。
元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载
的类信息的
运行时常量池:
字节码文件中,内部包含了常量池
方法区中,内部包含了运行时常量池
常量池:存放编译期间生成的各种字面量与符号引用
运行时常量池:常量池表在运行时的表现形式
编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。
理解为字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时常量池中取数据,被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。
它们不是一个概念,存放的位置是不同的。一个在字节码文件中,一个在方法区中。
直接内存:
直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分。在JDK 1.4中新加入了NIO(New Input/Output) 类, 引入了一种基于通道(Channel) 与缓冲区 (Buwer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的 DirectByteBuwer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了 在Java堆和Native堆中来回复制数据。
1.2、jvm内存模型
参考:堆栈方法区
每个线程都有自己的缓存(比如栈空间),所有线程都可以访问一些共有的内存(比如堆空间)。
CPU 在执行代码时,可能出现对代码的重排序。
1、编译器重排序:对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序
2、CPU指令重排序:在指令级别,让没有依赖关系的多条指令并行
3、CPU内存重排序:CPU有自己的缓存,指令执行的顺序和写入主内存的顺序不完全一致。
在上述三种重排序中,第三类就是造成"内存可见性"问题的主因
案例:
为了禁止编译器和CPU重排序,在编译器和CPU层面都有对应的指令,也就是内存屏障。这也是JMM和happen-before的底层实现。
编译器的内存屏障,只是为了告诉编译器不要对指令进行重排序。当编译完成之后,这种内存屏障就消失了,CPU并不会感知到编译器中内存屏障的存在。
而CPU的内存屏障是CPU提供的指令,可以有开发者显示调用。
内存屏障是很底层的概念,对于 Java 开发者来说,一般用 volatile 关键字就足够了。但从JDK 8开
始,Java在Unsafe类中提供了三个内存屏障函数。
在理论层面,可以把基本的CPU内存屏障分成四种:
- LoadLoad:禁止读和读的重排序。
- StoreStore:禁止写和写的重排序。
- LoadStore:禁止读和写的重排序。
- StoreLoad:禁止写和读的重排序。
1.3 heap与stack(堆与栈)的差别
- heap是堆,stack是栈;
2)stack的空间由操作系统自动分配和释放,存放函数的参数值、 局部变量的值等。heap上的空间一般由程序员分配和释放,并要指明大小;
3)栈空间有限而且是一块连续的内存区域,堆是很大的自由存储区;
4)C中的malloc函数分配的内存空间就是在堆上,C++是new;
5)程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时的参数传递也在栈上进行
1.4 引用和指针的区别
- 指针是一个变量,存放地址的变量,指向内存的一个存储单元,引用仅是别名;
2)引用必须初始化,指针不必;
3)不存在指向空值的引用,但是存在指向空值的指针;
4)sizeof引用对象得到的是所指对象,变量的大小,sizeof指针得到是指针本身的大小;
5)内存分配上,程序为指针分配内存,不用为引用分配内存
二、JVM 加载机制
2.1 有哪些类加载器
1、jvm支持两种类型的加载器,分别是引导类加载器和 自定义加载器
2、引导类加载器是由c/c++实现的,自定义加载器是由java实现的。
3、jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。
4、按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定
义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
启动类加载器
1、这个类加载器使用c/c++实现,嵌套再jvm内部 2、它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、 resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。 3、并不继承自
java.lang.ClassLoader,没有父加载器
扩展类加载器
1、java语言编写,由sun.misc.Launcher$ExtClassLoader实现
2、从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载;派生于 ClassLoader。
3、父类加载器为启动类加载器
系统类加载器
1、java语言编写,由 sun.misc.Lanucher$AppClassLoader 实现 2、该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类
库;派生于 ClassLoader 3、父类加载器为扩展类加载器 4、通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。
用户自定义类加载器
在日常的Java开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的
加载方式。
2.2 双亲委派模式,哪些场景是打破双亲委派模式
而有了双亲委派模型,黑客自定义的 java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的 java.lang.String 类,最终自定义的类加载器无法加载 java.lang.String 类。或许你会想,我在自定义的类加载器里面强制加载自定义的 java.lang.String 类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在 JVM 中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
双亲委派模式
1、隔离加载类
模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器,内部自定义了好几中类加载器,用于隔离web应用服务器上的不同应用程序。
2、修改类加载方式
除了Bootstrap加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。
3、扩展加载源
比如还可以从数据库、网络、或其他终端上加载
4、防止源码泄漏
java代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。
2.3 JIT即时编译?
java的编译机制是,javac将java源代码转换为java字节码,JVM会逐行解释字节码,对于频繁执行的代码会被即时编译成本地机器码放入codeCache里来提高执行效率
这个过程由java字节码执行引擎中的两个编译器完成,C1与C2编辑器,一个用于客户端(-client,启动快后续执行效率低),一个用于服务器(-server,启动慢后续执行效率高)
三、垃圾回收机制和算法
3.1 java gc 有哪些垃圾回收算法,优缺点
1、标记-清除(Mark-Sweep)
首先标记出所有需要回 收的对象, 在标记完成后,统一回收掉所有被标记的对象, 也可以反过来, 标记存活的对象, 统一回 收所有未被标记的对象
标记-清除算法有两个不足之处:
第一个是执行效率不稳定, 如果Java堆中包含大量对 象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作, 导致标记和清除两个过 程的执行效率都随对象数量增长而降低;
第二个是内存空间的碎片化问题, 标记、 清除之后会产生大 量不连续的内存碎片, 空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2、标记-复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题, 1969年Fenichel提出了一种称为“半区复制”(Semispace Copying) 的垃圾收集算法, 它将可用 内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着 的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。 如果内存中多数对象都是存活的, 这种算法将会产生大量的内存间复制的开销, 但对于多数对象都是可回收的情况, 算法需要复制的就是占少数的存活对象, 而且每次都是针对整个半区进行内存回收, 分配内存时也就不用考虑有空间碎片的复杂情况, 只要移动堆顶指针, 按顺序分配即可
这种算法也有缺点
1)需要提前预留一半的内存区域用来存放存活的对象(经过垃圾收集后还存活的对象),这样导致可用的对
象区域减小一半,总体的GC更加频繁了
2)如果出现存活对象数量比较多的时候,需要复制较多的对象,成本上升,效率降低
3)如果99%的对象都是存活的(老年代),那么老年代是无法使用这种算法的。
3、标记-整理算法
标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存,标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法, 而后者是移动式的。 是否移动回收后的存活对象是一项优缺点并存的风险决策
3.2 哪些对象可以作为 GC Roots
栈帧中的局部变量表中的reference引用所引用的对象
方法区中static静态引用的对象
方法区中final常量引用的对象
本地方法栈中JNI(Native方法)引用的对象
Java虚拟机内部的引用, 如基本数据类型对应的Class对象, 一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError) 等, 还有系统类加载器。
所有被同步锁(synchronized关键字) 持有的对象。
3.3 分代垃圾回收机制
java中不同对象的生命周期是不一样的,不同周期对象课采用不同垃圾回收算法,以提高效率,根据对象活跃程度分为年轻代、年老代、持久代。JVM堆区划分为Eden、Survivor、Tenured/Old区。
年轻代
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分为Eden区和两个Survivor(一般为两个,也可多个)。
年老代
在年轻代中经历N次垃圾回收后,仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代
存放静态文件,如java类、方法等,对垃圾回收没有显著影响
GC: Garbage Collection 垃圾回收
Minor GC
用于清理年轻代,Eden满了,就触发Minor GC ,清理无用对象,把有用对象放到Survivor1或Survivor2中。
Major GC
用于清理老年代。
Full GC
清理年轻代、老年代区域,成本高,对系统性能产生影响
清理过程
- 创建新对象,大多数放在Eden区
- Eden满了(或达到一定比例),触发Minor GC, 把有用的复制到Survivor1, 同时清空Eden区。
- Eden区再次满了,出发Minor GC, 把Eden和Survivor1中有用的,复制到Survivor2, 同时清空Eden,Survivor1。
- Eden区第三次满了,出发Minor GC, 把Eden和Survivor2中有用的,复制到Survivor1, 同时清空Eden,Survivor2。
形成循环,Survoivor1和Survivor中来回清空、复制,过程中有一个Survivor处于空的状态用于下次复制的。 - 重复多次(默认15),没有被Survivor清理的对象,复制到Old(Tenuerd)区.
- 当Old达到一定比例,触发Major GC,清理老年代。
- 当Old满了,触发Full GC。注意,Full GC清理代价大,系统资源消耗高。
注:
- 程序员可以像JVM提出垃圾回收请求,但是实际是否回收由JVM决定,程序员只提出清理请求建议,不能直接清理。
- Full GC清理代价大,系统资源消耗高,很多性能优化是针对Full GC做优化。
3.4 JVM 的 其中垃圾收集器
-
Serial收集器
单线程收集器,“单线程”的意义不仅仅说明它只会使用一个CPU或一个收集线程去完成垃圾收集工作;
更重要的是它在垃圾收集的时候,必须暂停其他工作线程,直到垃圾收集完毕; -
ParNew 收集器
ParNew收集器实质上是Serial收集器的多线程并行版本, 除了同时使用多条线程进行垃圾收集之 外, 其余的行为包括Serial收集器可用的所有控制参数 、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一致, 在实现上这两种收集器也共用了相当多的代码 -
Parallel Scavenge收集器
1.什么是Parallel Scanvenge
又称为吞吐量优先收集器,和ParNew收集器类似,是一个新生代收集器。使用复制算法的并行多线程收集器。
Parallel Scavenge是Java1.8默认的收集器,特点是并行的多线程回收,以吞吐量优先。
2.特点
Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput);
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) (虚拟机总共运行100分钟,垃圾收集时间为1分钟,那么吞吐量就是99%)
自适应调节策略,自动指定年轻代、Eden、Suvisor区的比例。
3.适用场景
适合后台运算,交互不多的任务,如批量处理,订单处理,科学计算等。 -
Serial Old收集器
Serial Old是Serial收集器的老年代版本, 它同样是一个单线程收集器, 使用标记-整理算法。 这个收集器的主要意
义也是供客户端模式下的HotSpot虚拟机使用。 -
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本, 支持多线程并发收集, 基于标记-整理算法实现。 这个收集器
是直到JDK 6时才开始提供的, 在此之前, 新生代的Parallel Scavenge收集器一直处于相 当尴尬的状态, 原因是如
果新生代选择了Parallel Scavenge收集器, 老年代除了Serial Old(PS MarkSweep) 收集器以外别无选择, 其他表
现良好的老年代收集器, 如CMS无法与它配合工作。 -
CMS垃圾回收器
CMS(concurrent mark sweep)是以获取最短垃圾收集停顿时间为目标的收集器,CMS收集器的关注点尽可能缩短
垃圾收集时用户线程的停顿时间,停顿时间越短就越适合与用户交互的程序,目前很大一部分的java应用几种在互联网
的B/S系统服务器上,这类应用尤其注重服务器的响应速度,系统停顿时间最短,给用户带来良好的体验,CMS收
集器使用的算法是标记-清除算法实现的;
整个过程分4个步骤:
1)初始标记
2) 并发标记
3) 重新标记
4) 并发清除
其中 初始标记 和 重新标记 都需要stopTheWorld
CMS整个过程比之前的收集器要复杂,整个过程分为4个阶段即、初始标记 并发标记 、重新标记、并发清除
初始标记(Initial-Mark)阶段:这个阶段程序所有的工作线程都将会因为"Stop-the-Wold"机制而出现短暂的的暂停,这个阶段的主要任务标记处GC Roots 能够关联到的对象.一旦标记完成后就恢复之前被暂停的的所有应用。 由于直接关联对象比较小,所以这里的操作速度非常快。
并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要暂停用户线程, 用户线程可以与垃圾回收器一起运行。
重新标记(Remark)阶段:由于并发标记阶段,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此,为了修正并发标记期间因为用户继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常比初始标记阶段长一些,但也远比并发标记阶段时间短。
清除并发(Concurrent-Sweep)阶段: 此阶段清理删除掉标记判断已经死亡的对象,并释放内存空间。由于不需
要移动存活对象,所以这个阶段可以与用户线程同时并发运行
- G1
Garbage First是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
- G1把内存划分为多个独立的区域Region
2、G1仍然保留分代思想,保留了新生代和老年代,但他们不再是物理隔离,而是一部分Region的集合
3、G1能够充分利用多CPU、多核环境硬件优势,尽量缩短STW
4、G1整体整体采用标记整理算法,局部是采用复制算法,不会产生内存碎片
5、G1的停顿可预测,能够明确指定在一个时间段内,消耗在垃圾收集上的时间不超过设置时间
6、G1跟踪各个Region里面垃圾的价值大小,会维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限事件内高效的收集垃圾
G1不再坚持固定大小以及固定数量的 分代区域划分, 而是把连续的Java堆划分为多个独立区域(Region) , 每一个Region都可以 根据需要, 扮演新生代的Eden空间、 Survivor空间, 或者老年代空间。
3.5 G1 垃圾收集的特点,为什么低延迟
参考:分为Eden Region、Old Region、Survior Region,还有一个比较特殊的Humongous Region这四种,一个空闲的region,可以被当做上述四种的任意一种逻辑类型的region来使用
有三种类型的GC,分别是YGC(整体Young region的数量超过了一定的阈值(默认45%))、FGC(在做Mixed GC的时候,没有新的region可以被分配了;在给大对象分配Humongous Region的时候,没有连续的满足大小的region可被分配)、Mixed GC(当old region中垃圾占比超过一定的阈值(默认10%),面向的是所有的Young region和部分的old region,这些region会被放到一个Collection Set中(简称CSet),CSet存在于垃圾回收线程中)
RSet(Remembered Set)结构的设计可以更快地去做region中对象的标记动作,同时可以判断哪个old region是收益高的,在Mixed GC过程可以依据这个来进行old Region的筛选,同时也可以基于这个来进行大概的回收时间的评估,来达到整体的GC时间满足MaxGCPauseMillis(GC的暂停预期时间)这个预期
四、JVM 调优的过程
4.1 介绍下 JVM 调优的过程
1.监控GC的状态
使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。
举一个例子: 系统崩溃前的一些现象:
每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
年老代的内存越来越大并且每次FullGC后年老代没有内存被释放
之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。
2.生成堆的dump文件
通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。
3.分析dump文件
打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件:
Visual VM
IBM HeapAnalyzer
JDK 自带的Hprof工具
Mat(Eclipse专门的静态内存分析工具)推荐使用
备注:文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。
4.分析结果,判断是否需要优化
如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
注:如果满足下面的指标,则一般不需要进行GC:
Minor GC执行时间不到50ms;
Minor GC执行不频繁,约10秒一次;
Full GC执行时间不到1s;
Full GC执行频率不算频繁,不低于10分钟1次;
5.调整GC类型和内存分配
如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。
6.不断的分析和调整
通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。
五、服务器出现的问题
5.1 定位问题常用哪些命令
jps 是(java process Status Tool), Java版的ps命令,查看java进程及其相关的信息,如果你想找到一个java进程的pid,那可以用jps命令替代linux中的ps命令了,简单而方便。
jinfo是用来查看JVM参数和动态修改部分JVM参数的命令
jstat命令是使用频率比较高的命令,主要用来查看JVM运行时的状态信息,包括内存状态、垃圾回收等。
jstack是用来查看JVM线程快照的命令,线程快照是当前JVM线程正在执行的方法堆栈集合。使用jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环等。jstack还可以查看程序崩溃时生成的core文件中的stack信息。
jmap可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及finalizer 队列
jhat是用来分析jmap生成dump文件的命令,jhat内置了应用服务器,可以通过网页查看dump文件分析结果,jhat一般是用在离线分析上。
5.2 服务器使用的什么垃圾收集器
5.3 线上服务器出现频繁 Full GC,怎么排查
1.Full GC
会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。
2.导致Full GC的原因
1)年老代(Tenured)被写满
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。
2)持久代Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例
3)System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制
在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节,下面详细介绍对应JVM调优的方法和步骤。
JVM性能调优方法和步骤