jvm的内存结构、相关的垃圾算法、分代回收算法、相关溢出的问题排查思路
jvm的内存结构:可以看到我们的java文件会首先编译成class文件,经过类加载器进行加载,然后经过jvm的相关区域:f方法区、堆、虚拟机栈、程序计数器、本地方法栈等地,可以进行本地方法接口进行调用,执行引擎,进行编译,执行程序。当中涉及到垃圾回收。
寄存器,记住下一条jvm指令的执行地址,是线程私有的,JVM中唯一不会存在内存溢出的。
每个线程只能有一个活动栈帧,通常是参数、局部变量、返回地址,对应着当前正在执行的那个方法
栈帧:每个方法运行时需要的内存。
垃圾回收设计栈内存吗?不涉及栈内存。
栈内存的分配越大越好吗?
不是的,通常是默认的,1024k,其中linux和mac电脑默认是1024k,而windows是根据系统而定。由于一个线程对应一个栈帧,如果越大,则会拖慢虚拟机的运行性能。
方法内的局部变量是否线程安全?
如果方法内的局部变量没有逃离方法的作用访问,则它是线程安全的,如果局部变量应用了对象,并逃离方法的作用范围,则需要考虑线程安全的问题。也即与变量的作用域有关。
栈内存溢出:栈帧过多、过大都会导致栈内存溢出。
栈内存溢出的排查方法:
首先采用top命令,查看当前是哪个进程cpu占用过高
接着采用ps H -eo pid,tid,%cpu |grep 进程id
用jstack 进程id排查到有问题线程,定位到问题代码的行号,从而解决问题。
主要存放操作系统的方法,存放native方法的内存调用。
通过new关键字创建的对象都会被存放在堆内存中。它是线程共享的,堆中对象都需要考虑线程安全的问题,同时有垃圾回收机制。
堆溢出问题解决:
通常会出现java.lang.OutofMemoryError:java heap space的错误信息。
首先通过jps查看到当前有哪些java进程在运行。
接着采用jmap查看堆使用情况,通常jmap只能查看某一时刻的信息。jmap -heap 进程id
jconsole工具可以以图形化界面看到相关的监控信息,做到连续监控。连接jconsole,打开其图形化界面,进行查看。
采用jvisualvm ,可以进行堆dump日志分析,排查问题。
主要存放成员方法、方法、构造方法、运行时常量池(String Table)
虚拟机启动时被创建,逻辑上是堆的一部分。jdk1.7及以前,存放在永久代中。而jdk1.8的时候存放在metaspace中。
方法区jdk1.7的时候会出现溢出,但jdk1.8之后存放在metaspace元空间中,因此通常情况下不会出现内存溢出的情况。
方法区常量池:运行时常量池StringTable
常量池就是一张类似于hash一样的表。虚拟机指令根据这张表找到执行的类名、方法名、类、字面量等信息。当类文件被加载时常量信息会加载到运行时常量池中,并把里面的符号地址变成真实地址。
串池(调用intern方法就可以放入)中如果有的常量,则不会到常量池中去找,其加载策略是惰性的。常量池中的字符串是符号,第一次用到时才是对象。同时利用串池可以避免重复创建,节省内存。字符串拼接通常是在编译期优化的。
如果项目中常量信息较多,同时又有重复,则可以考虑串池进行调优。同时如果常量信息较多,也可以调大桶的个数,由于其本身就是一张表。-XX:stringTableSize=桶个数
操作系统内存,常用于NIO操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受jvm内存回收管理。
分配与回收:使用unsafe对象完成直接内存的分配和回收,分配内存可以采用allocateMemory来进行分配内存,并且回收需要主动调用freeMemory方法。ByteBuffer的实现类内部使用了cleaner虚引用来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过clear的clean方法调用freeMemory来释放内存。
强引用、软引用、弱引用、虚引用、终结器引用
强引用:只有所有GC roots对象都不通过强引用引用该对象,该对象才能被垃圾回收。
软引用:仅有软引用该对象,在垃圾回收后,内存还不足时会再次触发垃圾回收,回收软引用对象,可以配合引用队列来释放引用自身。
弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放引用自身。
虚引用:必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会被徐引用入队,由Referencehandler线程调用虚引用相应的方法来释放直接内存。
终结器引用:无需手动编码,但器内部配合引用队列使用,在垃圾回收时,终结器引用入队,找到被引用对象并调用finalize方法,第二次GC回收的时候才能回收被引用对象。
如何判断对象可以回收?采用可达性分析算法,也即没有引用的对象,通常会进行回收,也即孤立的对象会被回收。
垃圾回收算法:
通常可分为三种:标记清除mark sweep、标记整理mark compact、复制copy。
这三种算法:标记清除算法速度快,但产生内存碎片较多,不利于内存的充分使用。标记整理,速度适中,同时能够整理内存碎片。复制算法,速度最慢,但不会产生内存碎片,占用内存双倍。
由于三种算法都有其优点和缺点,因此需要综合使用它们。因此就产生了分带垃圾回收算法。
新生代由于产生的对象是朝生夕死的,因此通常会常用复制算法,而老年代则采用标记整理或标记清除算法。
同时可分为新生代、老年代、MetaSpace区(jdk1.7永久代)
新生代又可分为Eden区、Survivor区,而Survivor区又可分为From、To两部分。
1.通常首先分配在Eden区
2.新生代空间不足时,会触发minor gc,Eden区和from存活对象使用copy算法复制到to中,存活的对象年龄+1,并且交换from、to
3.minor gc会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才回复运行
4.当对象寿命超过阀值时,会晋升到老年代,最大寿命是15(4bit)
5.当老年代空间不足时,会先尝试触发minor gc,如果空间不足,那么触发full gc,stw的时间更长
堆初始化大小:-Xms
堆最大大小:-Xmx或-XX:MaxHeapSize=size
新生代大小:-Xmn或-XX:newSize=size+-XX:MaxNewSize=size
幸存区比例(动态):-XX:InittalSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy
幸存区比例:-XX:SurvivorRatio=ratio
晋升阀值:-XX:MaxTenuringThreshold=threshold
晋升详情:-XX:+PrintTenuringDistribution
GC详情:-XX:+PrintGCDetails -verbose:gc
FullGC前MinorGC:-XX:+scavengeBeforeFullGC
可以分为串行垃圾回收器、并行垃圾回收器、并发垃圾回收器、G1等
串行垃圾回收器:单线程、堆内存较小,适合个人电脑,-XX:+userSerialGC=Serial+SerialOld,串行新生代和老年代
并行垃圾回收器:多线程,吞吐量高,-XX:+UserParallelGC —— -XX:+UseParallelOldGC,-XX:GCTimeRatio=ratio,-XX:MaxGCPauseMillis=ms,-XX:ParallelgcThreads=n,并行GC,新生代和老年代,同时可以设置比例,响应时间、线程数等信息
并发垃圾回收器:多线程,响应时间短,-XX:+UseConcMarkSweepGC—— -XX:UseParNewGC——SerialOld,-XX:ParallelGCThreads=n—— -XX:Concgcthreads,-XX:CMSInitiatingOccupancyFranction=percent,-XX:+CMSScavengeBeforeRemark,包含信息并发新生代GC,串行Old,并发线程数,CMS比例,重新标记再清理
使用场景:注重高吞吐量,低延迟的场景,默认的暂停目标是200ms。超大堆内存,会将堆划分为多个大小相等的Region,整体上标记+整理算法,两个区域之间采用复制算法。
-XX:+UseG1GC
-XX:G1HeapRegionsSize=size
-XX:MaxGCPauseMillis=time
垃圾回收阶段:
Young Collection、Mixed Collection、Young Collection+Concurrent Mark
Young Collection:会STW.
Young Collection+CM:在Young GC时会进行GC Root的初始化标记,老年代占用堆空间比例达到阀值时,进行并发标记,不会STW.
Mixed Collection:会对E、S、O进行全面垃圾回收,最终标记会STW,拷贝存活会STW
进行Full GC:相关代的FullGC
Young Collection跨代引用:新生代回收的跨代引用(老年大引用新生代)问题:卡表与Remebered Set,在引用变量时通过post-write barier+dirty card queue进行处理,同时concurrent refinement threads更新Remembered Set.
Remark:pre-write barrier + satb_mark_queue
通常调优需要考虑:内存、锁竞争、cpu占用、IO、代码这些层面上的调优。
考虑不发生GC,也即查看FullGC前后的内存占用,考虑数据是否太多、数据表示是否太大、是否存在内存泄漏(变量的作用范围、是否存在软、弱引用、第三方缓存等)。