JVM调优

一、JVM类加载机制

1、类加载过程

  当我们用java命令加载某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。

  具体步骤:1、调用底层的jvm.dll创建java虚拟机(C++)

       2、创建一个引导类加载器(C++)

       3、C++调用java代码创建JVM启动器实例com.misc.Lanucher(由引导类加载器加载)

       4、获取自己的类加载器并加载

       5、加载完成时会执行主类的main方法入口

       6、程序运行结束时销毁JVM

2、类加载过程的具体步骤

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main方法,new对象等,在加载阶段会在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据内存的指针或句柄(指针的指针),这个过程称之为静态链接过程,动态链接是在程序运行期间将符号引用替换为直接引用
  • 初始化:对类的静态变量初始化值,执行静态代码块

注意:主类在运行过程中如果引用到其他类会逐步加载,jar、war包里的类不是一次性全部加载的,是使用到时才会加载。

3、JAVA类加载器

  • 引导类加载器:负责加载lib目录下的核心类库,如rt.jar、charset.jar
  • 扩展类加载器:负责加载lib目录下ext扩展目录中的jar包
  • 应用程序类加载器:classPath路径下的,就是加载你自己写的那些类
  • 自定义加载器:负责加载自定义路径下的类

4、双亲委派机制

  HOW?

  1. 首先,检查指定类是否已经加载过,如果已经加载过了,就不需要再加载,直接返回。
  2. 如果没有加载过,判断一下是否有父加载器,如果有,则由父加载器加载
  3. 如果父加载器都没有找到,则由当前加载器负责加载

  WHY?

  • 沙箱安全机制:防止核心类库被随意篡改
  • 避免类的重复加载:当父加载器已经加载类该类时,子ClassLoader没必要再重新加载

  全盘委托机制

  “全盘负责”当一个classLoader加载一个类时,除非显示的使用另外一个加载器,否则该类所依赖的引用类也由这个ClassLoader载入。

  自定义类加载器  

  java.lang.ClassLoader类有两个核心方法

  loadCLass():实现了双亲委派机制

  findClass():默认是空,自定义加载器主要是重写此方法

5、Tomcat几个主要的类加载器

  1. commonLoader:Tomcat最基本的类加载器,加载的class可以被web容器及各个app所访问
  2. catalinaLoader:Tomcat私有类加载器,加载路径中的class对于webApp不可见
  3. sharedLoader:各个app共享的类加载器,加载路径中的class对于所有Webapp可见
  4. WebappClassLoader:webapp私有的类加载器,比如加载war包里的相关类

6、Tomcat打破双亲委派机制

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,因此要保证每个应用程序的类库都是独立的,是相互隔离的
  2. 部署在同一个web容器中的相同类库的相同版本可以共享
  3. web容器也有自己的依赖类库,不能与应用程序的类库混淆
  4. web容器要支持jsp的热加载,我们知道jsp也是翻译成class文件后执行的,要支持jsp修改后不用重启  

二、JVM整体结构及内存模型

 

 

 

 

 

关于元空间JVM参数:

XX:MaxMetaspaceSize:设置原空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小

XX:MetaspaceSize:元空间触发FullGc的初始伐值,默认是21M,达到该值就会触发full gc进行卸载,同时收集器会对该值进行调整,如果释放了大量空间就降低该值,如果释放了较少空间就在不超过XX:MaxMetaspaceSize的情况下适当提高该值

由于调整元空间大小需要Full Gc,这是非常昂贵的操作,如果在刚启动时就发生了Full Gc ,通常是由于永久代或元空间的大小发生了调整,一般建议XX:MaxMetaspaceSize和XX:MetaspaceSize 设置成一样的值,并设置的比初始值要大,一般8G物理机内存将这两个值都设置为256M

Xss:设置的count值越小,说明一个线程里能分配的栈帧就越少,但是对JVM来说能开启的线程数就越多

JVM调优:就是尽可能让对象在新生代里完成分配和回收,尽量别让太多对象进入老年代,避免频繁对老年代进行回收,给系统充足的内存大小,避免新生代频繁的进行垃圾回收

 

三、JVM对象创建及内存分配机制

