java-jvm-2-mm

134 阅读31分钟

JVM

内存结构

jvm_1_8.jpeg

Java8的JVM内存结构

  • 堆:heap(YG+OG,管理实例生命周期)
  • 栈:
    • 虚拟机栈(java-method-stack),
    • 本地方法栈(java-native-method-stack),
    • 程序计数器:
  • 系统本地内存
    • 元空间:Metaspace;其中元空间在本地内存中。
    • 直接内存:DirectBuffer;特殊场景使用:零copy,NIO

线程私有:程序计数器、虚拟机栈、本地方法区 线程共享:堆(方法区), 堆外内存

虚拟机栈

  1. 普通java方法执行时的内存空间,是线程私有的,生命周期与线程相同。
  2. 每个方法被执行的同时会创建栈桢主要保存执行方法时的局部变量表、操作数栈、动态连接、方法返回地址等信息
  3. 方法执行时入栈,方法执行完出栈,入栈出栈的时机很明确。出栈就相当于清空了数据,所以这块区域不需要进行GC.
  4. 可能出现栈溢出的异常

本地方法栈: java native方法的运行空间,也不需要进行GC.

stack-frame.png

  • 局部变量表(Local Variables): 方法参数和定义在方法体内的局部变量(基本类型 + 引用类型)
  • 动态链接(Dynamic Linking):指向运行时常量池的方法引用
  • 方法返回地址(Return Address):方法正常退出或异常退出的地址
  • 操作数栈(Operand Stack)(内部调用栈,死递归的溢出地方)

程序计数器

  • 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致;可以把它看作是当前线程执行的字节码的行号(内存地址)指示器。
  • 主要应用在线程切换的场景:在任何一个时刻,一个处理器只会执行一个线程。
  • 程序计数器是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域,所以这块区域也不需要进行GC

系统本地内存

就是机器内存 Java8中本地内存也是我们通常说的堆外内存.可以为java虚拟机提供元空间(MetaSpace)和直接内存(DriectBuffer)。也不需要进行GC.

Metaspace

就是java8之前的永久代(PG),实际上指的是 HotSpot虚拟机上的永久代,它用永久代实现了JVM规范定义的方法区功能。(PG>=方法区)

方法区:存储对象的方法,之前位于PG,现在MP.

主要存储类的信息(Class Meta),常量(final),静态变量(static),,即时编译器编译后代码等,这部分由于是在堆中实现的,受GC的管理,不过由于PG有-XX:MaxPermSize的上限,所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区,JDK7),很容易造成 OOM。

所以在 Java 8 中就把方法区(PG)的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制,也就不会进行GC,也因此提升了性能.也就不存在由于永久代限制大小而导致的OOM异常了. (java7中PG在jvm的堆内存,空间不足触发FGC,FGC会导致stopTheWorld,触发OOM异常)。

DirectBuffer

NIO类(JDK1.4引入)中基于通道(channel)和缓冲区(Buffer)的I/O方式,通过使用Native函数库直接分配的堆外内存,特点是不受堆大小限制,避免在Java堆和Native堆之间来回复制数据(零拷贝),提高IO性能。

heap-堆

GC发挥工作的空间,jvm中分配最大,JAVA8中包含YG+OG,主要存储java实例和数组.

这是Java7的JVM空间,方法区即PG应该在heap中(heap=YG+OG+PG)。 直接内存属于JVM运行分配的内存空间,是机器内存。

String常量区的理解

String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降

// 常量池调整大小
-XX:StringTableSize=99991 
  • 位于heap,String映射内存的char, Hashtable可以减少jvm-mm之间的映射.从而优化内存
  • heap的某个区域内

JVM内存结构

  • stack(栈)
    • java-stack
    • native-stack
    • program-counter
    • heap(堆) = young(YG) + old(OG) [+PG,1:2=3]
    • YG := eden + survivor,survivor = from + to(from=to,大小一样,8:1:1=10)
    • OG :=
    • PG : 属于jdk7
  • mem(系统内存)
    • metaspace
    • directbuffer

