魔方 网站,做厂房出租有那些推广网站,建筑模板公司,做购物网站之前做些什么JVM调优与垃圾回收器详解 这张是jdk8的jvm模型#xff1a; 黄色框的是线程共享区域、蓝色框的是线程私有(也就是每个线程单独一份) jvm模型从大的角度说有#xff1a;类装载子系统、字节码执行引擎、运行时数据区。我这里主要讲运行时数据区。
一、JVM内存模型
1、名词解释…JVM调优与垃圾回收器详解 这张是jdk8的jvm模型 黄色框的是线程共享区域、蓝色框的是线程私有(也就是每个线程单独一份) jvm模型从大的角度说有类装载子系统、字节码执行引擎、运行时数据区。我这里主要讲运行时数据区。
一、JVM内存模型
1、名词解释
堆: 其唯一的用途就是存放对象实例所有的对象实例及数组都在堆上进行分配。包含新生代(Eden区、S0、S1)、老年代。官方推荐配置为年轻代大小占整个堆的3/8。-XX:NewRatio3/5表示新生代和老年代的比值 而Eden:S0:S18:1:1 注意jdk1.8 开始 静态变量和字符串常量池在堆中 虚拟机栈 描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧Stack Frame用于存储 局部变量、操作数栈、动态链接、方法出口等信息.生命周期与线程相同。栈里面存放着各种基本数据类型和对象的引用 局部变量定义在方法中的变量 操作数栈程序运行中一块临时存放要做操作的数据的空间。进行运算的地方 动态链接Java Class文件中有很多符号引用一部分在类加载的时候转化为直接引用另一部分在每一次运行期间转化为符号引用这部分被称为动态链接 方法出口当一个方法执行的时候只有两种可以退出方法的方法。第一种是JVM碰到任意一个方法返回的字节码指令被称为正常完成出口。另一种是在执行方法中抛出异常并且未对异常进行处理被称为异常完成出口。方法退出的时候相当于把栈帧出栈 如果在栈帧中有一个变量类型为引用类型比如Object objnew Object()这时候就是典型的栈中元素指向堆中的 对象 。
方法区元空间: 存储已被虚拟机加载的类信息。jdk8的JVM不再有永久代PermGen原永久代存储的信息被分成两部分 1、虚拟机加载的类信息(放在元空间) 2、运行时常量池(放在堆中) 元空间和方法区第一个是hotspot的具体实现技术第二个是JVM规范的抽象定义不能说元数据区就是方法区但可以说元空间用来实现了方法区 方法区中会存放静态变量常量等数据jdk1.8 静态变量和字符串常量池在堆中。如果是下面这种情况就是典型的方法区中元素指向堆中的对象。private static Object objnew Object(); 本地方法栈: 本地方法栈则为虚拟机使用到的Native方法服务(非java代码的接口,比如C的方法Runtime.getRuntime().exec()是执行shell脚本的命令) 程序计数器: 当前线程执行的字节码的行号指示器各线程之间独立存储互不影响 堆—线程分配缓冲区 堆里面有一块线程私有的区域–线程分配缓冲区TLAB 对象逃逸分析就是分析对象动态作用域当一个对象在方法中被定义后它可能被外部方法所引用例如作为调用参数传递到其他地方中。 JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存这样该对象所占用的内存空间就可以随栈帧出栈而销毁就减轻了垃圾回收的压力 public User test1() {User user new User();user.setId(1);user.setName(zhuge);//TODO 保存到数据库return user;
}public void test2() {User user new User();user.setId(1);user.setName(zhuge);//TODO 保存到数据库
}很显然test1方法中的user对象被返回了这个对象的作用域范围不确定–会逃逸test2方法中的user对象我们可以确定当方法结束这个对象就可以认为是无效对象了对于这样的对象我们其实可以将其分配在栈内存里让其在方法结束时跟随栈内存一起被回收掉–不会逃逸. JVM对于这种情况可以通过开启逃逸分析参数(-XX:DoEscapeAnalysis)来优化对象内存分配位置使其通过标量替换优先分配在栈上(栈上分配)JDK7之后默认开启逃逸分析如果要关闭使用参数(-XX:-DoEscapeAnalysis) 逃逸分析的好处 1.栈上分配对象—对象占用的内存随着栈帧出栈而销毁减轻垃圾回收的压力 2.同步消除—一个变量不会逃逸出线程就无法呗其他线程访问那么这变量的读写就不会有竞争就可以消除它的同步措施 3.通过逃逸分析确定该对象不会被外部访问并且对象可以被进一步分解时JVM不会创建该对象而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替这些代替的成员变量在栈帧或寄存器上分配空间这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:EliminateAllocations)JDK7之后默认开启 二、垃圾回收
查看当前服务器用的垃圾回收器java -XX:PrintCommandLineFlags -version
1、垃圾回收算法
1.1、标记-清除(Mark-Sweep) 标记找出内存中需要回收的对象并且把它们标记出来 清除清除掉被标记需要回收的对象释放出对应的内存空间 缺点 标记清除之后会产生大量不连续的内存碎片空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 (1)标记和清除两个过程都比较耗时效率不高 (2)会产生大量不连续的内存碎片空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无 法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 1.2、复制(Copying) 将内存划分为两块相等的区域每次只使用其中一块如下图所示 当其中一块内存使用完了就将还存活的对象复制到另外一块上面然后把已经使用过的内存空间一次 清除掉 缺点: 空间利用率降低。 1.3、 标记-整理(Mark-Compact) 标记过程仍然与标记-清除算法一样但是后续步骤不是直接对可回收对象进行清理而是让所有存活 的对象都向一端移动然后直接清理掉端边界以外的内存让所有存活的对象都向一端移动清理掉边界意外的内存。 Young区复制算法(对象在被分配之后可能生命周期比较短Young区复制效率比较高) Old区标记清除或标记整理(Old区对象存活时间比较长复制来复制去没必要不如做个标记再清理) 复制发生在新生代(优点无内存碎片效率高 缺点需要两倍的空间) 标记清理发生在老年代(优点占用空间少 缺点会产生内存碎片) 标记整理发生在老年代(优点占用空间少无碎片 缺点对象移动会消耗资源)
2、垃圾回收器的种类: 1、Serial:串行(-XX:UseSerialGC)为单线程环境设计且使用一个线程回收垃圾会暂停所有的用户线程(Stop The World)不适合服务器环境(例如用户用餐餐厅叫出去要叫一个清洁工打扫打扫完再回来吃) 新生代采用复制算法老年代采用标记-整理算法 2、Parallel:并行(-XX:UseParallelGC)多个并行垃圾收集线程工作此时用户线程是暂停的(Stop The World)适用于科学计算、大数据处理首台处理等若交互环境(例如用户用餐餐厅叫出去要叫多个清洁工打扫打扫完再回来吃) 新生代采用复制算法老年代采用标记-整理算法 3、CMS:(-XX:UseConcMarkSweepGC)用户线程和垃圾收集线程同时执行(并不一定是并行可能交替执行)不需要停顿用户线程 适用对响应时间有要求的场景(例如用户用餐餐厅叫出去要叫多个清洁工打扫边吃边打扫) 采用的是标记-清除算法,整个过程分为4步 (1)初始标记: 暂停所有的其他线程(STW)并记录下gc roots直接能引用的对象速度很快 (2)并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程 这个过程耗时较长但是不需要停顿用户线程 可以与垃圾收集线程一起并发运行。因为用户程序继续运行可能会有导致已经标记过的对象状态发生改变 (3)重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题)这个阶段的停顿时间一般会比初始标记阶段的时间稍长远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记 (4)并发清除 CMS concurrent sweep 开启用户线程同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理 (5)并发重置重置本次GC过程中的标记数据。 对于8G内存我们一般是分配4G内存给JVM正常的JVM参数配置如下 -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize256M -XX:MaxMetaspaceSize256M -XX:SurvivorRatio8 这样设置可能会由于动态对象年龄判断原则导致频繁full gc。于是我们可以更新下JVM参数设置 -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize256M -XX:MaxMetaspaceSize256M -XX:SurvivorRatio8 4、G1:(garbage first)(-XX:UseG1GC)G1垃圾回收器将堆内存分割成不通的区域然后并发的对其进行垃圾回收 java11默认GC回收 器是ZGC。属于标记-整理算法 使用G1收集器时Java堆的内存布局与就与其他收集器有很大差别它将整个Java堆划分为多个大小相等的独立区域Region虽然还保留有新生代和老年代的概念但新生代和老年代不再是物理隔离的了它们都是一部分Region不需要连续的集合 1 初始标记Initial Marking 标记一下GC Roots能够关联的对象并且修改TAMS的值需要暂 停用户线程 STW 2并发标记Concurrent Marking 从GC Roots进行可达性分析找出存活的对象与用户线程并发 执行 3最终标记Final Marking 修正在并发标记阶段因为用户程序的并发执行导致变动的数据需 暂停用户线程 4筛选回收Live Data Counting and Evacuation 对各个Region的回收价值和成本进行排序根据 用户所期望的GC停顿时间制定回收计划 CMS和G1垃圾回收器使用场景对比
区别CMSG1回收对象回收老年代。需要配合新生代收集器一起使用老年代和新生代STW时间以最小停顿时间为目标可预计的垃圾回收停顿时间回收算法标记清除标记整理垃圾碎片产生内存碎片没有内存碎片垃圾回收过程1.初始标记STW 2.并发标记 3.重新标记STW4.并发清除1.初始标记(STW) 2.并发标记 3.最终标记(STW) 4.筛选回收(STW)浮动垃圾会产生浮动垃圾第四阶段产生没有浮动垃圾第四阶段用户线程卡停浮动垃圾产生原因 第四阶段并发清除GC线程和用户线程同时运行用户线程会产生浮动垃圾浮动垃圾导致结果浮动垃圾导致内存不足时候出现“Concurrent Mode Failure”出现此错误时就会切换到SerialOld收集模式大对象处理直接进入老年代如果大于一个region的50%会横跨多个region进行存放优点并发收集低停顿1.控制垃圾回收时间选择一组合适的region最为回收目标达到实时收集目的 2.空间整理不会产生空间碎片缺点1. 产生浮动垃圾,在并发清理过程中产生的垃圾只能下次gc进行清理 2. 吞吐量降低 3. 碎片化空间 4. 垃圾清理退化到serial 单线程清理 在并发清理过程中,老年代无法容纳新产生的对象,就会抛这个异常,然后stw进行单线程清理垃圾可以调整参数修改触发gc的阈值使用场景1.JDK8及更高版本同等环境下只要cpu性能比较好并且内存不算大 (最少4G)可以使用CMS 2.JDK7及更低版本同等环境下 可选择CMS (G1不完善)1. 50%以上的堆被存活对象占用 2. 对象分配和晋升的速度变化非常大 3. 垃圾回收时间特别长超过1秒 4. 8GB以上的堆内存(建议值) 5. 停顿时间是500ms以内
三色标记法 三色标记算法是把Gc roots可达性分析遍历对象过程中遇到的对象 按照“是否访问过”这个条件标记成以下三种颜色 黑色表示对象已经被垃圾收集器访问过 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过 它是安全存活 的如果有其他对象引用指向了黑色对象 无须重新扫描一遍。 黑色对象不可能直接不经过灰色对象 指向某个白色对象。 灰色表示对象已经被垃圾收集器访问过 但这个对象上至少存在一个引用还没有被扫描过。 白色 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段 所有的对象都是白色的 若在分析结束的阶段 仍然是白色的对象 即代表不可达。 3、如何判断一个对象是否应该被回收 判断一个对象是否可达不可达对象就将被回收所谓可达就是从GCROOT开始是否是可以找到该对象 GCROOT是什么 1、虚拟机栈中引用的对象本地变量表 2、方法区中静态属性引用的对象 3、方法区中常量引用的对象 4、本地方法栈中引用的对象Native Object java中的引用类型和回收策略 强引用不会被GC回收 软引用JVM充足不回收JVM不足会被回收 弱引用 会被GC回收 虚引用 任何时候都能被回收 4、设置垃圾回收器 1串行 -XXUseSerialGC -XXUseSerialOldGC 2并行(吞吐量优先) -XXUseParallelGC -XXUseParallelOldGC 3并发收集器(响应时间优先) -XXUseConcMarkSweepGC -XXUseG1GC 例如java -server -Xms1024m -Xmx1024m -XX:MetaspaceSize1024m -XX:PrintFlagsFinal -XX:UseG1GC -jar springboot2019-SNAPSHOT.jar
5、GC的类型 1.对Eden内存空间进行清理即垃圾收集(Garbage Collect)这样的GC我们称之为Minor GC 2.Old区的GC我们称作为Major GC 3.新生代(Minor GC)老年代(Major GC) Full GC 6、触发Full GC的条件 老年代采取的垃圾回收算法是标记整理算法 老年代触发垃圾回收的机制一般就是两个 ①在Minor GC之前一通检查发现很可能Minor GC之后要进入老年代的对象太多了老年代放不下 此时需要提前触发Full GC再然后再带着进行Minor GC ②在Minor GC之后发现剩余对象太多放入老年代都放不下了 在垃圾回收的过程中尤其是Full GC。JVM会启动STW机制停止一切的用户进程。当然不同的垃圾收集器STW的时间也不一样。 STW的作用 在垃圾回收过程中如果垃圾回收线程和用户线程一起工作。那么会造成一些对象的状态难以确定标记起来也比较复杂所以索性就使用STW机制停止一切用户线程。当垃圾回收结束之后再恢复用户线程。用户可能在这一段时间内出现卡顿的现象这就是STW 7、垃圾进入老年代的触发条件 1、当对象的年龄达到15岁时 默认的设置下也就是躲过15次GC的时候他就会转移到老年代里去 具体是多少岁进入老年代可以通过JVM参数“-XX:MaxTenuringThreshold”来设置默认是15岁 2、动态对象年龄判断 假如说当前放对象的Survivor区域里一批对象的总大小大于了这块Survivor区域的内存大小的50% 那么此时大于等于这批对象年龄的最大值对象就可以直接进入老年代了 例如年龄1年龄2年龄n的多个年龄对象总和超过了Survivor区的50%此时就会把年龄n以上的对象都放入老年代 3.大对象直接进入老年代 如果你要创建一个大于这个大小的对象比如一个超大的数组或者是别的啥东西此时就直接把这个大对象放到老年代里去压根不会经过年轻代,有一个JVM参数就是“-XX:PretenureSizeThreshold”可以把它的值设置为字节数比如“1048576”字节就是1MB 4.老年代空间分配担保机制 年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了如果有这个参数就会看看老年代的可用内存大小是否大于之前每一次minor gc后进入老年代的对象的平均大小。 如果上一步结果是小于或者之前说的参数没有设置那么就会触发一次Full gc对老年代和年轻代一起回收一次垃圾 如果回收完还是没有足够空间存放新的对象就会发生OOM 当然如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间那么也会触发full gcfull gc完之后如果还是没有空间放minor gc之后的存活对象则也会发生“OOM” 老年代大概率存放 1.静态变量 – 生命周期是全局的 2.缓存对象 3.spring容器生成的对象 4.对象池中的对象 8、 JVM内存担保机制 在发生Minor GC之前虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。 ① 如果大于则此次Minor GC是安全的 ②如果小于则虚拟机会查看-X:HandlePromotionFailure设置值是否允许担保失败。 如果HandlePromotionFailuretrue,那么会继续检查 老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。 2.1 如果大于则尝试进行一次Minor GC,但这次Minor GC依然是有风险的 2.2 如果小于则改为进行一次Full GC。 —— 如果HandlePromotionFailurefalse,则改为进行一次Full GC。 担保机制是为了减少Full GC的执行频次提高了应用的性能。jdk8默认就设置了次参数 9、什么是强引用、软引用、弱引用、虚引用 强引用:只要还有强引用指向对象就算OOM也不会回收该对象。Object o1new Object(),就算强引用 软引用内存足够的情况不回收内存不足时就回收的对象。SoftReference,用于内存敏感的地方 Object o1new Object(); SoftReference sf new SoftReference(o1); 弱引用:不管内存是否够用GC时一律回收 Object o1new Object(); WeakReference sf new WeakReference(o1); 虚引用:PhantomReference 虚引用并不会决定生命周期如果一个对象仅持有虚引用那么他就和没有引用是一样的任何时候都可能被垃圾回收器回收它不能单独使用也不能通过它访问对象虚引用必须和引用队列联合使用 主要作用是:跟踪对象被垃圾回收的状态 三、jvm参数
1.参数类型
①标配参数 例如java -version/java -help /java -showversion ②x参数 java -Xint解释执行java -Xint -version、java -Xcomp第一次使用就编译成本地代码、java -Xmixed混合模式 ③XX参数 -XX:或者-某个属性值其中表示开启、-表示关闭 例如jinfo -flag PrintGCDetails 进程号 如果出现-XX:-PrintGCDetails 代表关闭了GC回收参数配置如果是-XX:PrintGCDetails 带表已配置了GC回收参数 ④其他参数(所以这块也相当于是-XX类型的参数) -Xms1000等价于-XX:InitialHeapSize1000 -Xmx1000等价于-XX:MaxHeapSize1000 -Xss100等价于-XX:ThreadStackSize100 例如一个设置内存的示例: java -server -Xms1024m -Xmx1024m -XX:UseG1GC -jar springboot2019-SNAPSHOT.war 总结公式java -server jvm的各种配置参数 -jar jar包或者war包的名字 2.查看服务器启动了那些(查看jvm初始值 或者当前值的办法) 单位换算 1Byte(字节)8bit(位) 1KB1024Byte(字节) 1MB1024KB 1GB1024MB 1TB1024GB jvm常用参数含义
|
3.设置jvm调优的两种方法 设置jvm调优的两种方法: ①在tomcat的bin下面的catalina.sh 里位置cygwinfalse前 JAVA_OPTS‘-server -Xms512m -Xmx768m -XX:NewSize128m -XX:MaxNewSize192m -XX:SurvivorRatio8’ ②使用jar包启动的话 java -server -Xms1024m -Xmx1024m -XX:UseG1GC -jar springboot2019-SNAPSHOT.jar war包也可以这样java -server -Xms1024m -Xmx1024m -XX:UseG1GC -jar springboot2019-SNAPSHOT.war 参数解释 -Xms 初始堆内存大小 -Xmx 最大堆内存大小 -Xss 单个线程栈大小 -XX:NewSize 初始新生代堆大小 -XX:MaxNewSize 生代最大堆大小 -XX:MetaspaceSize 元数据区初始值(JDK1.8) -XX:MaxMetaspaceSize 元数据区最大值(JDK1.8) -XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例. 含以-XX:SurvivorRatioeden/fromden/to 总结:在实际工作中我们可以直接将初始的堆大小与最大堆大小相等 这样的好处是可以减少程序运行时垃圾回收次数从而提高效率。 例如linux设置tomcat的catalina.sh JAVA_OPTS-Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize128m -XX:MAXMetaspaceSize256m -XX:NewSize256m -XX:MaxNewSize256m 或者java -server -Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize128m -XX:MAXMetaspaceSize256m -XX:NewSize256m -XX:MaxNewSize256m -jar springboot2019-SNAPSHOT.jar 调整完查看,打印JVM所有参数列表的方法: java -XX:PrintFlagsFinal -version 查看JVM初始化参数java -XX:PrintFlagsInitial ①执行 jps(Java Virtual Machine Process Status Tool)是java提供的一个显示当前所有java进程pid的命令适合在linux/unix平台上简单察看当前java进程的一些简单情况 ②查看当前进程有哪些参数 打印命令行参数 jinfo -flags 进程号。 例如打印GC信息jinfo -flags 例如打印指定进程全部参数 jinfo -flags 14857 例如打印指定进程指定参数内容 jinfo -flag PrintGCDetails 14857 4.常用jvm调参语法:
1、jinfo 实时查看和调整JVM配置参数
查看 格式 jinfo -flag name PID 查看某个java进程的name属性的值 jinfo -flags PID 查看某个java进程的全部属性的值 jinfo -flag MaxHeapSize PID jinfo -flag UseG1GC PID 例如 jinfo -flags 13573 *重点* jinfo -flag MaxHeapSize 13573 jinfo -flag UseG1GC 13573 2、jstat 查看虚拟机性能统计信息
jstat -gc pid 1000 10 (每隔1秒执行1次命令共执行10次) jstat命令可以查看堆内存各部分的使用量以及加载类的数量 jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数] 格式 jstat -class PID 1000 10 查看某个java进程的类装载信息每1000毫秒输出一次共输出10次 例如 查看类装载信息: jstat -class 9939 垃圾回收统计jstat -gc 9939 3、Jstack 查看线程堆栈信息
格式jstack PID Jstack 用jstack加进程id查找死锁(查找递归死循环等得位置) 行 jstack 19663|grep 4cd0 -A60 //19663是进程号4cd0是十六进制的线程号-A60是打印错误附近的60行代码 4. jmap 生成堆转储快照 打印出堆内存相关信息 jmap -heap PID 例如jmap -heap 2304 jmap 用来查看内存信息实例个数以及占用内存大小 jmap ‐histo 14660 #查看历史生成的实例14660是进程id用jps查 jmap ‐histo:live 14660 #查看当前存活的实例14660是进程id用jps查。执行过程中可能会触发一次full gc 堆信息jmap -heap 14660 也可以设置内存溢出自动导出dump文件(内存很大的时候可能会导不出来) -XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath./ 路径 例如 ‐Xms10M ‐Xmx10M ‐XX:PrintGCDetail‐XX:HeapDumpOnOutOfMemoryError ‐XX:HeapDumpPathD:\jvm.dump jmap命令查看堆内存 jmap -heap 进程ID 查看内存使用情况 jmap -heap 9939 jmap ‐histo pid | more 例如 查看内存中对象数量及大小 jmap -histo:live 11927 | more 自动导出内存溢出文件 也可以设置内存溢出自动导出dump文件(内存很大的时候可能会导不出来) -XX:HeapDumpOnOutOfMemoryErro -XX:HeapDumpPath./ 路径 这样在内存溢出的时候就会自动导出一个dump文件到 路径 指定的位置 在jdk里面有个jvisualvm.exe导入此dump文件就可以看到GC、内存溢出详情 5、调优实例 ①错误java.lang.OutOfMemoryError:Metaspace 元空间大小内存溢出 设置元空间大小方法-XX:MetaspaceSize1024m* 就是设置元空间的大小 实战中设置元空间大小 java -server -Xms1024m -Xmx1024m -XX:MetaspaceSize1024m -XX:PrintFlagsFinal -jar springboot2019-SNAPSHOT.jar 元空间的本质和永久代类似都是对JVM规范中的方法区的实现。不过元空间与永久代的最大区别是:元空间并不在虚拟机中而是使用本地内存默认情况下元空间仅受本地内存大小限制 查看jvm所有配置信息java -XX:PrintFlagsFinal -version 这样找到MetaspaceSize对应的大小是21807104 字节 换算过来才 20.79m。也就是说元空间虽然只跟本地内存有关但它初始值只有21m可以调大 ②栈空间的大小调整 -Xss等价于-XX:ThreadStackSize java -XX:PrintFlagsFinal -version查找ThreadStackSize是栈空间大小会发现初始值为0 栈空间默认值跟平台有关 Linux(x64):1024kb OS X(64-bit):1024kb Windows:虚拟内存的默认值 例如java -server -Xss128k -jar springboot2019-SNAPSHOT.jar ③查看垃圾回收多少次后才会由新生代进入老年代 3.1、查看程序进程 jps -l 3.2、查看默认垃圾回收次数才进入老年代 jinfo -flag MaxTenuringThreshold 进程号 会看到默认是15次(最大设置为15) 四、调优示例
问题一 CPU占用高调优
方法 1、 cpu占用过高定位java代码中的办法 1、使用 top查看占用cpu高的程序 例如top 假如cpu占用最高的就是 elasticsearch 一般是业务jar包 37 root 20 0 0 0 0 S 0.3 0.0 0:01.82 elasticsearch 2、使用jps或ps -ef|grep “elasticsearch” 去找出这个占用高的程序的进程号 例如:jps -l 1541 Elasticsearch 3、定位到具体的线程或代码 ps -mp 进程 -o THREAD,tid,time -m显示所有线程 -p pid进程使用cpu时间 -o:该参数后是用户自定义格式 例如:ps -mp 1541 -o THREAD,tid,time 输出很多(最后一列是时间假如这个线程耗时最长)esuser 0.0 19 - futex_ - - 1541 00:00:08 找到时间最长的那个线程号 4、把需要的线程id转换为16进制的格式(要英文小写的) 方法一执行命令printf “%x\n” 有问题的线程id。例如 printf %x\n 2242 输出8c2 方法二用计算器转换为16进制 5、执行命令jstack 进程ID |grep tid(16进制的线程id英文小写) -A60 例如jstack 2242 |grep 8c2 -A60 tid(16进制的线程id英文小写)是一个整体是指上面换算后的线程id要16进制那个值 -A60是指打印最近的60行 在打印信息中找到包名就是java代码对应的哪一行报出来的错 方法 2 jstack找出占用cpu最高的线程堆栈信息 使用命令top 找到占用CPU比例最高的进程使用命令top -p 显示你的java进程的内存情况pid是你的java进程号比如19663 top -p 19663 3按H获取每个线程的内存情况找到内存和cpu占用最高的线程tid比如19664转为十六进制得到 0x4cd0此为线程id的十六进制表示执行 jstack 19663|grep -A 10 4cd0得到线程堆栈信息中 4cd0 这个线程所在行的后面10行从堆栈中可以发现导致cpu飙高的调 用方法查看对应的堆栈信息找出可能存在问题的代码 问题二 内存泄漏
2.什么是内存泄漏如何解决
1.定义Java中的内存泄露指的是程序在运行过程中未正确地释放不再使用的对象所占据的内存空间,最终导致致命的OutOfMemoryError. 2.发生内存泄漏的最常见场景 没有正确使用静态成员。(静态变量的什么周期是全局的)未关闭的资源。将没有 hashCode() 和 equals()的对象添加到 HashSet 中。过多的会话对象。自定义数据结构编写不当。长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏尽管短生命周期对象已经不再需要但是因为长生命周期持有它的引用而导致不能被回收这就是Java中内存泄漏的发生场景。 例如在全局静态map中缓存局部变量且没有清空操作随着时间的推移这个map会越来越大造成内存泄露 3、监测方法 分析日志文件。 通过查看应用程序生成的日志文件可以了解到系统中发生了什么样的错误或异常情况 使用工具进行性能分析可以使用JVM自带的工具如jmap、jstat或第三方工具如VisualVM、YourKit来获取有关内存使用情况的数据 jmap -heap pid:此命令可以用来查看内存信息实例个数以及占用内存大小 jstat -gc pid 最常用可以评估程序内存使用及GC压力整体情况 S0C第一个幸存区的大小单位KB S1C第二个幸存区的大小 S0U第一个幸存区的使用大小 S1U第二个幸存区的使用大小 EC伊甸园区的大小 EU伊甸园区的使用大小 OC老年代大小 OU老年代使用大小 MC方法区大小(元空间) MU方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小 YGC年轻代垃圾回收次数 YGCT年轻代垃圾回收消耗时间单位s FGC老年代垃圾回收次数 FGCT老年代垃圾回收消耗时间单位s GCT垃圾回收消耗总时间单位s 4. 解决办法 本地缓存可以使用ehCache等自带LRU的淘汰算法的框架来作为JVM级缓存过期自动淘汰数据及时清除无用对象在开发过程中要注意及时清除不再使用的对象防止它们被长期保持在内存中避免创建过多的全局变量全局变量会一直存活在内存中容易造成内存泄漏。尽量将变量限制在必要的作用域内避免过度使用全局变量使用try-with-resources语句当打开IO流或连接数据库等操作完成后应该立即关闭相关资源以避免资源泄漏。使用try-with-resources语句可以自动关闭资源避免手动关闭资源的麻烦。 问题三 内存溢出
1.定义 内存溢出(Out Of Memory,简称OOM)是指在编程中应用程序申请的内存超过系统能提供的最大内存导致程序无法继续运行的情况。这种情况通常发生在内存分配过程中例如在申请一个Integer类型变量时却分配了足够存储一个Long变量的内存空间 2.原因 内存中加载的数据量过大。例如一次从数据库中取出过多数据 集合类中有对对象的引用使用完后未清空导致JVM无法回收这些对象。代码中存在死循环或循环产生过多重复的对象实体。使用第三方软件中的BUG。启动参数内存值设定的过小 3.解决办法 优化代码避免无效的内存分配和内存泄漏。使用内存池技术重复利用已申请的内存空间。增大操作系统对程序可用的最大内存限制前提是操作系统支持。 将需要处理的数据划分为更小的块分批进行处理以减少每个块 所需的内存量。修改JVM启动参数直接增加内存。检查错误日志查看“OutOfMemory”错误前是否有其它异常或错误。对代码进行走查和分析找出可能发生内存溢出的位置 4.解决实例 以前使用固定大小线程池在任务量足够多的情况下会导致LinkedBlockingDeque 装入大量任务从而导致队列占用大量内存而导致内存溢出 Executors.newFixedThreadPool(3) 问题四 线程安全
3.线程安全是什么产生原因以及解决办法
1.定义 线程安全是指在多线程环境下一个类或对象在多个线程同时访问时能够保持其状态的一致性和正确性
2、出现线程安全问题的原因 操作系统对线程的调度是随机的/抢占式主要原因多个线程修改同一个变量修改操作不是原子的内存可见性问题指令重排序问题 3、如何解决线程安全 针对线程的抢占式执行可以采用waitnotify来调配线程执行顺序。 对于多个线程修改同一个变量可以调节代码结构做到一定程度上的规避(比如说给第二个线程创建一个新的对象来规避)。 操作不是原子性可以利用synchronized进行加锁操作使操作变为原子性。 synchronized可以修饰代码块、方法、修饰静态方法锁类对象。锁具有互斥性当一个线程对其加锁后其他线程就不能够在对其进行修改或访问需要等该线程释放锁后才能重新访问 内存可见性问题是编译器优化引入的bug当一个线程在修改一个内存数据时另一个线程无法及时感知到就会导致代码出现错误。使用volatile关键字修饰可能出现内存可见性问题的变量就可以避免编译器只读寄存器不读内存的情况解决了内存可见性问题 指令重排序也是编译器优化导致的bug这里就不过多讲解了 五 、JVM 类加载机制
1、JVM 类加载机制图解 2、类的加载机制过程
1.装载(Load) 查找和导入class文件 (1)通过一个类的全限定名获取定义此类的二进制字节流 (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 (3)在Java堆中生成一个代表这个类的java.lang.Class对象作为对方法区中这些数据的访问入口 2.链接(Link)
2.1 验证(Verify) 保证被加载类的正确性 文件格式验证 元数据验证 字节码验证 符号引用验证 2.2 准备(Prepare) 为类的静态变量分配内存并将其初始化为默认值 2.3 解析(Resolve) 把类中的符号引用转换为直接引用 符号引用Symbolic References符号引用以一组符号来描述所引用的目标符号可以是任何形式的字面量只要使用时能够无歧义的定位到目标即可。在Class文件中就是这种 直接引用直接引用可以是 1、直接指向目标的指针 2、相对偏移量 3、一个能间接定位到目标的句柄 3 初始化(Initialize) 对类的静态变量静态代码块执行初始化操作 4 使用
5 卸载
3、类加载模式
类加载器的双亲委派加载机制重点 当一个类收到了类加载请求他首先不会尝试自己去加载这个类而是把这个请求委派给父类去完成每一个层次类加载器都是如此因此所有的加载请求都应该传送到启动类加载其中 只有当父类加载器反馈自己无法完成这个请求的时候在它的加载路径下没有找到所需加载的 Class子类加载器才会尝试自己去加载。 采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object不管是哪个加载器加载这个类最终都是委托给顶层的启动类加载器进行加载这样就保证了使用不同的类加载 器最终得到的都是同样一个 Object 对象。 为什么要设计双亲委派机制 沙箱安全机制自己写的java.lang.String.class类不会被加载这样便可以防止核心API库被随意篡改 2.避免类的重复加载当父亲已经加载了该类时就没有必要子ClassLoader再加载一 次保证被加载类的唯一性