1.java运行时数据区
1.1程序计数器(线程独有)
类似于计算机中的寄存器,记录当前线程执行的指令和行号。如我们所知,线程和进行都是cpu执行的时间片段,只是粒度大小不一样而已。所有在线程进行切换的过程中,需要保存线程进入等待时的指令行号,这样下一次才能找到从哪个位置继续执行。
1.2 虚拟机栈(线程独有)
当一个线程的栈深度大于虚拟机所允许的深度的时候,将会抛出StackOverflowError异常;如果当创建一个新的线程时无法申请到足够的内存,则会抛出OutOfMemeryError异常。
1.2.1栈帧结构
在执行方法时,会创建一个栈帧(一个方法对应一个栈帧),并压入虚拟机栈当中。
1.2.2局部变量表和操作数栈
javap -c Test.class > p.txt 反汇编字节码文件后,得到操作指令
上述方法执行的指令含义如下:
本地变量表: this, i, j 。顺序为0,1,2
1.加载本地变量表变量1的值,压入操作数栈中。
2.加载本地变量表变量2的值,压入操作数栈中。
3.压入操作指令iadd,计算出结果并将之前的弹出栈外
4.返回int类型的值
更多汇编指令参考:http://bbs.gupaoedu.com/forum.php?mod=viewthread&tid=295&highlight=javap
1.2.3动态链接
多态情况下,方法动态绑定时,需要用到动态链接
1.2.4方法出口
当一个方法开始执行以后,只有两种方法可以退出当前方法:
当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
当方法返回时,可能进行3个操作:
恢复上层方法的局部变量表和操作数栈
把返回值压入调用者调用者栈帧的操作数栈
调整 PC 计数器的值以指向方法调用指令后面的一条指令
1.3本地方法栈(线程独有)
调用本地方法时使用
1.4方法区
1.5heap
2.java内存模型(JMM, java memory model)
2.1内存结构
问题1:新生代的垃圾回收算法为什么用复制算法?
因为新生代的对象90%都是朝生夕死,用标记清除法容易造成内存碎片,而标记整理法代价太高
所以采用复制算法。
问题2:为什么新生代不直接设置两个区,比例为1:1,或者9:1
如果是1:1的,空间利用率只有50%。如果是9:1的话,大部分对象的分代年龄只能达到1,也就是经过一次minorGC就会进入到老年代中。
问题3:怎么控制survivor的比例和新老年代的内存比例?
-XX:SurvivorRatio=8
-XX:NewRatio=2
更多JVM参数请参考:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
2.2对象生命周期
2.2.1类加载检查
在对象创建之前,需要先判断类是否已经被加载,判断方式在当前类加载器中寻找该类的类信息,如果没有找到,则是未加载,因此多个类加载器可以加载同一个类。
2.2.2为对象分配内存
问题1:如何知道哪块是可用内存?
如果采用的垃圾收集器是Serial、ParNew等带Compact过程的收集器的时候(新生代创建对象,复制对象),内存是规整的,采用移动指针的方式进行分配。
如果采用的垃圾回收器是CMS这种采用标记清除法的时候(新生代对象移动到老年代),内存是不规整的,采用空闲列表的方式记录可用的内存区域。
问题2:多个线程同时创建对象的话,会出现并发问题,如何解决?
- 指针碰撞 (使用CAS来同步)
- TLAB thread local allocation buffer,为每个线程分配一个缓冲区。-XX:+UseTLAB打开此策略。
2.3初始化内存空间
内存分配完成之后,虚拟机会将分配空间内都初始化为零值(不包括对象头),如果使用TLAB分配,这一过程也可以提前至TLAB分配时进行。
2.4设置对象头
包括对象的哈希码、类元素信息、GC分代年龄等。这些信息都放置在对象头中。
2.5执行<init>方法
2.6对象回收
问题1:什么样的对象需要被回收?
无用的对象,即外部引用指向的对象。
问题2:怎么判断该对象已无引用?
- 引用计数法,无法解决循环引用的问题
- 可达性分析 GCroot不可达
问题3:什么对象可以成为GCRoot?
局部变量表中引用的对象,静态变量引用的对象,常量引用。
问题4:强引用,软引用,弱引用,虚引用的对象回收策略。
- 强引用
Object obj = new Object(); // 这个obj就是对象的强引用,只要有强引用存在,就不会被回收。
- 软引用
Object obj = new Object(); // obj是强引用
SoftReference<Object> sf = new SoftReference<Object>(obj); // sf是软引用
obj = null; // 强引用置空
sf.get();//有时候会返回null //垃圾回收时,如果内存空间不足,会回收软引用的对象。
- 弱引用
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//弱应用对象只能存活到下一次垃圾回收
- 虚引用
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
虚引用主要用于检测对象是否已经从内存中删除
各类引用使用场景请参考:https://www.cnblogs.com/yw-ah/p/5830458.html
2.7 对象死亡拯救
3.垃圾回收
3.1垃圾回收算法
-
标记清除算法
最基础的算法便是标记-清除算法(Mark-Sweep)。算法分为“标记”和“清除”两个阶段:首先标记处需要收集的对象,在标记完成之后,再统一回收所有被标记的对象。
这是最简单的一种算法,但是缺点也是很明显的:一个是效率问题,标记和清除效率都不高。二是空间问题,清除之后会产生大量的空间碎片,导致之后分配大对象找不到足够的连续对象而不得不触发另一次垃圾收集动作。
-
复制算法
复制算法(Copying)将可用内存按照容量大小分成相等的两份,每次只使用一半。当这一块内存用完了,就会将还存活的对象复制到另一块内存上,然后将之前的那块内存清空。
优点是解决了空间碎片的问题,而且分配新对象的时候顺序分配,实现简单,运行高效。缺点是内存减小了一半。
-
标记整理法
复制算法在对象存活率较高的情况下,效率会变低。而且浪费了50%的空间。
根据老年代的特点,有人提出了另外一种“标记-整理”算法(Mark-Compact)。算法的也分为标记和整理两个阶段。标记和“标记-清除”算法的标记过程一样。当标记完成之后,并不直接对可回收对象进行整理,而是所有存活的对象整理成连续的,然后清理掉剩余的空间。
3.2 分代垃圾回收和垃圾收集器
- youngGC
当eden区空间不足时触发,采用回收算法是复制。在youngGC之前,会进行一次分配担保。
进行youngGC时,会在from和to区来回复制,每次youngGC,对象的年龄就会加1。当新生代空间不足时,或对象达到一定年龄后会进入老年代。
问题1:分配担保机制:
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
问题2:什么对象会进入老年代?
对象很大 -XX:PretenureSizeThreshold=3145728 3M
长期存活的对象 -XX:MaxTenuringThreshold=15
动态年龄判断 相同年龄所有对象的大小总和 > Suvivor空间的一般,将会重新设置晋升年龄阈值 - oldGC
老年代回收发生在剩余内存无法装载新生代存活的对象的时候和无法装载大对象的时候(大对象直接进入老年代。CMS的回收算法是标记清除法,其余如Serial Old, Parallel为标记整理算法。 - fullGC
触发机制:
① 永久代空间不足
② 分配担保检查不符合minorGC的条件
回收整个heap和metaspace(1.8之前叫做永久代),此过程十分缓慢
垃圾收集器
- Serial
特点:单线程,会造成用户停顿。
VM参数:
-XX:+UserSerialGC 在新生代和老年代使用串行收集器
-XX:+SurvivorRatio 设置eden和survivor区大小的比例
-XX:+PretenureSizeThreshold 直接晋升到年老代的对象大小,设置此参数后,超过该大小的对象直接在年老代中分配内存
-XX:+MaxTenuringThreshold 直接晋升到年老代的对象年龄,每个对象在一次Minor GC之后还存活,则年龄加1,当年龄超过该值时进入年老代 - ParNew
特点:serial的多线程版本
VM参数:
-XX:+UseParNewGC - Parallel Scavenge
特点:关注吞吐量,而不是用户停顿
VM参数:
-XX:+UseParNewGC 打开此开关参数后,使用ParNew+Serial Old收集器组合进行垃圾收集。
-XX:+UseParallelOldGC 打开此开关参数后,使用Parallel Scavenge+Parallel Old收集器组合进行垃圾收集。
-XX:+ParallelGCThreads 设置并行GC时进行内存回收的线程数。
-XX:+MaxGCPauseMillis Parallel Scavenge收集器最大GC停顿时间。
-XX:+GCTimeRation Parallel Scavenge收集器运行时间占总时间比率。
-XX:+UseAdaptiveSizePolicy java虚拟机动态自适应策略,动态调整年老代对象年龄和各个区域大小。 - Serial Old
特点:标记整理算法,单线程,CMS备用方案 - Parallel Old
特点:多线程,标记整理 -
CMS
特点: 多线程,标记清除,容易出发FullGC
CMS执行过程:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。主要用于互联网或B/S系统的服务端,这类应用尤其重视服务的响应速度。
从名字可以看出,CMS是基于“标记-清除”算法的,运作过程更加复杂一些,分为4个步骤:
①.初始标记(CMS initial mark) 标记GC Roots直接关联的对象
②.并发标记(CMS concurrenr mark) 可达性分析算法
③.重新标记(CMS remark) 并发变动修改
④.并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,但比并发标记阶段要短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。执行过程如下图。
CMS收集器的优点:并发收集、低停顿
CMS收集器的缺点:
①CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。
②由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
③CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。 - G1
特点:内存布局改变,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器的运作大致可划分为以下几个步骤:
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
3.java监控
3.1 查看GC类型和GC日志
-
查看GC类型
-XX:+PrintFlagsFinal -XX:+PrintCommandLineFlags
GC收集器默认类型是 Parallel Scanvage + Parallel Old
-
查看GC日志
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/administrator/james/gc.log-XX:+PrintHeapAtGC
3.2 jmap
查看内存分配情况以及设置
3.3 jstat
查看内存使用情况以及gc次数
查看更多信息请参考:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jstat.html
3.4jstack (thread dump)
jstack 4170
3.5 jvisualvm & jsoncole
4.常见问题排查
4.1cpu占用过高
https://jingyan.baidu.com/article/4f34706e3ec075e387b56df2.html
4.2 内存泄漏排查
GC回收不掉