JVM的垃圾回收与内存分配

简介:

   Java是一种内存动态分配和垃圾回收技术的一种语言,不需要显示的进行对象内存的分配,这一切操作都是由JVM来完成的,由于Java是“一切皆对象”的,所以对于内存分配的优化与速度非常的高效。在Java中一个对象在堆中的分配以及灭亡都是由JVM来完成的。JVM负责来垃圾回收与对象分配。

一 垃圾回收

   垃圾回收(Garbage Collection,GC),研究这个主要目的就是为了提升JVM的性能,在内存泄露中能及时查缺问题所在。对于垃圾回收,GC必须要解决的问题包括三个:

  1)哪些内存可以回收哪些对象可以回收? 这里主要就是判断哪些对象的死活

  2)什么时候回收? 一般就是当内存不够或者设置垃圾收集器的时间间隔

  3)如何回收? 对于已经判断为死的对象的回收算法以及实现这些算法的垃圾收集器

哪些内存和对象可以回收?

   JVM中的五大内存区域中,程序计数器、虚拟机栈和本地方法栈都是随着线程而生,随着线程而灭亡。栈中的大小基本上在类结构确定下来的时候就已知了,这三个区域内存分配与回收都具备确定性,所以当方法结束或者线程结束时候,这三块的内存就随着回收了。

   而Java堆和方法区中,存放了与类有关的信息以及类的实例对象,这些对象只会在具体运行期间才能创建,而这些对象创建与回收都是动态的,故垃圾回收需要考虑堆和方法区中的回收

   明确了需要回收哪一块的内存后,就需要再次解决哪些对象可以回收?

   垃圾回收的对象,这里主要回收的就是那些已经死去(即不再被任何途径使用的对象)。既然知道要回收这些死的对象,那么接下来就要确定怎么来判断一个对象的死活。

   在Java中使用的就是根搜索算法(GC Roots Tracing)来判断对象是否存活,而不是利用引用计数。

   根搜索算法的基本思想:通过一系列名为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所经过的路径称为引用链,当一个对象到GC Roots没有引用链的时候,则可初次判定该对象不可用。图论中表示就是从GC Roots到这个对象路径不可达。

   Java中可以作为GC Roots的对象有虚拟机栈中的引用对象,方法区中的类静态属性引用的对象,方法区中的常量引用的对象,本地方法区中Native的引用的对象。

垃圾回收的起点?(基本思想的详解)

   栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从Java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。

因此,垃圾回收的起点是一些根对象(java栈, 静态变量, 寄存器...)

   Java利用根搜索算法要经历两次标记过程来宣告一个对象死活:

   第一次标记并筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过一次(因该方法只会被调用一次),虚拟机则判定对象没有必要执行finalize()。如果有必要执行,将该对象放入F-Queue队列,稍后由虚拟机自动创建优先级低的Finalizer线程。第二次标记就发生在F-Queue,如果在Finalizer线程执行时F-Queue中的对象与引用链上的对象建立了关联,第二次标记时会将该对象移出F-Queue,队列中剩下的经过两次标记的对象就是可以回收的。

什么时候回收? 

    在Java中垃圾回收器启动的时间是不固定的,它根据内存的使用量而进行动态的自适应调整,来运行GC,在为内存分配的过程中就会有GC的产生过程。

如何回收

确定了哪些对象以及内存需要回收后,此时就需要考虑采用什么样的策略以及用什么来具体实现这些策略。

回收的算法:

   垃圾收集算法都是先用“根搜索算法”来判断哪些需要回收的,然后进行垃圾的回收处理,常用的垃圾收集算法的基本概况:

标记-清除算法(Mark-Sweep)这种算法主要分为两个阶段,“标记”和“清除”,标记的过程就是采用的“根搜索算法”,首先标记处所有被引用的对象,在标记阶段完成后,遍历整个堆,对于未被被标记的可回收的对象进行统一的回收掉优点:MS收集器可以在存储耗尽的时候启动,实现起来简单容易。缺点:工作的时候需要使得工作例程挂起等待很长时间,效率不高,会产生大量的不连续的内存碎片,就会导致一些比较大的对象无法找到足够连续内存从而提前出发另一次垃圾收集动作。

