当前位置: 首页 > 后端技术 > Java

JVM完整详解:内存分配+运行原理+回收算法+GC参数等

时间:2023-04-01 19:37:18 Java

JVM完整详解:内存分配+运行原理+回收算法+GC参数等内存模型,内存分配,回收算法机制等等,这些都是需要掌握的技能。JVM内存模型JVM内存模型可以分为两部分。如下图所示,堆和方法区是所有线程共享的,而虚拟机栈、本地方法栈和程序计数器是线程私有的。1.堆(Heap)堆内存是所有线程共享的,可以分为新生代和老年代两部分。下图中的Perm代表的是永久代,但是注意永久代不是堆内存的一部分,jdk1.8之后永久代也会被去掉。堆是java虚拟机管理的内存中最大的内存区域,也是各个线程共享的内存区域。该内存区域存储对象实例和数组(但并非所有对象实例都在堆中)。它的大小由-Xms(最小值)和-Xmx(最大值)参数设置(最大值和最小值均小于1G),前者是启动时申请的最小内存,默认为1/操作系统物理内存的64,后者是JVM可以申请的最大内存,默认是物理内存的1/4。默认情况下,当空闲堆内存小于40%时,JVM会增加堆内存到-Xmx指定的大小,可以通过-XX:MinHeapFreeRation=Ratio指定;当空闲堆内存大于70%时,JVM会将堆内存的大小减少到-Xms指定的大小,可以通过XX:MaxHeapFreeRation=来指定,当然可以避免频繁调整堆内存的大小Heap在运行时,通常把-Xms和-Xmx的值设置成一样的。堆内存=新生代+老年代+永久代。我们在收集垃圾的时候,往往会将堆内存分为新生代和老年代(大小比例1:2)。新生代由Eden、Survivor0和Survivor1组成。三者的比例为8:1:1。新生代恢复机制采用复制算法。在MinorGC的时候,我们都预留了一个生存区来存放存活的对象。实际区域是Eden+生存区域之一。当我们的对象超过一定年龄(默认15,可以通过参数设置),对象就会被放入老年代,当然大对象会直接进入老年代。老年代使用的回收算法是标记排序算法。2.方法区(MethodArea)方法区也称为“永久代”。用于存放虚拟机加载的类信息、常量和静态变量,是各个线程共享的一块内存区域。默认最小值为16MB,最大值为64MB(64位JVM由于指针膨胀默认为85M),方法区的大小可以通过-XX:PermSize和-XX:MaxPermSize参数来限制。它是一个连续的堆空间,永久代的垃圾回收是和老年代(oldgeneration)捆绑在一起的,所以不管谁满了,都会触发永久代和老年代的垃圾回收。但是,一个明显的问题是,当JVM加载的类信息容量超过参数-XX:MaxPermSize设置的值时,应用程序就会报OOM错误。参数由-XX:PermSize和-XX:MaxPermSize设置。3、虚拟机栈(JVMStack)描述了java方法执行的内存模型:每个方法执行时,都会创建一个“栈帧”,用来存放局部变量表(包括参数)、操作栈、方法出口等信息。每个方法从调用到执行的过程对应一个栈帧在虚拟机栈中从入栈到出栈的过程。语句周期与线程相同,是线程私有的。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区组织为一个数组,以字长为单位,从0开始计数。和局部变量区一样,操作数栈也组织为一个以字长为单位的数组。但与前者不同的是,它不是通过索引来访问的,而是通过push和popping来访问的,可以看成是临时数据的存储区。java栈帧除了局部变量区和操作数栈外,还需要一些数据来支持常量池解析、方法正常返回、异常派发机制。这些数据存放在java栈帧的帧数据区。局部变量表:存放编译器已知的各种基本数据类型和对象引用(引用指针,不是对象本身),其中64位long和double数据会占用2个局部变量的空间,其他数据类型只取1、局部变量表所需的内存空间是在编译时分配的。当进入一个方法时,这个方法需要在栈帧中分配多少个局部变量是完全确定的。栈帧在运行过程中不会改变局部变量表的大小。空间。4.本地方法栈(NativeStack)与虚拟机栈基本类似,区别在于虚拟机栈服务于虚拟机执行的Java方法,而本地方法栈服务于Native方法。(栈的空间大小远小于堆)5、程序计数器(PCRegister)是最小的内存区域,它的作用是当前线程执行的字节码的行号指示器。在虚拟机模型中,字节码解释器工作时,通过改变计数器的值来选择下一条要执行的字节码指令。分支、循环、异常处理、线程恢复等基本功能都依赖于计数器来完成。6、直接内存直接内存不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4新加入的NIO引入了channels和buffer的IO方式。可以调用Native方法直接分配堆外内存。这个堆外内存是本机内存,不会影响堆内存的大小。JVM垃圾回收算法1、标记清除原理:从根收集节点开始扫描,标记所有存活的对象,最后扫描整个内存空间,清除未标记的对象(即死对象)。适用场合:存活对象较多时效率更高适用于老年代(也就是老年代)缺点:标记清除算法带来的一个问题是会产生大量的空间碎片,因为空间after回收不是连续的,所以在为大对象分配内存时可能会更早。触发完整的gc。2、复制算法原理:从根集合节点开始扫描,标记所有存活的对象,将这些存活的对象复制到新的一块内存中(图中最下面的那块内存),然后复制原来的一块内存(图中最上方的那块内存)全部回收适用场合:当存活对象不多时,扫描一次整个空间效率更高(标记存活对象并复制移动)适用toyounggeneration(也就是新生代):基本上98%的对象都是“生死存亡”,存活下来的很少。缺点:需要一个空的内存空间,需要复制移动的物体3.标记原理:从根集合节点开始扫描,标记所有存活的对象,最后扫描整个内存空间,清除未标记的对象(即死对象)(你可以发现这些就是mark-clear算法的原理)。清除后,将所有幸存的物体一起向左移动。适用场合:用在老年代(也就是老年代)缺点:需要移动对象。如果对象很多,标记回收后的内存很不完整,可能需要一定的时间扫描整个空间两次(第一次:标记存活对象;第二次:清除未标记对象)优点:不占内存碎片化4.分代收集算法目前商用虚拟机的垃圾收集采用的是“分代收集”(GenerationalCollection)算法,这种算法并没有什么新意,只是将内存按照生命周期的不同分成若干块的对象。一般Java堆分为新生代和老年代,这样可以根据各个年代的特点采用最合适的收集算法。专项研究表明,新生代中98%的对象都是“生死存亡”,所以没有必要按照1:1的比例划分内存空间,而是将内存划分为一个更大的伊甸园空间和两个较小的。小Survivor空间,每次使用Eden和Survivor[1]之一。回收时,一次性将Eden和Survivor中的存活对象复制到另一个Survivor空间,最后清理掉刚刚使用过的Eden和Survivor空间。HotSpot虚拟机默认的Eden与Survivor比例为8:1,即每次新生代中的可用内存空间为整个新生代容量的90%(80%+10%),只有10%的新生代容量内存将被“浪费”。”。当然98%的可回收对象只是一般场景下的数据,我们没办法保证每次回收存活的对象不超过10%,当Survivor空间不够用的时候,就需要依赖在其他内存(这里指的是老年代)进行Assignmentguarantee(HandlePromotion),在新生代中,发现每次垃圾回收都有大量对象死亡,只有少量对象存活。然后采用复制算法,只需付出复制少量存活对象的代价就可以完成收集,在老年代,由于对象存活率高,没有额外的空间分配给它,需要使用“mark-clean”或“mark-organize”算法进行回收垃圾收集器1.串行收集器串行收集器是最古老的收集器,它的缺点是当串行收集器要进行垃圾收集时,它必须暂停用户的所有进程,即停止世界。到现在为止,它仍然是运行在客户端模式下的虚拟机默认的新生代收集器。与其他收集器相比,对于仅限于单CPU的运行环境,Serial收集器没有线程交互开销,集中精力进行垃圾收集,自然可以获得最高的单线程收集效率。2.ParNew收集器ParNew收集器是新一代Serial收集器的多线程实现。请注意,它在垃圾收集期间仍然会停止世界,但与Serial收集器相比,它会运行多个进程进行垃圾收集。ParNew收集器在单CPU环境下永远不会比Serial收集器有更好的效果。即使由于线程交互的开销,收集器在超线程技术实现的双CPU环境中也不能100%有效。保证覆盖串行收集器。当然,随着可以使用的CPU数量的增加,对于GC时的系统资源利用率还是很有好处的。默认启用的收集线程数与CPU数相同。在CPU数量较多的环境下(比如32个,现在CPU往往有4核和超线程,越来越多的服务器有超过32个逻辑CPU),可以使用-XX:ParallelGCThreads参数来限制数量垃圾收集线程。3.ParallelScavenge收集器Parallel是一种使用复制算法的多线程新生代垃圾收集器,它看起来与ParNew收集器有很多相似之处。但是ParallelScanvenge收集器的一个特点是它关注的是吞吐量(Throughput)。所谓吞吐量就是CPU花在运行用户代码上的时间与CPU消耗的总时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)。暂停时间越短,越适合需要与用户交互的程序。良好的响应速度可以提升用户体验;而高吞吐量可以最有效地利用CPU时间,尽快完成程序的计算任务。主要适用于后台计算。不需要太多交互的任务。4.CMS收集器CMS(ConcurrentMarkSwep)收集器是目前应用比较广泛的一个比较重要的收集器。让我们专注于它。CMS是一个收集器,旨在获取最短的恢复停顿时间,这使得它非常适合与用户交互的业务。从名字(MarkSwep)可以看出,CMS收集器是基于mark-and-sweep算法实现的。它的收集过程分为四个步骤:initialmark,concurrentmark,remark,concurrentsweeplongconcurrentmark和concurrentsweep阶段都可以和用户进程并发工作。但是由于CMS收集器是基于mark-and-sweep算法实现的,因此会产生大量的空间碎片。在为大对象分配内存时,经常会出现老年代还剩下很多空间,却找不到足够大的空间的情况。连续空间用于分配当前对象,因此必须提前启动FullGC。为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认开启),用于在CMS收集器中启动FullGC后内存碎片的合并排序过程。内存排序的过程不能并发,所以内存碎片的问题没有了,但是停顿时间要长一些。虚拟机设计器还提供了另一个参数-XX:CMSFullGCsBeforeCompaction参数,用于设置未压缩的FULLGC后跟压缩的次数(默认值为0,表示每次进入FullGC时进行碎片整理)。遗憾的是,作为老年代的收集器,无法与jdk1.4中已有的新生代收集器ParallelScavenge配合使用,所以在jdk1.5中使用cms收集老年代时,新生代只能选择其中之一ParNew或Serial收集器。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器后默认的新生代收集器,也可以使用-XX:+UseParNewGC选项强制指定。5.G1收集器G1收集器是服务端应用程序的垃圾收集器。HotSpot团队赋予它的使命是在未来替代JDK1.5发布的CMS收集器。与其他GC收集器相比,G1有以下特点:并行和并发:G1可以充分利用CPU,多核环境下的硬件优势可以缩短stoptheworld的停顿时间。分代收集:与其他收集器一样,分代的概念在G1中仍然存在,但是G1可以在没有其他垃圾收集器配合的情况下管理整个GC堆。空间整合:G1收集器有利于程序的长时间运行。在分配大对象时,不会因为无法获取连续空间而提前触发GC。可预测的非暂停:这是G1相对于CMS的另一大优势。减少暂停时间是G1和CMS共同关心的问题。它允许用户明确指定在M毫秒的时间段内,垃圾被消耗收集的时间不得超过N毫秒。使用G1收集器时,Java堆的内存布局与其他收集器有很大不同。它将Java堆划分为多个大小相等的独立区域。虽然仍然保留了新生代和老年代的概念,但是新生代和老年代之间不再是物理上的隔离。它们都是Region的一部分的集合(不一定是连续的)。虽然G1看起来优势很多,但实际上CMS依然是主流。GC相关的常用参数除了上面提到的一些参数外,下面增加一些GC相关的常用参数:-Xmx:设置堆内存的最大值。-Xms:设置堆内存的初始值。-Xmn:设置新生代的大小。-Xss:设置栈的大小。-PretenureSizeThreshold:直接提升到老年代的对象的大小。设置该参数后,大于该参数的对象将直接分配到老年代。-MaxTenuringThrehold:对象晋升到老年代的年龄。每个对象持久化一次MinorGC后,其年龄会加1,当超过这个参数值时,就会进入老年代。-UseAdaptiveSizePolicy:该模式下,新生代的大小、eden与survivor的比例、晋升到老年代的对象年龄等参数会自动调整,以达到堆大小、吞吐量、pause之间的平衡时间。在手动调优困难的情况下,可以直接使用这种自适应方式,只指定虚拟机的最大堆、目标吞吐量(GCTimeRatio)和暂停时间(MaxGCPauseMills),让虚拟机自己完成调优工作。-SurvivorRattio:新生代Eden区与Survivor区的容量比,默认为8,即Eden:Survivor=8:1。-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常它可以等于CPU的数量。但是在CPU数量较多的情况下,设置一个比较小的值也是合理的。-XX:MaxGCPauseMills:设置最大垃圾回收暂停时间。它的值是一个大于0的整数。收集器在工作的时候,会调整Java堆大小或者其他一些参数,尽可能的将停顿时间控制在MaxGCPauseMills范围内。-XX:GCTimeRatio:设置吞吐量大小,其值为0-100之间的整数。假设GCTimeRatio的值为n,那么系统在垃圾回收上花费的时间不会超过1/(1+n)。作者简介:mikechen,十余年BAT架构经验,资深技术专家,曾就职于阿里、淘宝、百度。关注个人公众号:Mikechen的互联网架构,十余年BAT架构经验!在公众号菜单栏对话框中回复【架构】关键字,即可查看我原创的300+BAT架构技术系列文章和1000+大厂面试问答合集。