doclist 阅读(5) 评论(0)

JVM结构

JVM总体结构图

类加载子系统与方法区:

  类加载子系统负责从文件系统和网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。

  除了类信息外,方法区中还可能会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

Java堆:

  java堆在虚拟机启动时建立,它是java程序最主要的内存工作区域。

  几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的。

直接内存:

  java的NIO库允许使用直接内存。

  直接内存是在java堆外的、直接向系统申请的内存空间。

  通常访问直接内存的速度会优于java堆。

  出于对性能的考虑,读写频繁的场合可能会考虑使用直接内存。

  由于直接内存在java堆外,因此它的大小不会受限于Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

垃圾回收系统:

  垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆是垃圾收集器的工作重点。

  和C/C++不同,java中所有的对象空间释放都是隐式的,java中没有类似free()或者delete()这样的函数释放指定的内存区域。

  对于不再使用的垃圾对象,垃圾回收系统会在后台查找、标识并释放对象,完成java堆、方法区和直接内存中的全自动化管理。

java栈:

  每一个java虚拟机线程都有一个私有的java栈。

  一个线程的java栈在线程创建的时候被创建。

  java栈中保存着帧信息,保存着局部变量、方法参数,同时和java方法的调用、返回密切相关。

本地方法栈:

  和java栈非常类似。

  java栈用于方法的调用,而本地方法栈则用于本地方法的调用。

  作为对java虚拟机的重要扩展,java虚拟机允许java直接调用本地方法(通常使用C编写)

PC(Program Counter):

  PC寄存器也是每一个线程私有的空间。

  java虚拟机会为每一个java线程创建PC寄存器。

  在任意时刻,一个Java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined。

执行引擎:

  负责执行虚拟机的字节码。

  现代虚拟机为了提高执行效率,会使用即使编译(just in time)技术将方法编译成机器码后再执行。

 

JVM堆结构及分代

  堆内存是java虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,程序中所有的对象实例都存放在堆内存中。

  给堆内存分代是为了提高对象内存分配和垃圾回收的效率。

  如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,所花费的时间代价是巨大的,也会严重影响GC效率。

  

  Java虚拟机根据对象存活的周期不同,把堆内存分为新生代、老年代和永久代(于JDK8中把存放元数据中的永久内存从堆内存中移到了本地内存(native memory)

  新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象放在老年代。

  新生代中的对象存活时间短,需要频繁地进行垃圾回收以保证无用对象尽早被释放掉;老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收。不同年代采用各自合适的垃圾回收算法,可以大大提高回收效率。

 

新生代(Young Generation)

  新生代主要存放新生成的对象,内存大小相对会比较小,垃圾回收会比较频繁。且垃圾回收效率高,通常进行一次垃圾收集一般可以回收70% ~ 95%的空间。

  HotSpot JVM把新生代代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to),默认比例是 8:1:1。这样划分的目的是因为HotSpot采用复制算法来回收新生代。(复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。)新生成的对象在Eden区分配(一些大对象除外),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

  在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor0区,Survivor1区“To”是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到“To”Survivor1区,而在“From”Surivor0区中,仍存活的对象会根据他们的年龄值来决定去向。新生代中的对象每熬过一轮垃圾回收,年龄值就加1,年龄值达到年龄阀值(默认为15,可以通过-XX:MaxTenuringThreshold来设置,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor1区。经过这次GC后,Eden区和“From"Surivor0区已经被清空。接着将”To“区与”From“区交换,确保”To“区在一轮GC后是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,没有足够的空间存放上一次新生代收集下来的存活对象时,会将所有的对象移入老年代。

 

老年代(Old Generation)

  老年代主要存放老年代的空间用于存放长时间幸存的对象,即在新生代中经历了多次GC后仍然存活下来的对象。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,回收的速度也比较慢。

  通常当老年代内存被占满时进行一次Major GC。相较于minor GC, Major GC的执行次数要比minor GC要少很多,同时,Major Gc 执行的时间较Minor Gc要长。

 

永久代(Permanent)

  于JDK8中把存放元数据中的永久内存从堆内存中移到了本地内存(native memory)。

 

 

 

JVM垃圾回收算法及收集器

垃圾回收常见算法

引用计数(Reference Counting)

  比较古老的回收算法,原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。

  垃圾回收时,只用收集计数为0的对象,此算法最致命的是无法处理循环引用的问题。

复制(Copying)

  此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。

  垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中,此算法每次只处理正在使用中的对象,因此复制成本较小,同时复制过去以后还能进行相应的内存管理,不会出现”碎片“问题。

  当然,此算法的缺点也很明显,就是需要两倍内存空间。

标记-清除(Mark - Sweep)

  此算法执行分两阶段,第一阶段从引用根结点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。

标记-整理(Mark-Compact)

  此算法结合了”标记-清除“和”复制“两个算法的优点。也是分两个阶段,第一阶段从根结点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象”压缩“到堆的其中一块,按顺序排放。此算法避免了”标记-清除“的碎片问题,同时也避免了”复制“算法的空间问题。

 

jvm中垃圾收集器

Scavenge Gc(次收集)和 Full GC(全收集)的区别

  新生代GC(Scavenge GC):Scavenge GC指发生在新生代的GC,因为新生代的java对象生命周期短,所以Scavenge GC非常频繁,一般回收速度也比较快。当Eden空间不足为对象分配内存时,会触发Scavenge GC。

  一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代Eden区进行,不会影响到老年代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。

  老年代GC(Full GC/Major GC):Full GC指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代对象大部分是Minor GC过程中从新生代进入老年代),比如分配担保失败。Full GC的速度一般会比Minor GC慢 10 倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full Gc。