复制算法(Copying):这是为了解决效率问题而出现的,主要用于堆中新生代的回收。它将可用内存按容量大小划分为大小相等的两块,每次只使用其中一块,当这一块完了后,就将还存活的对象复制到另一块上面,然后把刚已使用的内存空间一次清除掉。优点:提高了回收效率,回收后不会产生不连续的空间,工作开销正比于存活的对象。缺点:将可用内存缩小为原来的一般,当对象存活率较高时候,就要执行较多的复制操作,效率就会降低。

在新生代采用该算法的思路该算法用于新生代中,在现在一般并不是将内存划分为等大的两块,由于新生代的对象大多是朝生夕死的,在新生代中将内存划分为一个较大Eden空间和两块较小的Survivor,每次仅仅使用Eden和其中一个Survivor,当回收时,将Eden和Survivor还存活的对象一次性拷贝到另一块的Survivor中,最后清理掉Eden和刚才用过的Survivor空间。当要拷贝至Survivor空间不够容纳还存活的对象时候,此时就需要用老年代来进行分配担保,即将这些存活的对象拷贝至老年代中。

标记-整理算法(Mark-Compact):主要用于堆中的老年代回收。它依旧采用“标记-清理”中的“标记”方法,当“标记”阶段完成中,它是将对于存活的对象即被引用的对象进行标记,在“整理”阶段中,将标记完成的对象进行移动,使之与相邻的活动对象连续分配,从而将所有存活的对象都移向了堆的一端,然后直接清理掉堆端边界以外的所有内存。“标记-整理算法”克服在对象存活率较高中出现频繁的复制操作,并且解决回收后的内存出现不连续的空间,它是“标记清理”和“复制”的有机结合。

分代收集算法(Generational Collecting):当前垃圾收集都是采用这种算法。主要将Java堆分为年轻代和老年代,根据不同的年代采用不同的算法。在新生代中,由于只有少量的存活对象,此时就使用“复制”算法;在老年代中,由于存活对象比较长没有额外空间进行分配担保,就使用“标记-整理”或“标记-清理”算法。之所以要进行分代的原因是由于不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

  JVM中分代的模型如下:

回收的具体实现:

  垃圾收集器就是具体实现这些垃圾收集算法的。在HotSpot中主要包括如下:

年轻代垃圾收集器:Young generation,在垃圾收集的过程中都会使得用户线程等待。

    Serial收集器:一种单线程的收集器,采用“复制”收集算法,收集时候暂停所有的工作线程,直到收集结束,一般虚拟机在Client模式下的默认新生代收集器就是采用这个收集器。优点:与单线程收集器比较简单高效。对于单个CPU下,由于没有多个线程的交互开销,在堆比较小的时候,一般停顿比较短,可以采用。

    ParNew收集器:是Serial的一种多线程收集器,采用“复制”收集算法,它和Serial收集器除了多线程外其余行为都相同,即在收集的过程中会暂停所有的线程。它是运行在Server模式下的新生代收集器的首选。可以使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,或者使用-XX:+UseParNewGC选项来强制使用它。只能与CMS收集器配合使用

    Parallel Scavenge收集器:是一种并行(多条垃圾收集线程并行工作,用户线程依旧等待)多线程收集器,采用“复制”算法来收集新生代。它所关注的是达到一个控制的吞吐量,就是CPU运行用户代码时间与CPU总耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。利用-XX:MaxGCPauseMillis可设置垃圾收集停顿时间-XX:GCTimeRatio可设置垃圾收集占的总时间,是吞吐量的倒数。如果为19,则允许GC时间为5%(1/(1+19));这种收集器也有自适应调节策略。


老年代垃圾收集器:Tenured generation

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

主要是虚拟机在Client模式下的使用的收集器。它在工作中依旧需要暂停所有的用户线程主要用在作为CMS收集器后备预案使用。

   Parallel Old收集器:是一种多线程收集器,是Parallel Scavenge的老年代版本,使用的是“标记-整理”算法。它在工作中依旧需要暂停所有的用户线程。一般是结合Parallel Scavenge来一起使用,用于在注重吞吐量和CPU资源敏感的场合

   CMS收集器(Concurrent Mark Sweep):采用“标记-清除”的算法,目标是获取最短回收停顿时间的。整个过程分为4个步骤:

   1 初始标记(Stop the world)  2 并发标记

   3 重新标记(Stop the world)  4 并发清除

  初始标记,仅仅标记一下GC Roots能关联到的对象,速度非常快,需要停止用户线程。

  并发标记,进行GC Roots Tracing过程,可以与用户线程一起工作,不需要停止用户线程。

  重新标记,为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象,这个时间也是很短的,需要停止用户线程。

  并发清除,可以与用户线程一起工作,不需要停止用户线程。

   在CMS中,耗时最长的是并发标记和并发清除,在这两个过程中收集器线程都可以与用户一起工作,所以CMS收集器的内存回收过程是与用户线程一起并发地执行。

   缺点:CMS对CPU资源非常敏感,无法处理浮动垃圾,会产生碎片。

