JVM(一)

2018-05-31 10:48:27

摘录于:《深入理解JVM&G1 GC》

 JVM基本概念

判断一个对象是否存活的标准为是否存在指向这个对象的引用。Java中提供了4个级别的引用,即强引用,软引用,弱引用,虚引用。这个四个级别中只有强引用类是包内可见的,其他3个引用类型均为public,可以在应用程序中直接使用,垃圾回收器尝试回收只有弱引用的对象。

  • 强引用:在一个线程中,无需引用直接可以使用的对象,除非引用不存在了,否则强引用不会被GC清理。我们平时声明变量使用的就是强引用,普通系统99%以上都是强引用,比如,String s = "Hello World"。
  • 软引用:JVM抛出OOM之前,GC清理所有的软引用对象。垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列。类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。
  • 弱引用:弱引用对象和软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易,更快被GC回收。虽然,GC在运行时一定会回收弱引用对象,但是复杂关系的弱对象群常常需要好几次GC的运行才能完成。就像上面描述的场景,弱引用对象常常用于Map结构中,引用数据量较大的对象,一旦该对象的强引用为null时,GC能够快速地回收该对象空间。
  • 虚引用:又称为幽灵引用,主要目的是在一个对象所占的内存被实际回收之前得到通知,从而可以进行一些相关的清理工作。幽灵引用在使用方式上与之前介绍的三种引用类型有很大的不同。首先,幽灵引用在创建时必须提供一个引用队列作为参数,其次幽灵引用对象的get方法总是返回null,因此无法通过幽灵引用来获取被引用的对象。

finalization机制

当垃圾回收器发现没有引用指向一个对象时,会调用这个对象的finalize方法。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库连接等。由于finalize方法的存在,虚拟机中的对象一般处于三种可能的状态。第一种是可达状态,当有引用指向该对象时,该对象处于可达状态。根据引用类型的不同,有可能处于强引用可达,软引用可达或弱引用可达状态。第二种是可复活状态,如果对象的类覆写了finalize方法,则对象有可能处于该状态。虽然垃圾回收器是在对象没有引用的情况下才调用其finalize方法,但是在finalize方法的实现中可能为当前对象添加新的引用。因此在finalize方法运行完成之后,垃圾回收器需要重新检查该对象的引用。如果发现新的引用,那么对象会回到可达状态,相当于该对象被复活,否则对象会变成不可达状态。当对象从可复活状态变为可达状态之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可达状态,也就是说,一个对象的finalize方法只会被调用一次。第三种是不可达状态,在这个状态下,垃圾回收器可以自由地释放对象所占用的内存空间。

Java对象头

 在Hotspot虚拟机中,对象在内存中的布局可以分为对象头,实例数据,对其填充三部分。

  • 对象头:它主要包括对象自身的运行行元数据,比如哈希码,GC分代年龄,锁状态标志等,同时还包括一个类型指针,指向类元数据,表明该类对象所属的类型。
  • 实例数据:它是对象真正储存的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)。
  • 对齐填充:它不是必要的存在的,仅仅起着占位符的作用。

 JIT(Just-In-Time)编译器

JIT编译器能够将MSIL编译称为各种不同的机器代码,以适应对应的系统平台,最终使得程序在目标系统中得到顺利地运行。