Heap-堆

  • jvm分代

    • heap = YG + OG(YG:OG=1:2,OG占2/3))
    • YG = eden + survivor, survivor = from + to(e:s0:s1=8:1:1,eden占YG的8/10,eden:from:to=8:1:1)
  • 参数

    • -Xms 堆初始大小,模式物理内存1/64
    • -Xmx 堆最大大小,默认物理内存1/4
    • 设置大小一样,阿里建议
  • 异常

    • java.lang.OutOfMemory : java heap space (yong|old|perm)
  • Young generation (YG: 年轻代)

    • 存储new的对象和数组(可以制造YG溢出)
    • YG = eden(EG) + survivor(SG), survivor = From + To
    • eden空间不足触发YGC,扫描eden+from空间,存活的对象存入to并年龄代+1,to空间不足存入OG
    • 参数
      • -Xmn YG的大小,一般是Xmx的1/3
      • -XX:NewRatio = 2, YG:OG = 1:2
      • -XX:SurvivorRatio = 8, EG:From:To = 8:1:1 (这3个够用分配YG空间)
      • -XX:NewSize 初始EG大小
      • -XX:MaxNewSzie 最大EG大小
  • Old generation(OG: 年老代,空间不足FGC)

两个关键比例:1:2 = Y/O; 8:1:1

  • Permanent generation(PG: 永久代)

    • JVM存放Class的Meta信息,static,常量信息(final); 也叫方法区(如HotSpot虚拟机)
    • 运行期间GC不回收,空间不足直接触发FGC
    • 参数
      • -XX:PermSize 初始大小
      • -XX:MaxPermSize 最大大小
    • 异常: java.lang.OutofMemory: perm space, 可能加载过多的jar, Class, static, final 信息;
    • 空间不够,触发FULL GC
  • MetaSpace(元空间:不属于JVM的内存,是系统内存)

    • JDK8版本以后,MP替代PG,PG不存在heap了,MP在系统内存,不在JVM(只存储了class信息)
    • 参数
      • -XX:MetaspaceSize 初始大小
      • -XX:MaxMetaspceSize 最大大小
    • 异常: java.lang.OutOfMemory: Metaspace
  • DirectBuffer,JDK-NIO中增加了channel+Buffer,buffer可以调用native方法分配系统内存,并使用DirectByteBuffer对象做为引用使用该内存空间 - 早期的IO,如读入文件是读入YG空间 - Buffer是读入系统内存=>netty的零copy场景:提高IO性能

  • java.lang.OutOfMemoryError: Java heap space 是ERROR异常=>JVM退出

Stack-栈

  • 分为jvm-stack(虚拟机栈)和native-stack(本地方法栈)和程序计数器(记录jvm指令的执行的位置)
  • -Xss设置每个thread的栈大小,默认分配大小1M
  • 栈溢出: java.lang.StackOverFlowError
  • 线程的空间 = 程序计数器(记录字节码指令执行的位置) + jvm-stack(线程栈帧:局部变量,出口地址) + native-stack(本地方法)

JVM内存结构配置参数

  • -client | server - client:客户端模式,注重用户体验,YGC和FGC都是串行; server:服务端模式,注重吞吐、停顿时间, YGC,FGC都是并行
  • -Xdebug - 启动JVM的debug模式
  • -Xnoagent - 禁用默认debugger: sun.tools.debug
  • -Xms - 初始堆大小,单位[K/k/m/M]
  • -Xmx - 最大堆大小,物理内存1/4
  • -Xmn - YG大小
  • -XX:NewRatio=2 - YG : OG = 1 : 2
  • -XX:SurvivorRatio=8 - eden:from:to=8:1:1, survivor = from + to, eden:survivor = 8:2
  • -XX:PermSize - PG初始大小
  • -XX:MaxPermszie - PG最大Size,最大80M
  • -XX:MetaspaceSize - MP初始Size,不限制,参考PG;
  • -XX:MaxMetaspzceSize - MP最大Size

配置样本

-Dapp.id=app-id
-Ddubbo.registry.file=${JAR_PATH}/dubbo.cache
-Xdebug
-server
-Xmx512m
-Xms512m
-XX:NewRatio=2
-XX:SurvivorRatio=8
-XX:MaxMetaspaceSize=128m
-XX:MetaspaceSize=128m
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-Xloggc:${GC_LOG_PATH}

