asp网站安全吗网络推广内容
JVM调优与垃圾回收器详解
这张是jdk8的jvm模型:
黄色框的是线程共享区域、蓝色框的是线程私有(也就是每个线程单独一份)
jvm模型从大的角度说有:类装载子系统、字节码执行引擎、运行时数据区。我这里主要讲运行时数据区。
一、JVM内存模型
1、名词解释
堆:
其唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配。包含:新生代(Eden区、S0、S1)、老年代。官方推荐配置为年轻代大小占整个堆的3/8。-XX:NewRatio=3/5表示新生代和老年代的比值, 而Eden:S0:S1=8:1:1
注意:jdk1.8 开始 静态变量和字符串常量池在堆中
虚拟机栈
描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储 局部变量、操作数栈、动态链接、方法出口等信息.生命周期与线程相同。栈里面存放着各种基本数据类型和对象的引用
局部变量:
定义在方法中的变量
操作数栈:
程序运行中,一块临时存放要做操作的数据的空间。进行运算的地方
动态链接:
Java Class文件中有很多符号引用,一部分在类加载的时候转化为直接引用,另一部分在每一次运行期间转化为符号引用,这部分被称为动态链接
方法出口:
当一个方法执行的时候,只有两种可以退出方法的方法。第一种是JVM碰到任意一个方法返回的字节码指令,被称为正常完成出口。另一种是在执行方法中抛出异常并且未对异常进行处理,被称为异常完成出口。方法退出的时候相当于把栈帧出栈
如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的 对象
。
方法区(元空间):
存储已被虚拟机加载的类信息。jdk8的JVM不再有永久代(PermGen),原永久代存储的信息被分成两部分: 1、虚拟机加载的类信息(放在元空间) 2、运行时常量池(放在堆中)
元空间和方法区:第一个是hotspot的具体实现技术,第二个是JVM规范的抽象定义,不能说元数据区就是方法区,但可以说元空间用来实现了方法区
方法区中会存放静态变量,常量等数据(jdk1.8 静态变量和字符串常量池在堆中)。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。private static Object
obj=new 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:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
这样设置可能会由于动态对象年龄判断原则导致频繁full gc。于是我们可以更新下JVM参数设置:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
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垃圾回收器使用场景对比
区别 | CMS | G1 |
---|---|---|
回收对象 | 回收老年代。需要配合新生代收集器一起使用 | 老年代和新生代 |
STW时间 | 以最小停顿时间为目标 | 可预计的垃圾回收停顿时间 |
回收算法 | 标记清除 | 标记整理 |
垃圾碎片 | 产生内存碎片 | 没有内存碎片 |
垃圾回收过程 | 1.初始标记(STW) 2.并发标记 3.重新标记(STW) 4.并发清除 | 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)串行
-XX:+UseSerialGC -XX:+UseSerialOldGC
(2)并行(吞吐量优先):
-XX:+UseParallelGC -XX:+UseParallelOldGC
(3)并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGC -XX:+UseG1GC
例如:java -server -Xms1024m -Xmx1024m -XX:MetaspaceSize=1024m -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 gc,full
gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”
老年代大概率存放:
1.静态变量 – 生命周期是全局的
2.缓存对象
3.spring容器生成的对象
4.对象池中的对象
8、 JVM内存担保机制
在发生Minor GC之前
,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
。① 如果大于,则此次Minor GC是安全的
②如果小于,则虚拟机会查看-X:HandlePromotionFailure设置值是否允许担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
。
2.1> 如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的:
2.2> 如果小于,则改为进行一次Full GC。 —— 如果HandlePromotionFailure=false,则改为进行一次Full GC。
担保机制,是为了减少Full GC的执行频次,提高了应用的性能。jdk8默认就设置了次参数
9、什么是强引用、软引用、弱引用、虚引用?
强引用:>只要还有强引用指向对象,就算OOM也不会回收该对象。Object o1=new Object(),就算强引用
软引用:>内存足够的情况不回收,内存不足时就回收的对象。SoftReference,用于内存敏感的地方
Object o1=new Object();
SoftReference sf = new SoftReference<>(o1);
弱引用:>不管内存是否够用,GC时一律回收 Object o1=new 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:InitialHeapSize=1000
-Xmx1000等价于-XX:MaxHeapSize=1000
-Xss100等价于-XX:ThreadStackSize=100
例如一个设置内存的示例: java -server -Xms1024m -Xmx1024m -XX:+UseG1GC -jar springboot2019-SNAPSHOT.war
总结公式:java -server jvm的各种配置参数 -jar jar包或者war包的名字
2.查看服务器启动了那些(查看jvm初始值 或者当前值的办法)
单位换算
1Byte(字节)=8bit(位)
1KB=1024Byte(字节)
1MB=1024KB
1GB=1024MB
1TB=1024GB
jvm常用参数含义
|
3.设置jvm调优的两种方法
设置jvm调优的两种方法:
①在tomcat的bin下面的catalina.sh 里,位置cygwin=false前JAVA_OPTS=‘-server -Xms512m -Xmx768m -XX:NewSize=128m -XX:MaxNewSize=192m -XX:SurvivorRatio=8’
②使用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:SurvivorRatio=eden/from=den/to
总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等, 这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
例如:linux设置tomcat的catalina.sh
JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
或者:java -server -Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m -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:HeapDumpPath=D:\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:MetaspaceSize=1024m* 就是设置元空间的大小
实战中设置元空间大小:
java -server -Xms1024m -Xmx1024m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal -jar springboot2019-SNAPSHOT.jar
元空间的本质和永久代类似,都是对JVM规范中的方法区的实现。不过元空间与永久代的最大区别是:元空间并不在虚拟机中,而是使用本地内存,默认情况下元空间仅受本地内存大小限制
查看jvm所有配置信息:java -XX:+PrintFlagsFinal -version
这样找到MetaspaceSize对应的大小是21807104 字节 换算过来才 20.79m。也就是说元空间虽然只跟本地内存有关,但它初始值只有21m,可以调大
②栈空间的大小调整?
-Xss等价于:-XX:ThreadStackSizejava -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、如何解决线程安全?
针对线程的抢占式执行,可以采用wait,notify来调配线程执行顺序。
对于多个线程修改同一个变量,可以调节代码结构,做到一定程度上的规避(比如说给第二个线程创建一个新的对象来规避)。
操作不是原子性,可以利用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再加载一 次,保证被加载类的唯一性