1、对象的创建

  1. 类加载检查
    虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行类的加载过程。分配内存
  2. 分配内存
    在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

    划分内存的方法:
    指针碰撞:
    Java内存排列是绝对工整的,用过的内存放一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器
    空闲列表:
    内存是不工整的,虚拟机维护一个列表,记录哪些内存是可用的

    并发问题的解决办法:
    CAS:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理
    TLAB(本地线程缓冲):把内存分配的动作划分在不同的空间进行,即每个线程在Java堆中预先分配一小块内存
    通过­XX:+/­UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启­XX:+UseTLAB),­XX:TLABSize 指定TLAB大小。
     
  3. 初始化
    内存分配完成后,虚拟机将分配到的内存空间都初始化为零值,如果使用TLAB,这一过程也可提前至TLAB分配时进行,这一步骤保证了实例字段在JAVA代码中可以不赋初始值就直接使用。设置对象头初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息,这些信息存放在对象头之中。
  4. 对象在内存中的布局
    对象头:对象自身的运行时数据,如哈希码,GC分代年龄、线程持有的锁、锁状态标志、偏向线程ID、偏向时间戳。另一部分是类型指针,指向类的元数据的指针,通过这个指针确定是哪一个类的实例。
    实例数据:
    对齐填充:

    什么是java对象的指针压缩?
    启用指针压缩:­XX:+UseCompressedOops(默认开启),禁止指针压缩:­XX:­UseCompressedOops
    为什么要进行指针压缩?
    1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
    2.为了减少64位平台下内存的消耗,启用指针压缩功能
    3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)
    4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
    5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好

2、对象内存分配

  1. 内存分配流程
  2. 对象栈上分配(依赖于逃逸分析和标量替换
    JVM通过逃逸分析确定对象不会被外部访问,如果不会逃逸可以将对象在栈上分配内存,这样该对象锁占用的内存空间就可以随栈帧出栈而销毁,减轻了垃圾回收的压力。
    逃逸分析:当一个对象在方法中被定义后,可能被外部方法所引用,JDK7以后会默认开启逃逸分析
    标量替换:通过逃逸分析后,确定不会被外部所引用,JVM不会创建该对象,而是将对象分解若干个被这个方法所使用的成员变量,这些代替的成员变量在栈帧或寄存器上分配空间
    聚合量:不可被进一步分解的量称之为聚合量,例如java对象
  3. 对象在Eden区分配
    Minor GC/Young GC:新生代垃圾收集动作,minor GC 回收非常频繁,速度也比较快
    Full GC/Major GC:回收老年代,年轻代的垃圾,回收速度比Minor GC慢十倍以上
    Eden与Survivor区默认8:1:1 
    注意:当Eden区被分配完了时,虚拟机将发起一次Minor GC,GC期间又发现Survivor区满了,只好把新生代的对象提前移到老年代中去

  4. 大对象直接进入老年代(避免对大对象内存的复制操作而降低效率
    大对象就是需要大量连续内存空间的对象(比如:字符串,数组)。JVM参数-XX:PretenureSizeThreshold可以设置大对象的大小,超过这个大小会直接进入老年代
  5. 长期存活的对象将进入老年代
    对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。 
    年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。
  6. 老年代空间分配担保机制
    年轻代每次minor gc之前,jvm都会计算下老年代的剩余可用空间,如果这个空间小于现有年轻代里所有对象大小之和(包括垃圾对象),就会看  -XX:-HandlePromotionFailure 参数是否设置(jdk1.8默认设置),就会看老年代可用大小是否大于之前每一次minor gc后进入老年代的对象平均大小,如果小于或者没有设置,那么就会触发一次Full GC对老年代和年轻代一起回收,如果还是没有空间就会发生OOM,当然minor gc 后老年代还是没有空间放minor gc 中存活的对象,也会触发 Full GC ,也会发生OOM
  7. 如何判断对象已经死亡
    引用计数法:
    给对象添加一个引用计数器,每当有引用到的地方,计数器+1,失去引用,计数器-1,当计数器为0时,对象已经死亡
    可达性分析算法:
    GC Roots对象作为起点,开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余对象标记为垃圾对象
  8. 几种常见的引用类型:
    强引用:普通的变量引用 

    public static User user = new User(); 
    软引用:正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。 
    public static SoftReference<User> user = new SoftReference<User>(new User()); 
    弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用 1 public static WeakReference<User> user = new WeakReference<User>(new User());
    虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
 
 finalize()方法判定对象是否存活:即使可达性分析中不可达的对象也并非是非死不可,真正宣告一个对象死亡至少要经历再次标记的过程
    1、第一次标记:筛选此对象是否需要执行finalize()方法,如果没有覆盖finalize方法,对象直接被回收
    2、第二次标记:执行finalize方法,在finalize方法中添加一个引用便可拯救自己,注意finalize只能被执行一次
 

 如何判断一个类是无用的类?

  • 该类所有的实例已经被回收,java堆中不存在类的任何实例
  • 加载该类的classLoader已经被回收
  • 该类对应的class对象没有在任何地方被引用

四、垃圾收集算法

1、分代收集理论

  java堆一般分为年轻代和老年代,这样我们就可以根据各块的特点选择合适的垃圾收集算法

2、复制算法

  为了解决效率问题,复制算法出现了,他可以将内存分为大小相同的两块,每次使用其中的一块,当这一块内存使用完以后,将还存活的对象复制到另一块中去,然后把使用的空间一次清理掉,这样每次回收都是堆一半的内存进行回收。

3、标记清除算法

  标记存活的对象,统一回收未标记的对象(也可以反过来)

  会产生两个问题:1、标记的对象太多,效率不高
          2、标记清除后会产生大量的不连续碎片

4、标记整理算法

   第一步与标记清除算法一样,后续让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

五、垃圾收集器

1、Serial收集器(-XX:+UseSerialGC-XX:+UseSerialOldGC)

  单线程,并且会暂停其他所有线程(STW),新生代采用复制算法,老年代采用标记整理算法。

2、Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))   

  Serial收集器的多线程版本