GC

垃圾确定算法

判断对象存活,是否是垃圾的方法: 引用记数 和 GC-roots(可达性算法)

引用计数

在实例对象头里记录了被引用次数(每次引用计数器+1)。

但是不能解决循环引用的问题,已经废弃

  • 强引用(普通使用)
  • 软引用(内存不够的时候不会被GC回收,做临时缓存使用) => 业务编程可能用到
  • 弱引用(等待下次GC, ThreadLocalMap, StringPool) => 设计数据结构容器
  • 虚引用(GC通知)

GC-Root

现代虚拟机基本都是采用这种算法来判断对象是否存活. 从称为GC-Root的对象开始,根据对象的引用关系递归向下查找每一层的引用对象。 如果有的对象不在引用琏中,则被GC回收。当发生GC时,会先判断对象是否执行了finalize方法,如果未执行,则会先执行 finalize 方法。finalize方法只会被执行一次。

哪些对象可以作为GC-Root

  • (stack)虚拟机栈(栈帧中的本地变量表)中引用的对象(java普通方法的引用)
  • (native-stack)本地方法栈中JNI(即一般说的 Native 方法)引用的对象(native方法里的引用)
  • (PG/meta)方法区中类静态属性引用的对象(static/final)
这些引用会在运行期间长期保存
- Local variable and input parameters of the currently executing methods
- Active threads. => 正在使用
- JNI references
- Static field of the loaded classes

JNI

Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。通过调用本地的库文件的内部方法,使 JAVA 可以实现和本地机器的紧密联系,调用系统级的各接口方法.

当调用 Java 方法时,虚拟机会创建一个栈桢并压入 Java 栈,而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法.

jni.png

JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
   // JNI引用(GC-root)
   jclass jc = (*env)->FindClass(env, STRING_PATH);
}

垃圾回收算法

总结

GC算法描述作用的内存算法实现-垃圾回收器
copying(标记复制)copy到新空间,比如from=>to空间YG在用,YGCSerial,ParNew,ParallelScavenge
mark-sweep(标记清除)先标记再清理;容易产生内存碎片OG已经基本不用CMS
mark-compact(标记整理)先标记再移动;处理内存碎片OG在用,FGCParallelOld,SerialOld
分代收集每次YGC存活的对象年龄代+1,默认15时进入OGYG+OG都可用
分区收集整个heap分区,独立回收全堆

YGC:直接使用复制算法,有单线程/并发,分代检查 FGC:标记+整理,因为要判断对象的存活,例如CMS算法

标记清除算法

CMS

  1. 先用GC-root给对象打标记
  2. 清除标记的对象(类似LRU结构)
  3. 容易产生内存碎片

标记整理

在标记清除的算法基础上增加了内存整理。即:把RC-Root标记存活的对象移动到内存一边,另一边清除。

复制算法

  1. 内存空间分成2分,其中一份用来存储
  2. YGC时,把GC-Root标记存活的对象移动到另一份,
  3. 解决内存碎片,但是内存利用率不高

分代回收策略

  1. parallelGC = ps + pmc
  2. CMS = pnew + cms

是现代虚拟机采用的首选算法,与其说它是算法,倒不是说它是一种策略,因为它是把上述几种算法整合在了一起。

deap内存的分配2个关键比例1:2=YG:OG,8:1:1 = eden:S0:S1;(jstat中S0(survivor0)=from,s1(survivor1)=to);

每发生一次YGC,对象的年龄代+1;S0与S1交替使用(第一次YGC:从eden,s0拷贝存活对象到s1,第二次YGC从eden,s1拷贝存活对象到s0);

对象进入OG

  1. 年龄阈值设置为 15, 当发生下一次 Minor GC 时,S0中有个对象年龄达到15,达到我们的设定阈值,晋升到老年代
  2. 创建的对象过大,eden放不开
  3. 还有一种情况也会让对象晋升到老年代,即在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代

垃圾回收器

