首页 > 代码库 > JVM-内存分配与垃圾回收

JVM-内存分配与垃圾回收

内存分配

整个内存: 堆内存(年轻代大小 + 年老代大小)+ 非堆(持久代)。

1、堆参数:

-Xms:初始内存,默认是物理内存的1/64。

-Xmx:最大内存,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。 

-Xmn:指定年轻代: 包括Eden和两个Survivor区。

-XX:NewRatio:年轻代(-Xmn)与年老代的比值(除去持久代),默认值4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5,Xms=Xmx并且设置了Xmn的情况下,不需要设置-XX:NewRatio。

2、非堆就是JVM留给自己用的,所以方法区(虚拟机规范未规定此区域的具体数据结构,由虚拟机厂商自行实现)、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中,是各个线程共享的内存区域。

非堆(持久代)内存分配 

-XX:PermSize:初始值,默认是物理内存的1/64;

-XX:MaxPermSize:最大值,默认是物理内存的1/4。

技术分享

回收算法:

1、标记清除:会产生大量不连续的内存碎片,分配较大对象时,无法找到足够的连续内存,触发一次垃圾收集。

2、复制:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

新生代回收采用复制算法,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间,所以Survivor区总有一个是空的。如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,需要依赖其他内存(这里指老年代)进行分配担保。

涉及到的参数:-XX:SurvivorRatio默认为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。

3、标记-整理算法:老年代的特点,标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4、分代:在新生代中,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

垃圾收集器:

对回收算法的实现,暂略过。

内存分配方式:

1、指针碰撞:如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。

2、空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

内存分配方式选择:

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

内存分配的并发控制:

对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

主要通过两种方式解决:1.CAS加上失败重试分配内存地址。2. TLAB 为每个线程分配一块缓冲区域进行对象分配,new对象内存的分配均需要进行加锁,这也是new开销比较大的原因,所以Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB,TLAB仅作用于新生代的Eden,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。使用-XX:+/-UseTLAB

内存分配完成后操作:

虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,<init>方法还没有执行,所有的字段都还为零。执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象头:

1、官方称它为“Mark Word”,包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。

2、这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit。 

3、如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。

4、对象需要存储的运行时数据很多,其实已经超出了32位、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit空间中的25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,而在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)复用其它空间。

对象实例数据:

1、实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

2、无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

3、这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。

4、由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

Hotspot发起内存回收过程:

根节点GC Roots:

1、栈帧中的本地变量表中引用的对象。

2、方法区中类static属性、final常量引用的对象。

3、本地方法栈中JNI(即一般说的Native方法)引用的对象。

安全点:

1、安全点的选定基本上是以程序长时间执行,最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。

2、在安全点生成OopMap的数据结构:HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样,GC在扫描时就可以直接得知这些信息了。

3、GC发生时让所有线程(这里不包括执行JNI调用的线程)跑到最近的安全点,枚举根节点,已经提前在安全点生成OopMap的数据结构(可以快速且准确地完成GCRoots枚举),可达性分析,必须在一个能确保一致性的快照中进行,不可以出现分析过程中对象引用关系还在不断变化的情况。

安全区域:

指在一段代码之中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。

在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

GC中断:

1、抢先式中断:不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。

2、主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的。


经过JIT编译后被拆散为标量类型并间接地栈上分配,启动了本地线程分配缓冲,将按线程优先在TLAB上分配, 少数情况下也可能会直接分配在老年代中。

垃圾回收:

1、弱引用:当垃圾收集器工作时,被弱引用关联的对象总是被无条件回收。

2、虚引用:无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

3、finalize:

执行finalize()方法,对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行

方法区回收:永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类

无用的类包括:

1、该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

2、加载该类的ClassLoader已经被回收。

3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

4、大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

新生代回收:

1、动态对象年龄判定:虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

2、在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁

3、JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。不再关心HandlePromotionFailure的设置。

本文出自 “浅谈技术” 博客,谢绝转载!

JVM-内存分配与垃圾回收