3、ParNew收集器(-XX:+UseParNewGC)

   ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。 

4、CMS收集器(-XX:+UseConcMarkSweepGC(old)) ()

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

  步骤:
  1. 初始标记:STW,记录gc roots 直接能引用的对象,速度很快
  2. 并发标记:是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
  3. 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
  4. 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理
  5. 并发重置:重置本次GC过程中的标记数据。 
  6. 缺点
    对CPU资源敏感(会和服务抢资源);
    无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
    它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
    执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收 
  7. 核心参数
    1. -XX:+UseConcMarkSweepGC:启用cms
    2. -XX:ConcGCThreads:并发的GC线程数
    3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
    4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一
    5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
    6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设
    定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
    7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引
    用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
    8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
    9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW; 
    只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值

5、G1收集器(-XX:+UseG1GC)

  是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征. 
  G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。
  G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。 

 

G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。 
 
步骤:
  1. 初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;  
  2. 并发标记(Concurrent Marking):同CMS的并发标记 
  3. 最终标记(Remark,STW):同CMS的重新标记
  4. 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划
回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。
 
具备以下特点:
1、并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
3、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。 
 
G1收集器分类
YoungGC:YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做YoungGC,直到下一次Eden区放满,G1计算回收时间接近参数       -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC 

 

MixedGC :不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,
      需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC 
 
Full GC :停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了) 

   

G1收集器参数设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。 
 
G1垃圾收集器优化建议
里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc
 