YGC && FGC 期间挂起所有工作进程

  • YGC使用copy算法
    • eden空间不足,触发YGC,扫描eden+from; 存活的对象年龄+1,并存入to,to空间不足存入O
    • 清空eden+from
    • 交替使用from,to空间的引用,下一次YGC继续扫描新的from(原来的to)
  • FGC使用标记(整理)算法
    • 先标记OG空间存活的对象(GC-Root)
    • 清除=回收没有标记的对象; 整理=整理存活的对象,去除内存碎片

YGC回收器

  1. Serial在 Client 模式,单线程
  2. ParNew工作在 Server 模式,多线程,适合多核场景
  3. Parallel Scavenge。复制算法,多线程,

OGC回收器

  1. Serial Old,client模式,单线程
  2. Parallel Old
  3. CMC,CMS收集器是以实现最短STW时间为目标的收集器

CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间; 而 Parallel Scavenge 目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间))是自适应策略。(指标,触发GC的条件)

web应用一般使用CMS(JDK7)

初始GC参数

# YG 

-XX:+Serial # 使用copy算法,单线程,YG空间,client模式下默认
-XX:+UseParNewGC # YG并行GC,copy算法,server模式下默认设置,GC日志中`par=并行,new=新生代`
-XX:+ParallelScavenge - YG,copying算法,自适应调节触发,根据(吞吐量=运行用户代码时间/(运行用户时间+垃圾回收时间)); 保证高吞吐(其他的空间不足才触发->Parnew)
-XX:+UseParallelGC - 并行GC,只对YG有效(待测试);一般不用
# OG 

-XX:+SerialOld  #`old`=>OG空间,标记整理算法,client模式默认使用
-XX:+UseParOldGC  # OG并行 => Old是年老代,标记整理算法,吞吐量触发
`-XX:+UseConcMarkSweepGC` # 启用CMS,FGC,并发,多线程清除算法=>提供最短垃圾回收停顿时间(STP:stop the world)

# 其他
-XX:ParallelGCThreads      # GC并行线程数,默认=CPU核数
-XX:ScavengcBeforeFULLGC   # FGC之前先执行YGC
-XX:GCTimeRatio  # gc时间占程序运行时间的百分比(默认99%),配置Parallel-Scavenge
-XX:MaxGcPauseMills # YG回收最大时间,如果无法满足,JVM自动调整YG大小来满足此值,配置Parallel-Scavenge
# garbage firts(G1:最新的算法)

# 从年轻代晋升到老年代的年龄阈值,默认15
-XX:MaxTenuringThreshold =n

CMS参数

(CMS)Concurrent Mark and Sweep,并发mark和sweep,It uses the parallel stop-the-world mark-copy algorithm in the Young Generation and the mostly concurrent mark-sweep algorithmin the Old Generation.(YG使用标记复制,OG使用标记清除), By default, the number of threads used by this GC algorithm equals to 1⁄4 of the number of physical cores of your machine.(并发默认是1/4内核数个线程)

  • 使用系统1/4核执行CMS
  • YG使用并发复制算法
  • OG使用并发清除算法

CMS减少STW时间,支持垃圾回收线程和应用线程并行执行, CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停,它的收集周期是这样:(原文分成的7Phrase)

  1. 初始标记(CMS-initial-mark) ,发生STW,先找到GCroot
  2. 并发标记(CMS-concurrent-mark),并发扫描GCroot
  3. 重新标记(CMS-remark) ,发生STW(就是检查GCroot的时候)
  4. 并发清除(CMS-concurrent-sweep)

分成4个阶段,标记阶段应用是暂停的(1,3阶段是暂停的,发生STW),整个过程中耗时最长的是并发标记和标记清理(2,4),不过这两个阶段用户线程都可工作,所以不影响应用的正常使用,所以总体上看,可以认为 CMS 收集器的内存回收过程是与用户线程一起并发执行。

CMS 无法处理浮动垃圾(Floating Garbage),可能出现 「Concurrent Mode Failure」而导致另一次 Full GC 的产生,由于在并发清理阶段用户线程还在运行,所以清理的同时新的垃圾也在不断出现,这部分垃圾只能在下一次 GC 时再清理掉(即浮云垃圾)

CMS 采用的是标记清除法,上文我们已经提到这种方法会产生大量的内存碎片