G1收集器:采用的是“标记-整理”算法,可以非常精确的控制停顿,可以实现在基本上不牺牲吞吐量的前提下完成低停顿的内存回收。

垃圾收集器中的并发与并行:

并行(Parallel):多条垃圾收集线程并行工作,此时用户线程处于等待停止状态

并发(Concurrent):用户线程与垃圾收集线程同时执行,即用户程序继续运行,而垃圾收集程序运行在另一个CPU中。


二 内存分配

  对象的内存分配,就是在Java堆上分配的,对象主要分配在堆中的新生代的Eden区。

  内存分配的几个原则:

  对象优先在Eden分配:大多数情况下,对于一个新的对象将会首先分配在新生代的Eden区,只有Eden区没有足够的空间进行分配的时候,虚拟机发起一次Minor GC,可以使用-verbose:gc -XX:+PrintGCDetails来打印内存分配的状态。当分配的对象无法容纳在Eden区的时候,首先会将Eden中存活的对象复制到另一个Survivor中,如果Survivor无法容纳这些存活的对象,则只有通过分配担保机制将这些存活对象提前移动到老年代中,然后将要分配的对象分配到Eden区中。

  大对象直接进入老年代:大对象就是需要连续的大量内存空间,最典型的就是字符串或者数组。可以设置-XX:PretenureSizeThreshold参数,当大于这个值的对象直接会在老年代中分配,避免了在Eden区和两个Survivor之间大量的拷贝。

  长期存活的对象将进入老年代:虚拟机为每个对象定义了对象年龄,当对象在Eden出生经过第一个Minor GC还存活着,并且能被Survivor容纳,则移动到Survivor,年龄加1.每次熬过一次Minor GC,对象年龄就会加1.对象晋升到老年代的年龄阀值,可以通过设置

-XX:MaxTenuringThreshold

  动态对象年龄判定:不一定非得达到年龄阀值才会进入老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象就会直接进入老年代,无需等待MaxTenuringThreshold的阀值年龄

  空间分配担保:在发生Minor GC时候,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则直接对于老年代进行一个Full GC。如果小于,则查看HandlePermotionFailure设置是否允许进行担保失败,如果允许,则在新生代进行Minor GC;如果不允许,则在老年代进行Full GC。

注意:Minor GC和Full GC的区别

  新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

  老年代 GC(Major GC  / Full GC):指发生在老年代的 GC,出现了Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。MajorGC的速度一般会比Minor GC慢10倍以上。Major GC会触发整个heap的回收,包括回收young generation。



本文转自 zhao_xiao_long 51CTO博客,原文链接:http://blog.51cto.com/computerdragon/1220373


相关文章
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
1035 55
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
1043 6
|
8月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
559 5
|
8月前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
772 29
JVM简介—1.Java内存区域
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
缓存 算法 Java
JVM深入原理(八)(一):垃圾回收
弱引用-作用:JVM中使用WeakReference对象来实现软引用,一般在ThreadLocal中,当进行垃圾回收时,被弱引用对象引用的对象就直接被回收.软引用-作用:JVM中使用SoftReference对象来实现软引用,一般在缓存中使用,当程序内存不足时,被引用的对象就会被回收.强引用-作用:可达性算法描述的根对象引用普通对象的引用,指的就是强引用,只要有这层关系存在,被引用的对象就会不被垃圾回收。引用计数法-缺点:如果两个对象循环引用,而又没有其他的对象来引用它们,这样就造成垃圾堆积。
308 0
|
算法 Java 对象存储
JVM深入原理(八)(二):垃圾回收
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为StopTheWorld简称STW,如果STW时间过长则会影响用户的使用。一般来说,堆内存越大,最大STW就越长,想减少最大STW,就会减少吞吐量,不同的GC算法适用于不同的场景。分代回收算法将整个堆中的区域划分为新生代和老年代。--超过新生代大小的大对象会直接晋升到老年代。
308 0
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
2774 1
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
1360 166