次收集

  当年轻代堆空间紧张时会被触发

  相对于全收集而言,收集间隔较短

全收集

  当老年代或者持久代堆空间满了,会触发全收集操作

  可以使用System.gc()方法来显式的启动全收集

  全收集一般根据堆大小的不同,需要的时间不尽相同,但一般会比较长。

 

分代垃圾回收器

新生代收集器

串行收集器(Serial)

  Serial收集器是JAVA虚拟机中最基本、历史最悠久的收集器,在JDK 1.3.1之前是JAVA虚拟机新生代收集的唯一选择。Serial收集器是一个,但它的“单线程”的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

  Serial收集器到JDK1.7为止,它依然是JAVA虚拟机运行在Client模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial收集器对于运行在来说是一个很好的选择。

并行收集器(ParNew)

  ParNew收集器是JAVA虚拟机中垃圾收集器的一种。它是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器一致。

  ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器,,除了Serial收集器外,只有它能与CMS收集器配合工作。

Paraller Scavenge收集器  

  Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

  Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多线程和”标记-整理”算法。这个收集器是在jdk1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外别无选择。由于单线程的老年代Serial Old收集器在服务端应用性能上的”拖累“,即使使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,又因为老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合”给力“。直到Parallel Old收集器出现后,”吞吐量优先“收集器终于有了比较名副其实的应用祝贺,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

  -UseParallelGC: 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收。-UseParallelOldGC: 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行垃圾回收

 

老年代收集器

Serial Old收集器

  Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。

Parallel Old收集器

  Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS收集器  

  CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为6个步骤,包括:     

    初始标记(CMS initial mark)

    并发标记(CMS concurrent mark)

    并发预清理(CMS-concurrent-preclean)

    重新标记(CMS remark)

    并发清除(CMS concurrent sweep)

    并发重置(CMS-concurrent-reset)

  其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。其他动作都是并发的。

 

分区收集-G1 收集器

 

JVM优化

JVM小工具

  java jdk的bin目录下带有一些jvm小工具。

jps(Java Virtual Machine Process Status Tool)

  列出正在运行的java进程,并显示执行主类的名称及进程在本地JVM中的ID。

  使用方法:

    jps [options][hostid] [options]:

  -q: 只输出LVMID

  -m: 输出JVM启动时传给主类的方法

  -l:输出主类的全名,如果是Jar则输出jar的路径

  -v: 输出JVM启动参数

jstat(Java Virtual Machine Statistics Monitoring Tool)

  JVM统计信息监控工具.

  监控JVM各种运行状态信息,如虚拟机进程中的类装载、内存、GC、JIT编译等数据。

  使用方法:

  vmid是虚拟机ID,在Linux/Unix系统上一般就是进程ID。interval是采样时间间隔。count是采样数目。

  S0C、S1C、S0U、S1U:Surivor 0/1区容量(Capacity)和使用量(Used)

  EC、EU:Eden区容量和使用量

  OC、OU:年老代容量和使用量

  MC、MU:方法区容量和使用量

  CCSC、CCSU:压缩类容量和使用量

  YGC、YGT:年轻代GC次数和GC耗时

  FGC、FGT:Full GC次数和Full GC耗时

  GCT:GC耗时

jmap(Java Virtual Machine Memory Map for Java)

  java内存映像工具。

  用于生成堆转储快照,即dump文件可以查询finalize执行队列、Java堆和永久代的详细信息(使用率、当前用的GC等)。

jstack(Java Virtual Machine Stack Trace for Java)

  堆栈跟踪工具。

  用于生成JVM当前的线程快照(即当前JVM内每一个条线程正在执行的方法堆栈集合)用于分析线程出现长时间停顿的原因。

javap

  查看经javac之后产生的JVM字节码代码

jcmd

  一个多功能工具,可以用来导出堆,查看Java进程、导出线程信息、执行GC、查看性能相关数据等。

jvisualvm

  JDK中最强大运行监视和故障处理工具

 

JVM参数介绍

  -Xms:初始堆大小

  -Xmx:最大堆大小

  -XX:NewSize=n 设置年轻代大小

  -XX:NewRatio=n 设置年轻代和老年代的比值

  -XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值

  -XX:MaxPermSize=n 设置持久代大小

 

  收集器设置

  -XX:+UseSerialGC 设置串行收集器

  -XX:+UseParallelGC 设置并行收集器

  -XX:+UseParalledlOldGC 设置并行年老代收集器

  -XX:+UseConcMarkSweepGC 设置并发收集器

 

  垃圾回收统计信息

  -XX:+PrintGC

  -XX:+Printetails

  -XX:PrintGCTimeStamps

  -Xloggc:filename