-XX:+UseConcMarkSweepGC - 启用CMS,使用标记清除算法,会出现内存碎片
-XX:ParallelCMSThreads=20 - CMS并行线程
-XX:+UseCMSCompactAtFullCollection - CMS收集器顶不住要进行 FullGC时开启内存碎片的合并整理过程,内存整理会导致 STW,停顿时间会变长
-XX:+CMSScavengeBeforeRemark - 在FGC前,先YGC
-XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled - 开启回收PG, 默认不开启
-XX:+CMSInitiatingOcuupancyFraction = 80 - OG达到80%时开始回收,自适应触发

G1参数

G1(Garbage First)收集器是面向服务端的垃圾收集器

  • 能与应用程序线程并发执行
  • 运作期间不会产生内存碎片(CMS),G1从整体上看采用的是标记-整理法
  • 在STW上建立了可预测的停顿时间模型,用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内
-XX:+UseG1GC  # G1处理器
-XX:MaxGCPauseMillis=200 # 指定STW时间
-XX:InitiatingHeapOccupancyPercent=45 # 45%,初始deap占用比,通常可以调整这个数值,使得并发周期提前进行

G1(grabage-first:包含大部分garbage的region优先回收->可以理解为YG优先回收); 内存分位不同的region,每个周期回收全部的YG+部分OG,可以指定STW的时间

GC日志参数

垃圾回收器使用演变

  1. 默认垃圾回收器
  2. jdk8 使用CMS
    1. 未来减少STW
  3. jdk9 使用G1
    1. 减少内存碎片
    2. 可预测性STW算法
# 参数
-XX:+PrintGC - 输出GC日志,  输出格式: [GC 118250K->113543K(130112K), 0.0094143 secs]  [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails - 输出GC详细日志, 
-XX:+PrintGCTimeStamps - GC时间戳,自JVM启动开始计算
-XX:+PrintGCDateStamps - GC日期格式,2013-05-04T21:53:59.234+0800
-XX:+PrintHeapAtGC  - GC时堆信息
-Xloggc: ../logs/gc-·date +%F·.log  - GC日志文件
-XX:+PrintTenuringDistribution - 每次YGC后,输出YG中存活周期,存活周期的阈值可以设置
-XX:+PrintGCApplicationStoppedTime - GC发生期间,应用停止的时间, 可以用于优化GC方法
-XX:+PrintGCApplicationConcurrentTime - GC发生期间,应用并行执行的时间
-XX:+PrintTLAB - 查看TLAB空间的使用情况
-XX:+PrintClassHistogram - class histogram信息
-verbose:gc - 效果与-XX:PrintGC一样,区别在于执行的工具类不一样,很old的版本,基本不用了

gc参数模版

// 目的是保存发生异常时堆栈现场
GCARGS=
"
-XX:+PrintGCDetails  
-XX:+PrintGCDateStamps 
-XX:+PrintHeapAtGC  
-Xloggc:/tmp/app-gc-`date +%F`.log
"

逃逸分析

通过逃逸分析,Java Hotspot 编译器能够分析出stack中的一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

  • JIT 编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配
  • 分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无需进行垃圾回收了。
  • 常见栈上分配的场景:成员变量赋值、方法返回值、实例引用传递
  1. 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  2. 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中,称为方法逃逸(有点像反闭包)
public static StringBuffer craeteStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   // 发生逃逸
   return sb;
}
public static String createStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   // 底层是new, 不会逃逸
   return sb.toString();
}

逃逸分析对代码的优化

  1. 栈分配: 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配,不需要GC
  2. 同步省略(锁消除): 如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步,即锁消除(synchronized消除)
  3. 分离对象或标量替换: 有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而存储在 CPU 寄存器
// 标量(Scalar)是指一个无法再分解成更小的数据的数据。Java 中的原始数据类型就是标量。
// 还可以分解的数据叫做聚合量(Aggregate)
class Point{  // 聚合引用,可以拆解
    private int x;
    private int y;
}
private static void alloc() {
   // 没有逃逸,并可以拆解 => 标量替换
   Point point = new Point1,2);
   System.out.println("point.x="+point.x+"; point.y="+point.y);
}
// 标量替换 的 等价替换效果
private static void alloc() {
  //不需要创建对象Point, 只创建int
   int x = 1;
   int y = 2;
   System.out.println("point.x="+x+"; point.y="+y);
}

