??Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的虚拟计算机,Java虚拟机屏蔽了与具体操作系统的相关性,使得Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上运行。正是Java虚拟机使得Java程序能够做到“编译一次,到处运行”,成就了Java。作为Java程序员,必须对Java虚拟机有学习理解,才能做好Java程序开发。当然这里的学习理解只是基本的虚拟机规范的理解,具体细节的理解掌握就太难了,对于Java开发来说也是没有必要的。我认为的Java程序员必须了解的Java虚拟机知识总结如下:
一、Java虚拟机内存管理模型
??我们平时使用的虚拟机基本都是HotSpot VM,我就以这个虚拟机为准。平时开发和维护系统的时候会经常遇到java.lang.OutOfMemoryError的错误。产生这个问题的原因就是虚拟机内存不够了。内存不够了只是直接原因,真正的原因需要我们理解虚拟机的内存模型、垃圾回收机制,结合我们程序来分析找到具体原因。
??Java虚拟机管理的内存包括几个运行时数据内存:方法区、堆、虚拟机栈、本地方法栈、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区。
- Java堆(Heap)
??堆是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的内存区域。在Java虚拟机规范中有描述:所有的对象实例和数组都要在堆上分配。所以Java堆是垃圾书收集器管理的主要区域,可以通过-Xmx和-Xms控制堆的大小。Java堆还细分为:新生代和老年代。分代划分的目的是为了更高效的回收内存。新建的对象都由新生代分配内存。常常又被划分为Eden区和Survivor区。Eden空间不足时会把存活的对象转移到Survivor。新生代的大小可由-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。经过多次垃圾回收仍然存活的对象就转入老年代。 - 方法区(Method Area)
??方法区也是线程共享的内存区域,存储被虚拟机加载的类信息、常量、静态变量、即时编译的代码数据等。HotSpot虚拟机GC分代收集扩展到方法区,把方法区称为“永久代”。该区域主要是针对常量池和类型的卸载回收。由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。Jdk8废弃永久代,引入了元空间(Metaspace)。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,理论上可以扩展到32位/64位系统可虚拟的内存大小。 - 虚拟机栈(VM Stack)
??虚拟机栈是线程私有的,生命周期与线程是一致,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出入口等信息,每个方法的调用到执行完成的过程就是一个栈帧入栈到出栈的过程。虚拟机栈规定了2种异常情况,一种是线程请求栈的深度大于虚拟机栈所允许的深度,这时候将会抛出StackOverflowError异常(一般的函数调用都不会大于默认的栈深度,除非递归函数非常深);另一种是如果当Java虚拟机允许动态扩展虚拟机栈的时候,当扩展的时候没办法分配到内存的时候就会报OutOfMemoryError异常。 - 本地方法栈(Native Method Stack)
??与虚拟机栈执行的基本相同,唯一的区别就是虚拟机栈是执行Java方法的,本地方法栈是执行native方法的;本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。 - 程序计数器(Program Counter Register)
??程序计数器是一块较小的内存空间,用来指定当前线程执行字节码的行数,每个线程计数器都是私有的,因为每个线程都需要记录执行的行数。该内存区域是Java虚拟机唯一没有规定任何OutOfMemoryError的区域。所以程序计数器对程序员开发来说基本是透明不用管的。
二、内存分配策略
- 对象优先在Eden分配
??新对象在新生代Eden区中分配,当Eden没有足够空间分配时,虚拟机将发起一次Minor GC(从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC),GC后将已有对象放入Survivor中,若Survivor空间不足,则通过分配担保机制提前转移到老年代。 - 大对象直接进入老年代
??大对象是指需要大量连续内存空间的Java对象,例如较长的字符串和较长的数组。经常出现大对象容易导致内存还有不少空间就提前触发垃圾收集来获得连续的大空间来分配给大对象,为了避免这种问题,虚拟机提供了一个-XX:PretenureSizeThreshold参数(只有Serial和ParNew收集器有效),令大于这个参数的值的对象直接在老年代分配。 - 长期存活的对象进入老年代
??虚拟机用分代收集的思想来管理内存,那么新生代是怎么进入老年代的呢。虚拟机给每个对象定义一个对象年龄计数器。如果对象在Eden出生并经过第一次MinorGC然后仍然存活,并且能被Survivor容纳的话,将被移到Survivor中,并且对象年龄设为1。对象在Survivor中没过一次Minor GC,年龄就增加一岁。当年龄增加到一定程度(默认为15岁),就会晋升到老年代。对象晋升老年代的年龄阈值,可以通过参数:-XX:MaxTenuringThreshold设置。 - 动态对象年龄判定
??如果Survivor中的对象年龄都不超过晋升老年代的年龄阈值并且消耗完了Survivor的空间就出问题了,所以虚拟机并不是永远的要求对象必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到MaxTenuringThreshold的要求。 - 空间分配担保
??在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么Minor GC可以确保是安全的(极端情况下内存回收后新生代中所有对象都存活,把Survivor无法容纳的对象直接放入老年代,老年代就必须有能容纳这些对象的空间)。若不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,若允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小(相当于一个经验值,不保证可以成功),如果大于,尽管这次Minor GC冒险,也会尝试进行一次Minor GC;如果小于,或者HandlePromotionFailure设置为不允许冒险,那么需要进行一次Full GC。
三、垃圾收集器
??Jdk7以后,HotSpot虚拟机有7种垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。他们之间的关系如下图(连线表示可以组合使用)。
新生代收集器:Serial、ParNew、Parallel Scavenge,基本是采用复制算法,因为新生代对象大多是朝生夕灭,存活的少,复制内存小,使用复制简单高效。
老年代收集器:Serial Old、Parallel Old、CMS, 基本是采用标记-清除或标记-整理算法,因为老年代对象存活率高,不适合复制算法。
整堆收集器:G1,从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法。
并行(Parallel)指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态,如ParNew、Parallel Scavenge、Parallel Old;
并发(Concurrent)指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),如CMS、G1;
Minor GC又称新生代GC,指发生在新生代的垃圾收集动作,因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,回收速度也比较快;
Major GC又称老年代GC,指发生在老年代的GC;
Full GC是指Major GC和Minor GC一起发生同时执行,执行Major GC的时候都会伴随执行Minor GC。 - Serial收集器
??Serial垃圾收集器是最基本、最早的回收新生代内存的单线程串行收集器;进行垃圾收集时,必须暂停所有工作线程。对于单个CPU的环境,Serial收集器没有线程切换开销,可以获得最高的单线程收集效率。只是说Serial收集器在回收的时候会暂停其他线程,所以就在Client模式下用户的桌面应用场景中适合,管理内存不大,回收时间短,停顿是可以接受的。-XX:+UseSerialGC:添加该参数来显式的使用Serial收集器。 - ParNew收集器
??ParNew垃圾收集器是Serial收集器的多线程版本,除了多线程外,其余的行为、特点和Serial收集器一样。在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作。
??-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器。
??-XX:+UseParNewGC:强制指定使用ParNew收集器。
??-XX:ParallelGCThreads:指定垃圾收集的线程数,ParNew默认的收集线程与CPU数量相同。 - Parallel Scavenge收集器
??Parallel Scavenge收集器和ParNew垃圾收集器相似,只是Parallel Scavenge注重吞吐量,也称为吞吐量收集器(Throughput Collector)。他的目标是达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间。Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大 大专栏 Java虚拟机理解总结垃圾收集停顿时间的-XX:MaxGCPauseMillis参数和设置吞吐量大小的-XX:GCTimeRatio。此外,还有一个值得关注的参数-XX:+UseAdptiveSizePolicy,开启这个参数后,就不用手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等。JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量。
??Parallel Scavenge收集器适合在要求高吞吐量,对暂停时间要求不高,不需要太多用户交互的后台计算为主的场景。如:批处理系统、科学计算等。 - Serial Old收集器
??Serial Old收集器是Serial收集器的老年代版本,单线程串行收集器,主要用于Client模式。在Server模式有两大用途:1、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);2、作为CMS收集器的后备预案,在CMS并发收集发生Concurrent Mode Failure时使用(CMS收集器里面再谈)。 - Parallel Old收集器
??Parallel Old收集器Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。JDK1.6开始使用这个收集器,在注重吞吐量以及CPU资源敏感的场景,Parallel Scavenge加Parallel Old收集器的组合最合适。-XX:+UseParallelOldGC:指定使用Parallel Old收集器。 - CMS收集器
??并发标记清理(Concurrent Mark Sweep,CMS)收集器以获得最短回收停顿时间为目标的收集器。是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器。非常适合希望系统停顿时间最短,注重服务的响应速度的互联网网站、B/S系统。
??CMS收集器是基于标记-清除算法实现,整过过程包括四个步骤,初始标记、并发标记、重新标记、并发清除。其中,初始标记、重新标记这两个步骤需要暂停工作线程,会有短暂的停顿。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。重新标记是为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,稍微比初始标记慢一点,但是远比并发标记时间短。并发标记和并发清除过程都可以和用户线程一起工作,所以说,CMS总体来说是低停顿的并发收集器。但是CMS也不是完美的,还有三个明显的缺点:
??1、对CPU资源非常敏感
??并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。MS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
??2、无法处理浮动垃圾,可能出现Concurrent Mode Failure失败
??在并发清除时,用户线程新产生的垃圾,称为浮动垃圾。因此不能像其他收集器在老年代几乎填满再进行收集,需要预留一定的内存空间提供给并发收集时使用。-XX:CMSInitiatingOccupancyFraction:设置CMS预留内存空间,JDK1.5默认值为68%,JDK1.6变为大约92%。如果CMS运行期间预留的内存无法满足需要,就会出现“Concurrent Mode Failure”失败,这是虚拟机启动后备预案:临时启用Serail Old收集器来重新进行老年代垃圾回收,这样就停顿很长时间了。所以不能为了节约内存这个参数值设置太大,引起不必要的性能下降。
??3、产生大量内存碎片
??由于CMS基于”标记-清除”算法,会产生大量不连续的内存碎片,导致分配大内存对象时,无法找到足够的连续内存,从而提前触发Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS在进行Full GC 前进行内存压缩。但是内存压缩非常耗时,如果每次Full GC都压缩停顿时间就大了,所以引入了另外一个参数-XX:+CMSFullGCsBeforeCompaction设置执行多少次不压缩的Full GC后,来一次压缩整理。默认值是0(表示每次Full GC前都压缩)。 - G1收集器
??G1(Garbage-First)是JDK7-u4才推出的收集器,未来可以替换掉CMS的收集器。目标是多处理器机器、大内存机器。它符合垃圾收集暂停时间短,同时实现高吞吐量的目标。G1收集器有四个特点:
??1、并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop the World停顿时间。回收线程和客户线程可以并发执行。
??2、分代收集,收集范围包括新生代和老年代:能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式处理不同时期的对象;虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region); 新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合。
??3、空间整合:结合多种垃圾收集算法,空间整合,不产生碎片。从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法。
??4、可预测的停顿: G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。
??G1收集器最主要的应用是为需要低GC延迟,并具有大堆的应用适合,如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;如果现在采用的收集器没有出现问题,不用急着去选择G1;如果应用程序追求低停顿,可以尝试选择G1;是否代替CMS需要实际场景测试后才决定。
四、虚拟机监控
??刚上线的系统一般都会去监控一下虚拟机的运行情况,以便调优。当系统出现内存溢出或者崩溃的时候为了定位问题我们也需要监控来发现问题,Jdk为我们提供了监控工具,在${JAVA_HOME}/bin目录下面有6个命令行工具以及两个可视化工具。一般我们在服务器上使用命令行工具来监控比较方便,在本地机器使用图形化界面远程监控方便。
五、类加载器
??类加载器是什么、有什么作用,我觉得对程序员来说必须了解,因为你要实现一个框架、一个Web服务器、热拔插功能等,都可能需要自己实现类加载器来加载类。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够满足大多数情况的需求了。不过如果遇到了需要与类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试 ClassNotFoundException和 NoClassDefFoundError等异常。
??顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。任何一个类都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,也就是说即使是同一个类被两个不同的类加载器加载,在虚拟机中他们也是不同的类。
??从Java虚拟机角度上来看,类加载器可以分为两类,一类是虚拟机启动需要的由虚拟机自身具有的启动类加载器(需要启动加载器来加载java基础类库);另一类就是其他的非启动类加载器(jvm提供的非启动加载器和自定义类加载器)。
??但是从开发者角度来区分类加载器的分类和组织结构才能真正了解加载器的机制和作用,从开发者来看可以分为四类加载器,四类加载器是一种层级关系。如图:
- Bootstrap ClassLoader(启动类加载器)
??Bootstrap ClassLoader是Jvm自带的C++实现的根加载器,JVM启动时即初始化此ClassLoader,加载Java的核心API:$JAVA_HOME中jre/lib/rt.jar中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。 - Extension ClassLoader(扩展类加载器)
??Extension ClassLoader负责加载<JAVA_HOME>libext目录或者java.ext.dirs系统变量所指定的路径中的所有类库。 - App ClassLoader(应用程序类加载器)
??App ClassLoader这个类加载器一般也成为系统类加载器,ClassLoader.getSystemClassLoader()的返回值就是这个类加载器。他负责加载用户类路径(ClassPath)上所有指定的类库。如果没有自定义类加载器,这个类加载器就是程序默认的类加载器。 - Custom ClassLoader(自定义类加载器)
??应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据J2EE规范自行实现ClassLoader,web服务器要解决多个web应用的java类隔离、类共享、热部署等功能。必须自定义类加载器来实现一套加载机制。 - 加载器之间的关系
??加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。JVM在加载类时默认采用的是双亲委派机制。就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归。如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
??但是双亲委派机制并不是一种强制性的约束模型,还存在打破这个模型的情况,例如:线程上下文类加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器就是应用程序类加载器。像JDBC就是采用了这种方式。这种行为就是逆向使用了加载器,违背了双亲委派模型的一般性原则。–参考资料 《深入理解java虚拟机》
原文地址:https://www.cnblogs.com/liuzhongrong/p/12251347.html