在Java编程语言和环境中,即时编译(JIT,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源程序的语句将由Java虚拟机编译成字节码,而不是编译成某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台运行的独立于平台的代码。

JIT编译器为JVM的执行机制提供了性能保证。由于Java字节码是通过解释执行的,因此效率很低。在Java的发展历史里,一共有两套解释执行器,即古老的字节码解释器、现在普遍使用的模板解释器。字节码解释器在执行时通过纯软件代码模拟字节码的执行,因此效率非常低下,而模板解释器将每一条字节码和一个模板相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。但即便如此,仅凭借解释器,JVM的执行效率依然很低,为了解决这个问题,JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。

  • 编译执行和解释执行的区别:举个简单的例子,编译执行就是做好了一桌子菜之后才开始吃,而解释执行就相当于在吃火锅一边弄着一边吃着。

枚举根节点

在Java语言里面,可作为GC Roots的节点主要是在全局性的引用(例如常量或静态属性)与执行上下文(例如栈帧中的本地变量表)中。如果要使用可达性分析来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行——这里“一致性”的意思是整个分析期间整个执行系统看起来就像是被冻结在某个时间点上,不可以出现分析过程中,对象引用关系还在不断变化的情况,这点不满足的话分析结果的准确性就无法保证。这点也是导致GC进行时,必须“Stop the world”的一个重要的原因,即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

由于目前的主流JVM使用的都是准确式GC,所以当系统停顿下来之后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象的引用。在Hotspot的实现中,是使用一组成为OopMap的数据结构来达到这个目的的,在类加载完成的时候,Hotspot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在待定的位置记录下栈里和寄存器哪些位置是引用的。这样GC在扫描时就可以直接得知这些信息了。

并行和并发

  • 并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。
  • 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
  • 并行是在一台处理器上“同时”处理多个任务,并发是在多台处理器上同时处理多个任务。如Hadoop分布式集群。

并发在JVM里面,是指垃圾收集线程和用户线程同时执行,但不一定是并行执行,可能是交叉执行,用户程序继续运行,而垃圾收集程序运行在另一个CPU上。

对象提升规则

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过选项-XX:MAXTenuringThreshold来设置。

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象),G1 GC针对大对象有自己的处理方法。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(年轻代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC会进入Survivor区,之后每经过一次Minor GC,则对象的年龄加1,直到达到阈值对象进入老年代。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余价值大小则进行一次Full GC,如果小于则进入检查HandlePromotionFailure逻辑。判断这个逻辑,如果是True则只进行Minor GC,如果是False则进行Full GC。

触发Full GC执行的情况

  1. 老年代空间不足:老年代空间只有在年轻代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误Java.lang.OutOfMemoryError:Java heap space。为了避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收,让对象在年轻代多存活一段时间,以及尽量不要创建过大的对象和数组。
  2. 永久代空间满:永久代中存放的是一些类的信息,当系统中要加载的类、反射的类和调用的方法比较多时,永久代可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下信息Java.lang.OutOfMemoryError:PermGen space。为了避免永久代被占满造成Full GC现象,可采用的方法为增大永久代空间或转为使用CMS GC。
  3. CMS GC时出现Promotion Failed和Concurrent Mode Failure:对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有Promotion Failed和Concurrent Mode Failure两种状况,当这两种状况出现时可能会触发Full GC。Promotion Failed是在进行Minor GC时,Survivor Space放不下、对象只能放入老年代,而此时老年代也放不下时造成的。Concurrent Mode Failure是在执行CMS GC的过程中,同时有对象要放入老年代,而此时老年代空间不足造成的。应对措施为增大Survivor Space、老年代空间或调低触发并发GC的比率,但在JDK1.5+、6.0+的版本中有可能会由于JDK的Bug29导致CMS在Remark完毕后很久才触发清除动作。对于这种情况,可通过设置选项-XX:CMSMaxAbortablePrecleanTime=5(单位为毫秒)来避免。
  4. 统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间:这是一个较为复杂的触发情况,Hotspot为了避免由于年轻代晋升到老年代导致老年代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间,那么久直接触发Full GC。

对象存活判断

  • 引用计数器:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象的相互循环引用的问题。
  • 可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的不可达对象。
  • GC Roots包括:虚拟机栈内引用的对象;方法区中类静态属性实体引用的对象;方法区中常量引用的对象;本地方法栈内JNI引用的对象。
posted @ 2018-05-31 10:50  Accper  阅读(138)  评论(0编辑  收藏  举报