张伦聪的技术博客 Research And Development

读《深入理解java虚拟机》后感

2018-05-06

第二章 java内存区域与内存溢出异常

java虚拟机运行时数据区

程序计数器。

线程私有,可以看作是当前线程所执行的字节码的行号指示器。

java虚拟机栈。

线程私有,描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧StackFrame用于存储局部变量表等信息,程序计算器就是栈帧地址的指针。如果线程请求的栈深度大于虚拟机所允许的深度将抛出StackOverflowError异常;如果拓展时无法申请到足够的内存就会抛出OutOfMemoryError异常。

本地方法栈。

线程私有,为虚拟机使用到的native方法服务

java堆。

线程共享,存放对象实例,细分还能分新生代和老生代,继续细分还能分eden,from survivor,to survivor

方法区。

线程共享,存放已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。运行时常量池在次区。

第三章 垃圾收集器与内存分配策略

判断对象是否存活方法

引用计数算法。

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效,减1;任何时刻计数器为0的对象就是不可能再被使用的。缺点:它很难解决对象之间互相循环引用的问题。

可达性分析。

通过一系列的gc roots的对象作为起始点,从这些节点向下搜索,路径称为引用链,当一个对象到gc roots没有任何引用链相连,就称为对象不可达。

引用的几种类型

强引用。

类似Object obj=new Object()这类引用。

软引用。

在将要发生内存溢出异常之前,将会把这些对象列进回收范围,进行第二次回收。

弱引用。

将在下一次垃圾收集被回收掉。

虚引用。

在这个对象被收集器回收时收到一个系统通知。

垃圾收集算法

标记-清除算法。

缺点:1.效率不高2.产生很多内存碎片

复制算法。

缺点:真实可用内存变小了。很多商业公司是这样处理的,因为大部分新生代对象都是‘朝生夕死’,所以不需要1:1的比例来划分内存,通常eden:from survivor:to survivor=8:1:1,当回收时,将eden和from survivor中还存活对象一次性复制到to survivor,这样就把可以内存大大扩大了。

标记-整理算法。

缺点:整理效率有所下降

分代收集。

只是整合前面几种算法中优点

垃圾收集器

并行(Parallel):指多条垃圾收集器线程并行工作,但此时用户线程仍然处于等待。

并发(Concurrent):指用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集器程序运行于另一个CPU之上。

Serial收集器。

串行GC ,Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,它不仅只会使用一个CPU或者一条收集线程去完成垃圾收集作,而且必须暂停其他所有的工作线程(用户线程),直到它收集完成。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,简单高效,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,因此是运行在Client模式下的虚拟机的不错选择(比如桌面应用场景)。

ParNew收集器。

并行GC,ParNew收集器其实就是serial收集器的多线程版本,使用复制算法。除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。是运行在Service模式下虚拟机中首选的新生代收集器,其中一个与性能无关的原因就是除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作。PreNew收集器在单CPU环境中绝对没有Serial的效果好,由于存在线程交互的开销,该收集器在超线程技术实现的双CPU中都不能一定超过Serial收集器。默认开启的垃圾收集器线程数就是CPU数量,可通过-XX:parallelGCThreads参数来限制收集器线程数

Parallel Scavenge收集器。

并行回收GC,Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。短停顿时间适合和用户交互的程序,体验好。高吞吐量适合高效利用CPU,主要用于后台运算不需要太多交互。提供了两个参数来精确控制吞吐量:1.最大垃圾收集器停顿时间(-XX:MaxGCPauseMillis 大于0的毫秒数,停顿时间小了就要牺牲相应的吞吐量和新生代空间),2.设置吞吐量大小(-XX:GCTimeRatio 大于0小于100的整数,默认99,也就是允许最大1%的垃圾回收时间)。还有一个参数表示自适应调节策略(GC Ergonomics)(-XX:UseAdaptiveSizePolicy)。就不用手动设置新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)今生老年代对象大小(-XX:PretenureSizeThreshold),会根据当前系统的运行情况手机监控信息,动态调整停顿时间和吞吐量大小。也是其与PreNew收集器的一个重要区别,也是其无法与CMS收集器搭配使用的原因(CMS收集器尽可能地缩短垃圾收集时用户线程的停顿时间,以提升交互体验)。