常量池

heap中

JVM调优工具

主要的指令: jps|jmap|jstat|jstack|jinfo|jhat|jhsdb|btrace, 一切使用以--help为准

jps

jps(jvm process state), jps [-lv] [hostid]; // hostid不是localhost时使用rmi通信,不建议使用;直接jps -l够用了;

jinfo

jinfo(jvm info);jvm启动配置信息(jvm,参数)

jstack

jstack(java stack trace),jstack -l pid

日志定位

  • prio: 表示线程的优先级
  • nid: tid映射到系统的线程id,通过系统指令: top -Hp pid获取系统线程id,nid是hex进制的
  • tid: jvm内的线程Id; tid != Thread.currentThread().getId()
  • Thread.currentThread().getId(): 好像是线程池工厂中的计数器。
  1. 查看死锁
  1. grep线程的状态WAITING,BOLOKED;waiting表示线程空闲,调用了对象的wait(),blocked表示线程竞争锁(monitor)状态
  2. 死锁就是2个线程互相竞争对方持有的monitor,查看关键词lockedwaiting to lock;
  3. 定位代码的行数
  1. 查看启动的worker线程个数, 需要注意排除jvm自带的线程
  1. StackOverflowError

jmap

(jvm memory map),如同Solaris系统的pmap, 其中jmap -histo[:live] pid 最重要的分析工具

# 参数
- histo[:live]  输出所有对象的直方图统计, =>可以查看异常的对象的占有情况(可能出现堆内存溢出的对象)
- heap 输出堆info,=>可以查看堆内存分配情况
- permstat 输出永久代数据统计
- finalizerinfo 输出等待销毁的对象
  • 可以读取的信息
    • heap = YG + OG + PG
    • YG = EG + SG
    • SG = From + To
    • NewRatio = 2 , YG : OG = 1:2
    • SurvivorRation = 8, EG : From : To = 8 : 1 :1

比较重要的命令

java-jmap-heap.jpg

jhat

配合jmap使用,主要读取jmap的dump文件的内容

jstat

java-jstat.jpg

EC、EU:Eden区容量和使用量
S0C、S1C、S0U、S1U:Survivor(0|1)区容量(Capacity)和使用量(Used),S0=from,S1=to
OC、OU:年老代容量和使用量
PC、PU:永久代容量和使用量
YGC、YGT:年轻代GC次数和GC耗时
FGC、FGCT:Full GC次数和Full GC耗时 GCT:GC总耗时

jhsdb(jdk9提供的工具)

btrace(shell指令)

jcmd

interview-remind

GC过程(什么时候会出现FULL GC)

  • 对象被new以后存在YG, 绝大部分对象在这里die,YG满以后触发YGC
  • YGC把在YG中存活的实例迁移至OG,OG满以后触发FGC,FGC后依然不能存储新的实例,则OOM(Exception: out of Memory)
  • PG在运行期间不进行GC,PG满了之后直接触发FGC(JDK7)
  • 代码调用Syste.gc() 触发FGC
  • metaspace满了之后
  • CMS GC 出现异常 promotion failed和concurrent mode failure (也属2情况)
    • promotation failed : FGC过程中,从YG想OG复制对象,OG空间不足(OG太小)
    • concurrent mode failure : 并行回收OG太慢,调整CMS的参数
  • (rmi调用,默认每分钟调用一次system.gc,可以通过-Dsun.rmi.dgc.server.gcInterval=3600000来设置大点的间隔,现阶段远程调用已经升级为rpc)

GC算法设置考虑的主要因素

  • 系统吞吐量(throught)要求 => 推荐(ParScavenge + ParOld)
  • 应用暂停时间:(gc在执行过程中,会暂停应用执行时间) => CMS,推荐(G1)

定位内存溢出等问题的步骤

