自动内存管理机制
JVM内存分为:
1.方法区:线程共享的区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
2.堆:线程共享的区域,存储对象实例,以及给数组分配的内存区域也在这里。
3.虚拟机栈:线程隔离的区域,每个线程都有自己的虚拟机栈,生命周期和线程相同。虚拟机栈描述方法执行的内存模型,以站栈帧为单位,每个栈帧存储和方法运行有关的局部变量表、操作数栈、动态链接、方法返回地址等信息。
4.程序计数器:线程隔离的区域,每个线程都有自己的程序计数器,存储程序当前执行的字节码的行号。
5.本地方法栈:线程隔离,和虚拟机栈类似,是虚拟机调用Native方法时使用的。
堆的分区,以及各个分区的特点:
Java堆是垃圾收集器管理的主要区域,按照分代收集算法的划分,堆内存空间可以继续细分为年轻代,老年代。年轻代又可以划分为较大的Eden区,两个同等大小的From Survivor,To Survivor区。默认的Eden区和Survivor区的大小比例为8:1:1,这个比例可以调节。在为新创建的对象分配内存的时候先将对象分配到Eden区和From Survivor区,在立即回收时,会将Eden区和Survivor区还存活的对象复制到To Survivor区中,如果To Survivor区的大小不能容纳存活的对象,会把存活的对象分配到老年区。总体来说,新创建的小对象会放在年轻代,年轻代的对象大多在下一次垃圾回收时被回收,老年代存储大的对象和存活时间长的对象。
1.局部变量表所需的内存空间在编译期间完成
2.每当java程序启动一个新的线程时,java虚拟机会为他分配一个栈,java栈以帧为单位保持线程运行状态;当线程调用一个方法是,jvm压入一个新的栈帧到这个线程的栈中,只要这个方法还没返回,这个栈帧就存在。 如果方法的嵌套调用层次太多(如递归调用),随着java栈中的帧的增多,最终导致这个线程的栈中的所有栈帧的大小的总和大于-Xss设置的值,而产生生StackOverflowError溢出异常。
3.在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
5.在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作
6.在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
7.对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
8.除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能
-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。
栈容量只由-Xss参数设定
在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
操作系统分配给每个进程的内存是有限制的,譬如32位的Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜
每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽
Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
Java的四种引用类型及特点:
1.强引用:程序中普遍存在的,类似“String s=”hello wold””这类的引用,强引用的对象不会被回收。
2.软引用:有用但是非必须的对象在系统将要发生内存溢出之前会对软引用的对象进行垃圾回收,SoftReference类实现软引用。
3.弱引用:非必须的对象,被弱引用关联的对象只能存活到下一次垃圾收集发生之前。
4.虚引用:最弱的引用关系,不能通过虚引用取得对象的实例,为对象设置虚引用的唯一目的就是在这个对象被收集器回收时收到一个系统通知。
四种引用强度依次减弱,强软弱虚。
对象死亡两次标记过程:
1,没有与gc roots的引用链,被标记第一次
2,通过是否有必要执行finalize()方法进行筛选。(当对象finalize方法没被覆盖或者没被调用过会被判断为没必要执行)。
3,若有必要执行,放入队列,由Finalizer线程执行(异步执行,防止一个对象执行时间过长,导致队列阻塞甚至回收系统崩溃)。
4,执行finalize方法,可以自救,比如将自身引用赋值给另一个对象,此方法只能执行一次。可达性分析(有无与gc roots引用链),被标记第二次后被回收or移出回收集合
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行
忘掉这个方法的存在,历史遗留原因。
finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时,所以笔者建议大家完全可以忘掉Java语言中有这个方法的存在。
标记-清除算法:
1,定义:先标记需回收对象,再统一回收被标记对象。
2,缺点:标记和清除效率低。/清除后产生碎片化内存
3.3.1 标记-清除算法 最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实在前一节讲述对象标记判定时已经介绍过了。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
Eden读iden音,Eden区域是单块survivor的16倍,为什么会将eden区域从一块survivor复制到另一块?空间够吗?其实新生代里的对象都是生命周期较短的,因此在GC时只复制存活的,重要的是,空间不够时通过老年代的担保分配将新生代符合存活周期要求的对象放入老年代。
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[插图]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90% (80%+10%),只有10%的内存会被“浪费”。
GC的三种收集算法的原理和特点,用途,优化思路
三种垃圾收集算法:复制算法,标记-清除算法、标记-整理算法
标记-清除算法:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。缺点:标记和清除两个过程效率都不高;标记清楚后会产生空间碎片,空间碎片导致分配较大对象时可能提前出发垃圾回收。
复制算法:将可用内存分为两个区域,每次只使用其中一块,当使用的那一块内存用完时,将还存活的对象复制到另外一块内存中,然后把已使用过的内存空间一次清理掉。优点:解决的空间碎片问题,实现简单。缺点:将内存缩小为两块,内存使用率不高。复制操作频繁效率变低。
标记-整理算法:可回收对象标记后,让所有存活的对象向一端移动,然后清理掉边界以外的内存。优点:不会产生空间碎片,比复制算法提高了内存空间利用率。
复制算法用在年轻代的垃圾回收中,标记整理和标记清除算法用在老年代垃圾回收的收集器中。
GC收集器有哪些?CMS和G1收集器的特点
GC收集器按照回收区域不同,新生代有Serial,Parnew,Paralell Scanvage,老年代有Serial Old,CMS,Parallel old,还有新生代老年代通用的G1;
Serial 和Serial old是早期jdk中发布的垃圾收集器,特点是都为单线程,新生代采用复制算法,老年代采用标记整理算法,两个垃圾收集器在工作的时候必须要停掉所有的用户线程,直到收集完成后才能回复用户线程,由于是单线程工作方式,没有线程交互的开销所以能够活的最高的单线程收集效率,使用在client模式下的虚拟机。
ParNew收集器是Serial收集器的多线程版本,是年轻代的垃圾收集器,可以和Serial old以及CMS老年代收集器搭配使用。Parnew在单CPU环境中的性能没有Serial好,因为单CPU环境下的多线程按照时间顺序串行执行,还要承担线程间交互的额外开销,不过在多cpu环境下,Parnew的性能就会好很多,是运行在server模式下的虚拟机首选的新生代收集器。
在jdk1.4时新推出的垃圾收集器是Parallel Scanvage 和对应的Parallel Old,新生代基于复制算法,老年代基于标记整理算法.Parallel Scanvage也是并行性的多线程收集器,它和Parnew 的区别在于两者的关注点不同。Parnew关注于减少垃圾回收时用户线程停顿的时间,而Parllel Scanvage 关注点事获得最大的吞吐量,也就是CPU运行用户代码与CPU总消耗时间的比值。停顿时间短适合于和用户有交互的程序,吞吐量高则可以高效的利用CPU时间,尽快完成运算任务,主要是和在后台运算不需要太多的交互任务。
Jdk1.5时推出了能够和用户线程并发执行的CMS收集器,CMS是老年代垃圾收集器。CMS是一种以获取最短回收停顿时间为目标的收集器,基于标记清除算法来实现。它的工作过程先后分为初始标记、并发标记、重新标记、并发清除四个步骤,其中初始标记和重新标记是需要停顿用户线程的,并发标记和并发清理过程是可以和用户线程并发执行的,在整体垃圾收集时间里,初始标记和重新标记所占的时间很少,重新标记阶段又是可以多个垃圾回收线程并行执行的,所以整体用户线程停顿的时间很短。CMS的缺点:对CPU资源敏感,CMS默认启动的垃圾回收线程数为(CPU数量+3)/4,在并发阶段由于占用用户线程导致应用变慢,cpu不足4个时候对用户程序影响很大;CMS无法处理在并发清理阶段新产生的垃圾,只有等下一次垃圾回收标记后才能清除;CMS基于标记清除算法会产生空间碎片,CMS的解决方式是在进行Full GC时开启内存整理,这一过程无法并发,延长了用户线程的停顿时间。
G1收集器是在jdk1.7时推出的用于新生代和老年代的垃圾收集器,面向server模式。G1把内存区域划分成多个大小相同的独立区域,G1跟踪每个区域里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域,这种收集策略可以在有限时间内获取尽可能高的收集效率。G1垃圾回收过程:初始标记(单线程,停顿)、并发标记(单线程,并发)、最终标记(多线程,并行,停顿)、筛选回收(多线程,并行,停顿)。