干货-深入理解Java虚拟机-1-虚拟机内存分配与垃圾收集器
前言
基础不牢,地动山摇,菜如老哥还经常巩固自己的基本功,你就更要努力学习了。
最近博主在复习Java虚拟机,对Java虚拟机的理解又有了一个更深层次的理解,记录下一些笔记及重点摘要,让我们一起学习一下吧!
现在用不上,不代表以后就能用上,一句话,学,就行了。
学习JVM有什么意义和作用?
1、学习JVM能更深入的理解Java这门语言,能理解Java语言底层的执行过程。
2、学习JVM,为了项目上线后去排查一些程序log日志中无法呈现的问题,可以通过GC日志来排查项目问题以及进行调优。
3、能够利用一些工具,jmap, jvisualvm, jstat, jconsole等工具可以辅助你观察Java应用在运行时堆的布局情况,由此你可以通过调整JVM相关参数提高Java应用的性能。
4、学习之前面试官虐待你你会觉得很痛苦,学完之后再被虐待时你会觉得很享受。
链接附上:《干货-深入理解Java虚拟机》
正文
1. Java虚拟机的内存结构
2.内存溢出
Java堆溢出:
异常信息: java.long.OutOfMemoryError,跟着会进一步提示Java heap space。
解决方法: 遇到堆内存溢出,首先需要判断是不是内存泄露,也就是存在大量的对象在引用链中存在导致垃圾回收器无法回收,这时需要找到导致内存泄漏的代码进行修改;
如果不是内存泄漏,换句话说就是确实所有的对象都还活着,也就是确实出现了内存溢出,这时需要检查是不是某些对象生命周期过长、持有时间过长导致,尝试减少运行时间所使用的内存,如果还是不行,就需要通过-Xmx和-Xms参数来调整虚拟机参数,调大内存。
虚拟机栈和本地方法栈溢出:
对于StackOverflowError异常,在单线程时常常会产生,产生的原因通常是因为方法的无限循环调用或者无限递归,导致栈帧过大溢出,这种需要检查代码逻辑,还有一种可能时代码逻辑正确,但是方法栈不够用,这时需要-Xss参数来设置栈容量大小。
对于OutOfMemoryError异常,通常由于过多的创建线程导致,这种情况下如果不能减少线程数时我们只能通过减小最大堆和减小栈容量来换取更多的线程,如果没有接触过,通过这种“减少内存”的方式来解决内存溢出的方法会比较难以想到。
这里解释一下为什么这样可以解决,因为操作系统给每个进程分配的内存是有限制的,虚拟机提供的参数来控制最大堆内存和方法区的最大内存,用进程的内存减去这两个参数设定的内存,再减去虚拟机进程消耗的内存,剩下的就由本地方法栈和虚拟机栈瓜分了,前面我们知道,这两块都是线程私有的内存,如果每个线程瓜分到的内存越大,那自然我们能建立的线程就会变少,建立线程时就更容易耗尽内存。
3. Java对象的四种引用状态
强引用: 直接被引用的对象,当虚拟机内存不足时,抛出异常也不会回收这些对象。
Object object = new Object();
String str = "StrongReference";
软引用: 有用但不是必须的对象,当内存不足时会被当做辣鸡回收,下面代码是示例,obj指向了null就会被JVM回收,但是如果内存够用,我们通过sr.get()能获取到对象,如果内存不足,他就会被回收结果为null。
Obj obj = new Obj();
SoftReference<Obj> sr = new SoftReference<Obj>(obj);
obj = null;
System.out.println(sr.get());
弱引用: 在垃圾回收时不管内存够不够用都会被回收,但是垃圾回收器的线程优先级较低,因此不一定很快发现弱引用对象并回收。
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
虚引用: 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
4. Java虚拟机垃圾回收机制
如何判断对象成为垃圾?
传统的算法是引用计数法,就是一个对象被引用就+1,引用失效就-1,任何引用计数是0的对象都是可回收的垃圾,这种算法简单、效率高,但是有一个致命问题就是无法解决循环引用,就是a指向b,b指向a,但是他们两个都没有再其他任何地方被使用,就不会被视为垃圾。
Java虚拟机采用的是可达性分析算法,算法的思路就是通过一系列被称为“GC Roots”的对象作为起点,从这些节点开始构建出引用链,如果一个对象没有任何一条链路可以到达“GC Roots”节点,那么认为对象已经成为垃圾,可以回收,这样就解决了循环引用得问题。
如何判定是否回收?
在Java中,判定对象回收(对象死亡)要经历两次标记,第一次标记是对象无引用链路可达“GC Roots”(死缓),第二次判断是此对象是否有必要调用finalize()方法(宣布死刑),如果对象没有覆盖重新finallize()方法或者finallize()已经被调用过一次,那么直接判定死亡,否则会调用finallize()方法,执行完成后再判定死亡,所有死缓对象可以在这个方法中实现最后一次自救,例如将自己赋值给一个全局的强引用,但在下次回收时如果还是死缓则会被直接回收,原因是finallize()方法只会被执行一次。
通过finallize()执行的功能大多数都可以通过try{}finally{}的方式执行,而且finallize()方法有可能造成垃圾回收器崩溃,所以基本不使用。
JVM垃圾回收的方法原理?
JVM垃圾回收主要对堆内存(存放对象)进行扫描回收,为了提高回收效率并且不影响系统性能,堆内存管理被分为新生代和老年代,采用不同的回收算法。
新生代: 新创建的对象一般放在新生代内存区域,极少部分会直接放在老年代,新生代内存区域被划分为三块:
这里简单说明一下:新创建的对象会被放在Eden(新生伊甸园)和一块Survivor(存活者)区域中,当进行一次垃圾回收时,将内存中的垃圾对象进行回收,这时内存区域会出现不规则的间断,就是不是一块完整的存储区域,不利于我们的存储,所以我们把没有被清除的对象复制到另一块Survivir区域中,修改下对象的引用地址,再把之前Eden和Survivor内存区域清除,这样就完成了一次GC,熬过一次GC的对象存活年龄会被+1,当到达某一年龄(默认15,可配)时会被移入老年代。
新生代使用的这种回收算法叫做复制算法,Java中叫做Minor GC,因为新生对象90%以上都是要被回收的,而复制算法的优势是可以保证内存区域块的完整性,但是需要修改引用占用一定资源,这正好符合新生代所需。
老年代: 老年代对象大多数是新生代熬过来的,也有少部分占用内存很大(需要直接分配一块完整内存)的对象会被直接放入老年代,老年代垃圾回收使用的是Full GC,采用标记-清除算法,这种算法会直接将垃圾进行回收。Full GC操作没有Minor GC频繁,且进行一次消耗的时间会比较长。
5. 虚拟机收集器
Serial 收集器
标记-复制。
单线程,一个CPU或一条收集线程去完成垃圾收集工作,收集时必须暂停其他所有的工作线程,直到它结束。
虽然如此,它依然是虚拟机运行在Client模式下的默认新生代收集器。简单而高效。
ParNew 收集器
ParNew是Serial收集器的多线程版本。Server模式下默认新生代收集器,除了Serial收集器之外,只有它能与CMS收集器配合工作。
Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器。看上去来ParNew一样,有什么特别?
Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS等收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间。而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间和CPU总小号时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间),虚拟机总共运行了100min,其中垃圾收集花费了1min,那吞吐量就是99%.
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用CPU时间,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis以及直接设置吞吐量大小的-XX:GCTimeRatio。
CMS 收集器
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。 目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上 ,这类尤其重视服务的响应速度,希望系统停顿时间最短。CMS收集器就非常符合这类应用的需求。
CMS基于 标记-清除算法实现。整个过程分为4个步骤:
1. 初始标记(CMS initial mark) -stop the world
重新标记这两个步骤仍然需要Stop The World, 初始标记仅仅标记以下GC Roots能直接关联的对象,速度很快。
2. 并发标记(CMS concurrent mark)
并发标记就是进行GC Roots Tracing(对象调用链路搜索,看是否能从根节点找到调用链)的过程;
3. 重新标记(CMS remark) -stop the world
而重新标记阶段则是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段停顿比初始标记稍微长,但远比并发标记的时间短。
4. 并发清除(CMS concurrent sweep)
整个过程耗时最长的并发标记和并发清除过程,收集器都可以与用户线程一起工作。总体上来说,CMS收集器的内存回收过程与用户线程一起并发执行的。
CMS特点:并发收集,低停顿。
缺点
1.CMS收集器对CPU资源非常敏感。默认启动的回收线程数是(CPU+3)/4. 当CPU 4个以上时,并发回收垃圾收集线程不少于25%的CPU资源。
2. CMS收集器无法处理浮动垃圾(Floating Garbage),如果CMS运行期间预留的内存无法满足程序需求,就会出现”Concurrent Mode Failure“失败,这是虚拟机将启动后备预案:临时弃用Serial Old收集器来重新进行老年代的回收,就相当于会再进行一次Full GC。由于CMS并发清理时,用户线程还在运行,伴随产生新垃圾,而这一部分出现在标记之后,只能下次GC时再清理。这一部分垃圾就称为”浮动垃圾“。
3. CMS基于”标记-清除“算法实现的,则会产生大量空间碎片,这会给大对象的分配带来很多麻烦,如果不能够找到一块完整的内存存储大对象,就不得不进行一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCmsCompactAtFullCollection开关参数(默认就是开启的),用于CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,但这会引起停顿时间加长。虚拟机设计者还提供了一个-XX:CMSFullGCSBeforeCompaction(默认值是0,表示每次进入Full GC时都进行碎片整理),这个参数用于设置多少次不压缩的Full GC后,就来一次带压缩(即内存整理)的。
面试问题:CMS一共会有几次STW(Stop the world)
首先,回答两次,初始标记和重新标记需要。
然后,CMS并发的代价是预留空间给用户,预留不足的时候触发FUllGC,这时Serail Old会STW.
然后,CMS是标记-清除算法,导致空间碎片,则没有连续空间分配大对象时,FUllGC, 而FUllGC会开始碎片整理, STW.
即2次或多次。
G1分代收集器(JDK1.9默认采用,被Oracle官方大力推行)
oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。
开发人员仅仅需要声明以下参数即可:
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。
G1将新生代,老年代的物理空间划分取消了。
G1将新生代,老年代的物理空间划分取消了。
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1提供了两种GC模式,Young GC和Mixed GC
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
Young GC 阶段:
阶段1:根扫描
静态和本地对象被扫描
阶段2:更新RS
处理dirty card队列更新RS
阶段3:处理RS
检测从年轻代指向年老代的对象
阶段4:对象拷贝
拷贝存活的对象到survivor/old区域
阶段5:处理引用队列
软引用,弱引用,虚引用处理
G1 Mix GC
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
它的GC步骤分2步:
1.全局并发标记(global concurrent marking)
2.拷贝存活对象(evacuation)
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?
在G1 GC中,全局并发标记主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking(全局并发标记)的执行过程分为五个步骤:
初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
根区域扫描(root region scan
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
6.垃圾收集器的参数
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+Serial Old的收集器组合进行回收 |
UseParNewGC | 打开此开关后,使用ParNew+Serial Old的收集器组合进行回收 |
UseConcMarkSweepGC | 打开此开关后,使用ParNew+CMS+Serial Old的收集器组合进行回收,Seroal Old作为CMS收集器出现Conurrent Mode Failure失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge+Serial Old(PS MarkSweep)的收集器组合进行内存回收 |
UseParallelOldGC | 打开此开关后,使用Parallel Scavenge+Parallel Old(PS MarkSweep)的收集器组合进行内存回收 |
SurvivorRatio | 新生代中Eden区(一块)和Surivor区域(两块)的容量比值,默认为8,代表Eden:Survivor=8:1 |
PretenureSizeThreshold | 晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接分配在老年代 |
MaxTenuringThreshold | 晋升到老年代的对象年龄,每个对象坚持过一次Minor GC之后,年龄就会增加1,超过这个参数值就会进入老年代 |
UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的挣个Eden和Survivor区域的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认值为99,即允许1%的GC时间,尽在使用Parallel Scavenge收集器时有效 |
MaxGCPauseMillis | 设置GC的在最大停顿时间,仅在使用Parallel Scavenge 收集器时生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值68%,仅在使用CMS收集器时有效 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否需要进行一次内存碎片整理,仅在使用CMS收集器时有效 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾手机后在启动一次内存碎片整理。仅在CMS收集器时有效 |
以上就是博主本次关于虚拟机的学习分享,谢谢大家观看,记得一键三连哦!
另外,附上学习书籍:《深入理解Java虚拟机》,需要某宝可以进行购买
你只有通过自身不懈的努力,才能受到社会不断的毒打,干就完了!
- 点赞
- 收藏
- 关注作者
评论(0)