=> 先看异常日志信息:主要定位溢出内存区间(栈/堆/metaspace)

  1. 查询进程Id的启动参数:jps -v| grep purse
  2. 查看jvm的gc状态: jstat -gc pid 250 10 #输出10次gc数据,间隔250ms, FGC,FGCT
    • a. 如果出现FGC>0, 则出现Full gc, 出现时根据异常信息分析堆、栈信息
    • b. gc日志查看
  3. 查看进程堆信息:
    • a. jmap -heap pid, 查看堆空间的使用信息(-PrintGCDetail)
    • b. jmap -histo pid, 查看堆空间中加载的class信息,个数
    • c. 使用Memory Analysis 查看不同class占用deap的比例和异常数据
  4. 查看栈信息:
    • a. 查看栈信息: jstack pid, 能统计线程个数,线程状态,通过查看线程状态判断线程空闲(空闲的线程多可以优化配置减少上下文切换),等待(io),死锁和死锁的位置
    • b. 查看进程中线程的个数: top -Hp pid #使用的shell命令:查看总线程个数,各个线程占CPU,MEM的情况
    • c. 查看线程信息: jstack pid | grep nid -C 10 # nid 是b中进程ID的小写的16进制,定位到线程()里面是代码的行数
    • d. 查看关键信息: jstack pid | grep main | mq | dubbo 等关键词,定位线程的状态和代码的行数

PS: java线程创建时会使用native方法(start)创建一个对应的系统的原生线程,两个进程拥有相同的生命周期,java线程池的调度很多native方法; jstack日志中(tid-nid的映射关系,thread-native thread).

  • Thread.currentThread().getId(): 代码中计数器的Id, 跟JVM没有关系
  • tid: jvm thread id
  • nid: native thread id

heap

  • jstat
  • gc日志

stack-dump

1. "Timer-0" daemon prio=10 tid=0xac190c00 nid=0xaef in Object.wait() [0xae77d000] 
# 线程名称:Timer-0;线程类型:daemon;优先级: 10,默认是5;
# JVM线程id:tid=0xac190c00,JVM内部线程的唯一标识(通过java.lang.Thread.getId()获取,通常用自增方式实现)。
# 对应系统线程id(NativeThread ID):nid=0xaef,和top命令查看的线程pid对应,不过一个是10进制,一个是16进制。(通过命令:top -H -p pid,可以查看该进程的所有线程信息)
# 线程状态:in Object.wait();
# 起始栈地址:[0xae77d000],对象的内存地址,通过JVM内存查看工具,能够看出线程是在哪儿个对象上等待;
2.  java.lang.Thread.State: TIMED_WAITING (on object monitor)
3.  at java.lang.Object.wait(Native Method)
4.  -waiting on <0xb3885f60> (a java.util.TaskQueue)     # 继续wait 
5.  at java.util.TimerThread.mainLoop(Timer.java:509)
6.  -locked <0xb3885f60> (a java.util.TaskQueue)         # 已经locked
7.  at java.util.TimerThread.run(Timer.java:462)
Java thread statck trace:是上面2-7行的信息。到目前为止这是最重要的数据,Java stack trace提供了大部分信息来精确定位问题根源。

CMC对比G1

javadoop.com/post/g1

G1-CMS都支持并发,

  1. 可控的停顿时间(参数配置STW时间,预估计算region,增量回收)
  2. G1(分区)没有CMS(标记清除)的碎片化问题。
-XX:MaxGCPauseMillis=200 # 最大停顿时间

G1 使用了停顿预测模型来满足用户指定的停顿时间目标,并基于目标来选择进行垃圾回收的region数量。G1 采用增量回收的方式,每次回收一些区块,而不是整堆回收。

过程:

G1将整个堆划分为一个个大小相等的小块,和分代算法一样,G1 中每个块会充当 Eden、Survivor、Old 三种角色。整个堆分位2048个region(1MB-32MB);

the collection set: the regions that contain the most garbage are collected first. Hence the name: garbage-first collection.

  1. 年轻代收集,STW
  2. 并发收集(YG+OG,类似CMS)和应用线程同时执行
  3. 混合式垃圾收集
  4. 必要时的 Full GC

GC的垃圾回收方法的原理和对比