Serial Old收集器。

串行GC,Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。如果在Service模式下使用:1.一种是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,因为那时还没有Parallel Old老年代收集器搭配;2.另一种就是作为CMS收集器的后备预案,在并发收集发生Concurrent Model Failure时使用。

Parallel Old收集器。

并行GC,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,JDK1.6才提供。由于之前有一个Parallel Scavenge新生代收集器,,但是却无老年代收集器与之完美结合,只能采用Serial Old老年代收集器,但是由于Serial Old收集器在服务端应用性能上低下(毕竟单线程,多CPU浪费了),其吞吐量反而不一定有ParNew+CMS组合。

CMS收集器。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是HotSpot虚拟机中的一款真正意义上的并发收集器,第一次实现了让垃圾回收线程和用户线程(基本上)同时工作。用CMS收集老年代的时候,新生代只能选择Serial或者ParNew收集器。

CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

①.初始标记(CMS initial mark)

②.并发标记(CMS concurrenr mark)

③.重新标记(CMS remark)

④.并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程(Stop The World)。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点:   1.CMS收集器对CPU资源非常敏感。在并发(并发标记、并发清除)阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。收集器线程所占用的CPU数量为:(CPU+3)/4=0.25+3/(4*CPU)。因此这时垃圾收集器始终不会占用少于25%的CPU,因此当进行并发阶段时,虽然用户线程可以跑,但是很缓慢,特别是双核CPU的时候,已经占用了5/8的CPU,吞吐量会很低。为了解决这种情况,产生了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)。就是采用抢占方式来模拟多任务机制,就是在并发(并发标记、并发清除)阶段,让GC线程、用户线程交替执行,尽量减少GC线程独占CPU,这样垃圾收集过程更长,但是对用户程序影响小一些。实际上i-CMS效果很一般,目前已经被声明为“deprecated”。

  2.CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以降低内存回收次数提高性能。JDK1.6中,CMS收集器的启动阈值已经提升到92%。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。

  3.最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个内存碎片的合并整理过程,但是内存整理过程是无法并发的,因此解决了空间碎片问题,却使停顿时间变长。还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程(默认值是0,表示每次进入Full GC时都进行碎片整理)。

G1收集器。

新生代和老生代收集器。将堆内存分成很多region,局部region使用复制算法回收,整体使用标记整理算法回收region存活的老生代。每个刚好弥补cms缺点,1.并行与并发,能充分利用多cpu,多核环境下多硬优势2.筛选回收,停止用户线程,停顿时间模型预测,及时清理死亡对象,不产生浮动垃圾。3.标记-整理算法,不会产生内存碎片

其他

双亲委派模型,如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

  • 启动类加载器:它的作用是将JAVA_HOME/lib目录下的类加载到内存中。需要注意的是由于启动类加载器涉及到虚拟机本地的实现细节,开发人员将无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  • 扩展类加载器:它是由Sun的ExtClassLoader实现的,它的作用是将JAVA_HOME/lib/ext目录下或由系统变量 java.ext.dir指定位置中的类加载到内存中,它可以由开发人员直接使用。

  • 应用程序类加载器:它是由Sun的AppClassLoader实现的,它的作用是将classpath路径下指定的类加载到内存中。它也可以由开发人员使用。

  • 自定义类加载器:自定义的类加载器继承自ClassLoader,并覆盖findClass方法,它的作用是将特殊用途的类加载到内存中。


如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏博主

上一篇 34. 搜索范围

下一篇 分布式锁

留言