JVM
Last updated
Last updated
1.不断的new对象
2.对象过大,超过堆大小
3.内存泄漏,导致内存无法回收
4.永久代内存溢出:Class过多
在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间
的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。
新生代
Serial 垃圾收集器(单线程、复制算法):是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
ParNew垃圾收集器(Serial+多线程):是很多 java 虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
Parallel Scavenge 收集器(多线程复制算法、高效):吞吐量。
老年代
SerialOld收集器(单线程标记整理算法):运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。
ParallelOld收集器(多线程标记整理算法)
CMS收集器(多线程标记清除算法):主要目标是获取最短垃圾回收停顿时间。会产生内存碎片。
G1收集器:可预测的停顿,减少内存碎片
CMS垃圾收集器 通过参数-XX:+UseConcMarkSweepGC进行设置。 回收过程:
初始标记(CMS-initial-mark),仅标记GC Roots能直接关联到的对象,会导致stw;
并发标记(CMS-concurrent-mark),与用户线程同时运行,进行GC Roots Tracing,从“初始标记”阶段标记的对象开始找出所有存活的对象;
重新标记(CMS-remark),为了修正并发标记期间因程序继续运行而导致标记产生变动的那一部分对象的标记记录,会导致stw;
并发清除(CMS-concurrent-sweep),与用户线程同时运行;
G1垃圾收集器 TODO
G1为什么可预测停顿时间
G1有一个步骤是让使用者自行设置暂停应用的时间,因为G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,其它GC每次回收都会回收整个内存(Eden, Old),回收内存所需的时间取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点相反就多回收点。G1垃圾收集器的G1其实是Garbage First的意思,这里并不是垃圾优先的意思,而是优先处理那些垃圾多的内存块的意思。
CMS 与 G1 区别
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
CMS收集器以小的停顿时间为目标的收集器;G1收集器可预测垃圾回收的停顿时间;
CMS收集器是使用“标记-清除”算 法进行的垃圾回收,容易产生内存碎片G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片;
CMS是整代一起收集;G1不是整代收集,而是收集那些垃圾多的内存块。
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
无法处理循环引用。
根搜索法
设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可以被回收的。
从根(GC Roots)的对象作为起始点,开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连(用图论的概念来讲,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
复制算法(基于根搜索法)
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
缺点:空间的浪费
现代虚拟机将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。
标记-清除算法(基于根搜索法)
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。
缺点:
效率比较低(递归与全堆对象遍历)
空闲内存是不连续的
标记-整理算法(基于根搜索法)
和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记;但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间。
标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即native方法)引用的对象
Young GC 触发条件
对象优先在新生代Eden区中分配,如果Eden区没有足够的空间时,就会触发一次young gc
Full GC 触发条件
在执行Young gc之前,JVM会进行空间分配担保——如果老年代的连续空间小于新生代对象的总大小(或历次晋升的平均大小),则触发一次full gc
显式调用System.gc()方法时(此方法的调用是建议JVM进行Full GC)
大对象直接进入老年代,从年轻代晋升上来的老对象,尝试在老年代分配内存时,但是老年代内存空间不够
方法区空间不足,如加载的类、反射的类和调用的方法较多时可能造成方法区空间不足
1.启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
2.扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs 系统变量指定路径中的类库。
3.应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。
FQA: 为什么不能合并成一个类加载器?
每一个类加载器都是为了去在不同的情景下去加载类。加载类的方法有无数种,需要一个灵活的加载器系统去在特定的情况下来加载类,如从本地系统、网络、源文件、jar包中加载类。
类加载器可以在 load class 时对 class 进行重写和覆盖,在此期间就可以对类进行功能性的增强。
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化。
1.加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
2.验证
确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。有文件格式验证、元数据验证(语义分析,类与类的继承关系等)、字节码验证(数据流和控制流分析)、符号引用验证(对类自身以外的信息进行匹配校验)。
3.准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
4.解析
虚拟机将常量池中的符号引用替换为直接引用的过程。
5.初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
TODO
类卸载条件
该类所有的实例都已经被GC。
加载该类的ClassLoader实例已经被GC。
该类的java.lang.Class对象没有在任何地方被引用。
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
意义:
避免重复加载
保证java核心库安全的性
1.类延迟加载导致代码部署后未立刻执行
static代码块延迟加载导致 静态变量configs 没有在第一时间初始化,等到所在的类被引用时才会初始化并启动定时任务。
jmap,jstack,jstat,jinfo,jps TODO
可能原因
程序执行了System.gc()
执行了jmap -histo:live pid命令,这个会立即触发fullgc
在执行Minor GC的时候进行的一系列检查 a.执行Minor GC的时候,JVM会检查老年代中最大连续可用空间是否大于了当前新生代所有对象的总大小 b.如果大于,则直接执行Minor GC(这个时候执行是没有风险的) c.如果小于了,JVM会检查是否开启了空间分配担保机制,如果没有开启则直接改为执行Full GC d.如果开启了,则JVM会检查老年代中最大连续可用空间是否大于了历次晋升到老年代中的平均大小,如果小于则执行改为执行Full GC e.如果大于则会执行Minor GC,如果Minor GC执行失败则会执行Full GC
使用了大对象,大对象会直接进入老年代
在程序中长期持有了对象的引用,对象年龄达到指定阈值也会进入老年代
程序中存在内存泄露,导致老年代内存缓慢增长,且无法被回收,达到了GC阈值
排查步骤
通过JVM参数获取dump文件(-XX:HeapDumpBeforeFullGC)
通过JDK自带的工具jmap获取dump文件
通过JDK自带的jvisualvm工具分析或者下载三方软件jprofiler来分析dump文件即可
最重要的一点,要把fullgc发生时刻的dump文件和正常没有发生fullgc时间的dump文件都下载到本地,然后对比观察分析方便找到问题的原因
1.jcmd {pid} VM.flags
2.jinfo -flags {pid}
或 jmap -heap {pid}
3.jps -v
或 ps -ef | grep java
-XX:+HeapDumpOnOutOfMemoryError
当堆内存OOM时自动dump堆内存
通过JMX的MBean生成当前的堆栈信息(可用于JVM监控)
jmap -dump:live,format=b,file=heap.hprof {pid}
或 jcmd {pid} GC.heap_dump {file-path}
例如,jcmd 2523 GC.heap_dump ~/Desktop/heap.hprof
jstack {pid} > {file-path}
或 jcmd {pid} Thread.print > {file-path}
例如,jstack 2523 > ~/Desktop/threaddump.txt
堆dump文件 -> MAT, VisualVM, IDEA Profiler
栈dump文件 -> https://gceasy.io/
-XX:MetaspaceSize=128m (元空间默认大小)
-XX:MaxMetaspaceSize=128m (元空间最大大小)
-Xms1024m (堆默认大小)
-Xmx1024m (堆最大大小)
-Xmn256m (年轻代大小,建议不要太大,太大可能会是YGC时间过长,影响吞吐量)
-XX:MaxDirectMemorySize=64m (堆外内存大小)
-Xss256k (栈最大深度大小)
-XX:+UseConcMarkSweepGC (指定使用的垃圾收集器,这里使用CMS收集器)
-XX:+UseParNewGC (年轻代收集器)
-XX:+PrintGCDetails (打印详细的GC日志)
-XX:+PrintGCTimeStamps (打印gc时间戳)
-Xloggc:/appl/gc-%t.log (定义gc日志目录,建议加时间而不是-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=100)
-XX:+HeapDumpOnOutOfMemoryError (当JVM发生OOM时,自动生成DUMP文件)
-XX:HeapDumpPath=./logs (生成DUMP文件的路径)