复制算法,标记清除、标记整理、分代收集算法

GC回收器有哪些,各自的优劣势

Serial,CMS, G1 等,为啥没有一个统一的万能的垃圾回收器

新生代为啥要设置成Eden, S0,S1 这三个区,基于什么考虑

对象的生命周期,eden:s0:s1=8:1:1,大部分对象在eden被GC了。

堆外内存不受GC控制,那该怎么释放呢

对象可回收,就一定会被回收吗?

GC日志格式怎么看

主要有哪些发生OOM的场景,写代码制造场景

发生OOM,如何定位,常用的内存调试工具有哪些

如何识别垃圾

引用计数法 + 可达性算法.

堆外内存不受GC控制无法通过GC释放内存,是如何释放内存的。

对于java8,堆外内存包括Meatsapce,DirectBuffer。

key-word

TP90

GC的一个性能指标,TP90就是满足百分之九十的网络请求所需要的最低耗时(样本数据中满足某一条件的数据量/总样本数量 >= 90%)。

StopTheWorld(STW)

即在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起(Stop the world)。造成挺大的性能开销。

一般 Full GC 会导致工作线程停顿时间过长(因为Full GC 会清理整个堆中的不可用对象,一般要花较长的时间),如果在此 server 收到了很多请求,则会被拒绝服务!所以我们要尽量减少 Full GC(Minor GC 也会造成 STW,但只会触发轻微的 STW,因为 Eden 区的对象大部分都被回收了,只有极少数存活对象会通过复制算法转移到 S0 或 S1 区,所以相对还好)

SafePoint

由于 Full GC(或Minor GC) 会影响性能,所以我们要在一个合适的时间点发起GC,这个时间点被称为 Safe Point,这个时间点的选定既不能太少以让 GC 时间太长导致程序过长时间卡顿,也不能过于频繁以至于过分增大运行时的负荷。一般当线程在这个时间点上状态是可以确定的,如确定 GC Root 的信息等,可以使 JVM 开始安全地 GC。Safe Point 主要指的是以下特定位置:

  • 循环的末尾
  • 方法返回前
  • 调用方法的 call 之后
  • 抛出异常的位置

safepoint是GC发生的时机,STW是发生的结果

JMM

JMM,java内存模型(Java Memory Model); c/c++使用的操作系统内存;java为了解决平台依赖性:使用了JVM内存,即JMM;

共享内存=>多线程共享的JMM空间=>堆内存;

线程对共享内存的数据复制到栈空间,在运行开始时复制进入线程stack,结束时回写到堆空间 =>导致内存可见性的处理问题

happen-before

指令重排,发生在JVM编译器和CPU指令优化阶段;

A happen-bebore B => 操作A先执行,B后执行; AB 之间出现数据依赖时,不会出现命令重排

A=0;
B=A; // 数据依赖于前一句,指令不重排

minorGC,MajorGC,FullGC

  • minorGC:YGC
  • majorGC:OGC
  • FullGC:YGC+OGC

as-if-serial

Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义

Idea-jvm参数配置详解

Help -> VM oprions

// 1. jvm堆栈
-Xms128m
-Xmx1024m
// XX:InitialCodeCacheSize
// XX:ReservedCodeCacheSize
// codecache代码缓存,jit编译的代码,导致jit不能compiler,字节码将不再会被编译成机器码
-XX:ReservedCodeCacheSize=512m
// 2. 垃圾回收
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50

// 3. JIT即时编译
// 并发编译数
// https://www.codercto.com/a/49133.html
-XX:CICompilerCount=2
// 4. 异常保存
// XX:TraceClassLoading
// XX:TraceClassUnloading 日志打印meta加载/卸载类信息
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow

-ea
-Dsun.io.useCanonCaches=false
-Djdk.http.auth.tunneling.disabledSchemes=""
-Djdk.attach.allowAttachSelf=true
-Djdk.module.illegalAccess.silent=true
-Dkotlinx.coroutines.debug=off

// 5.日志
-XX:ErrorFile=$USER_HOME/java_error_in_datagrip_%p.log
-XX:HeapDumpPath=$USER_HOME/java_error_in_datagrip.hprof


ps