什么场景适合使用G1(Kafka
1. 50%以上的堆被存活对象占用
2. 对象分配和晋升的速度变化非常大
3. 垃圾回收时间特别长,超过1秒
4. 8GB以上的堆内存(建议值)
5. 停顿时间是500ms以内 

 6、ZGC收集器(-XX:+UseZGC) 

  TB级别收集器,不分代

六、常用命令

1、jps:查看所有java进程

 

 

2、jmap -histo 15322     (查看内存信息:实例个数、内存大小、类名)

 

 

 3、jmap -heap 15322 (查看堆信息)

 

 

 

 

 

 

 

4、jmap‐dump:format=b,file=eureka.hprof 14660(内存很大的时候,可能会导不出来)

jvm 参数内存溢出自动导出
1.-XX:+HeapDumpOnOutOfMemoryError
2.-XX:HeapDumpPath=./ (路径)
导出后用jvisualvm 分析dump文件
 

5、Jstack 找出占用cpu最高的线程堆栈信息

  1. top -p 15322  显示java进程的内存情况

     

     

  2. 按H获取每个每个线程的内存情况

     

     

  3. 找到内存和cpu占用最高的线程tid,假设是18929,转换16进制49f1

     

     

  4. 查看对应对应堆栈信息找出可能存在问题的代码

     

6、Jinfo 查看正在运行java程序的扩展参数

  1. jinfo -flags 15322  查看jvm参数


  2. jinfo -sysprops 15322 查看java系统参数

7、Jstat 查看堆内存各部分的使用量以及加载类的数量

  1. jstat -gc 15322 评估内存使用及GC压力情况

     

     S0C:第个幸存区的大小,单位KB

    S1C:第二个幸存区的大小
    S0U:第个幸存区的使用大小S1U:第二个幸存区的使用大小
    EC:伊甸园区的大小
    EU:伊甸园区的使用大小
    OC:老年代大小
    OU:老年代使用大小
    MC:方法区大小(元空间)
    MU:方法区使用大小
    CCSC:压缩类空间大小
    CCSU:压缩类空间使用大小
    YGC:年轻代垃圾回收次数
    YGCT:年轻代垃圾回收消耗时间,单位s
    FGC:老年代垃圾回收次数
    FGCT:老年代垃圾回收消耗时间,单位s
    GCT:垃圾回收消耗总时间,单位s 
  2. jstat -gccapacity 15322 (堆内存统计)
    NGCMN:新生代最小容量
    NGCMX:新生代最大容量
    NGC:当前新生代容量
    S0C:第个幸存区大小
    S1C:第二个幸存区的大小
    EC:伊甸园区的大小
    OGCMN:老年代最小容量
    OGCMX:老年代最大容量
    OGC:当前老年代大小
    OC:当前老年代大小
    MCMN:最小元数据容量
    MCMX:最大元数据容量
    MC:当前元数据空间大小
    CCSMN:最小压缩类空间大小
    CCSMX:最大压缩类空间大小
    CCSC:当前压缩类空间大小
    YGC:年轻代gc次数
    FGC:老年代GC次数 
  3. jstat -gcnew 15322 (新生代垃圾回收统计)

    S0C:第个幸存区的大小
    S1C:第二个幸存区的大小
    S0U:第个幸存区的使用大小
    S1U:第二个幸存区的使用大小
    TT:对象在新生代存活的次数
    MTT:对象在新生代存活的最大次数
    DSS:期望的幸存区大小
    EC:伊甸园区的大小
    EU:伊甸园区的使用大小
    YGC:年轻代垃圾回收次数
    YGCT:年轻代垃圾回收消耗时间
  4. jstat -gcnewcapacity 15322 (新生代内存统计)

    NGCMN:新生代最小容量
    NGCMX:新生代最大容量
    NGC:当前新生代容量
    S0CMX:最大幸存1区大小
    S0C:当前幸存1区大小
    S1CMX:最大幸存2区大小
    S1C:当前幸存2区大小
    ECMX:最大伊甸园区大小
    EC:当前伊甸园区大小
    YGC:年轻代垃圾回收次数
    FGC:老年代回收次数 
  5. jstat -gcold 15322 (老年代垃圾回收统计)

    MC:方法区大小
    MU:方法区使用大小
    CCSC:压缩类空间大小
    CCSU:压缩类空间使用大小
    OC:老年代大小
    OU:老年代使用大小
    YGC:年轻代垃圾回收次数
    FGC:老年代垃圾回收次数
    FGCT:老年代垃圾回收消耗时间
    GCT:垃圾回收消耗总时间
  6. jstat -gcoldcapacity 15322 (老年代内存统计)

     

     OGCMN:老年代最小容量

    OGCMX:老年代最大容量
    OGC:当前老年代大小
    OC:老年代大小
    YGC:年轻代垃圾回收次数
    FGC:老年代垃圾回收次数
    FGCT:老年代垃圾回收消耗时间
    GCT:垃圾回收消耗总时间 

  7. jstat -gcmetacapacity 15322 (元空间垃圾回收统计)

     

     MCMN:最小元数据容量

    MCMX:最大元数据容量
    MC:当前元数据空间大小
    CCSMN:最小压缩类空间大小
    CCSMX:最大压缩类空间大小
    CCSC:当前压缩类空间大小
    YGC:年轻代垃圾回收次数
    FGC:老年代垃圾回收次数
    FGCT:老年代垃圾回收消耗时间
    GCT:垃圾回收消耗总时间

  8. jstat -gcutil 15322
    S0:幸存1区当前使用比例
    S1:幸存2区当前使用比例
    E:伊甸园区使用比例
    O:老年代使用比例
    M:元数据区使用比例
    CCS:压缩使用比例
    YGC:年轻代垃圾回收次数
    FGC:老年代垃圾回收次数
    FGCT:老年代垃圾回收消耗时间
    GCT:垃圾回收消耗总时间 

 七、如何调优?

1、JVM运行情况预估
用 jstat gc-pid 命令可以计算出如下些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
2、年轻代对象增长的速率
可以执行命令 jstat-gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
3、Young GC的触发频率和每次耗时
知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发次,Young GC的平均耗时可以通过 YGCT/YGC公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。
4、每次Young GC后有多少对象存活和进入老年代
这个因为之前已经大概知道Young GC的频率,假设是每5分钟次,那么可以执行命令 jstat-gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。
 
5、Full GC的触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。
优化思路
其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。
 

full gc比minor gc还多的原因有哪些?

1、元空间不足导致频繁full gc

2、显示调用System.gc()造成多余full gc ,-XX:+DisableExplicitGC 参数金庸

3、老年代空间分配担保机制

 

什么是内存泄漏?

常见的多级缓存架构redis+jvm缓存,很多程序员图方便jvm缓存就只是适用一个hashmap,结果这个缓存map越来越大,一直占用老年代的空间,时间长了就会发生full gc,对于一些老旧数据没有及时清理,时间长了除了会导致full gc 还会导致OOM。这种情况考虑使用一些成熟的JVM框架来解决,如ehcache等自带的LRU数据淘汰算法的框架作为JVM级的缓存

 

三种字符串操作

String s = “abc”;// s指向常量池中的引用(用equals方法检查常量池中有没有这个常量,直接有返回引用,没有创建一个返回对象引用)

String s1 = new(“abc”);// s1指向内存中的对象引用(equals方法检查常量池中有没有这个常量,没有创建,然后在堆中在创建一个对象,返回引用)
String s2 = s1.intern(); // 如果池中已经包含一个等于s1对象的字符串,则返回池子中的字符串,否则直接指向s1

 

1 String s0="zhuge";
2 String s1="zhuge"; 
3 String s2="zhu" + "ge";
4 System.out.println( s0==s1 ); //true 
5 System.out.println( s0==s2 ); //true
都是字符串常量,在编译时期就确定了

 

1 String s0="zhuge"; 
2 String s1=new String("zhuge"); 
3 String s2="zhu" + new String("ge"); 
4 System.out.println( s0==s1 ); // false 
5 System.out.println( s0==s2 ); // false 
6 System.out.println( s1==s2 ); // false

new()创建的字符串不是常量,在编译时期不能确定

 

1 String a = "a1"; 
2 String b = "a" + 1; 
3 System.out.println(a == b); // true 
4 String a = "atrue"; 
5 String b = "a" + "true"; 
6 System.out.println(a == b); // true 
7 String a = "a3.4"; 
8 String b = "a" + 3.4; 
10 System.out.println(a == b); // true

1、true、3.4在字符串之后在编译时期就确定为常量

 

 String a = "ab"; 
 String bb = "b"; 
 String b = "a" + bb; 
System.out.println(a == b); // false

bb作为变量在编译时期不确定,在运行时才确定,会生成一个新的对象

 

 String a = "ab"; 
 final String bb = "b"; 
 String b = "a" + bb; 
 System.out.println(a == b); // true

final在编译时期被解析为常量

 

String a = "ab"; 
 final String bb = getBB(); 
 String b = "a" + bb; 
 System.out.println(a == b); // false 
private static String getBB() {  return "b";  }

getBB()在编译时期无法确定

 

 String s = "a" + "b" + "c"; //就等价于String s = "abc"; 
 String a = "a"; 
 String b = "b"; 
 String c = "c"; 
 String s1 = a + b + c; // 编译时期a\b\c作为变量不确定

 

八种基本类型的包装类和对象池

 

posted @ 2020-10-08 22:57  vvning  阅读(1199)  评论(0编辑  收藏  举报