Java内存分配与垃圾回收
1.JVM管理的内存包含下图所示的几个运行时数据区域,其中方法区和堆为线程共享的数据区域,程序计数器,虚拟机栈以及本地方法栈为线程私有的数据区域。
程序计数器:可以看做是当前线程所执行的字节码的行号指示器,告诉字节码解释器该读取哪条指令
虚拟机栈:生命周期和线程相同,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用到完成的过程就对应了一个栈帧在虚拟机中入栈到出栈的过程。栈中存放了编译器可知的各种基本数据类型和对象引用。
本地方法栈:与虚拟机栈十分类似,区别在于本地方法栈是为Native方法服务。
堆:堆中存放对象实例。几乎 所有的对象实例都在这里分配。堆是垃圾回收器管理的主要区域。从内存回收的角度来看,由于现有垃圾回收器基本都采用分代收集算法,所以堆可以细分为新生代和老年代,再细致一点可以分为:Eden空间,From Survivor 空间,To Survivor空间。
方法区:用于存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。(HotSpot虚拟机的将GC分代收集扩展至方法区,使用永久代来实现方法区。因此存在把方法区看成永久代)。
运行时常量池:方法区的一部分,用于存放编译器生成的各种字面量和符号引用。
2.对于字符串,其对象的引用存储在栈中,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中 。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。对于通过new产生一个字符串时,会先去常量池中查找是否已经有了此对象,如果没有则在常量池中创建一个此字符串对象,然后堆中再创建一个常量池中此对象的拷贝对象。例如:String s = new String(“xyz”);如果常量池中原来没有”xyz”,就会产生两个对象。
3.如何判断对象已死:
(1)引用计数法:给每个对象添加一个引用计数器,有引用计数器+1,引用失效计数器-1,计数器为0则对象不可用。缺点是无法解决循环引用的问题。
(2)可达性分析法:当一个对象到GC Roots没有任何引用链相连(类似树的叶子节点到根节点之间断开连接),则此对象不可用。
4.即使在可达性分析算法中不可达的对象也不是非死不可的,这时候他们暂时处于'缓刑'阶段,要真正宣告一个对象的死亡至少要经历两次标记过程:
如果对象在可达性分析时发现不可达,将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法(当对象没有重写父类的 finalize 方法或者 finalize 方法已经被调用过,虚拟机就认为没必要执行finalize方法,值得注意的是,对象的finalize方法只会被调用一次),如果这个对象被判定为有必要执行 finalize 方法,那么这个对象将会被放置到一个叫做F-Queue 的队列中,并在稍后由一个虚拟机自动建立的,低优先级的finalizer线程去执行它(虚拟机出发finalize方法,但不一定会等待它执行完成)。稍后GC将对 F-Queue 中的对象进行第二次小规模的标记,如果对象仍被标记为回收(如果对象在第二次标记之前与其他可达对象建立了引用关系,则可以'逃过一劫'),则该对象基本上就真的被回收了。
5.垃圾回收一般发生在堆上,然而方法区也可能发生垃圾回收。在堆中,尤其是新生代中,常规应用进行一次垃圾收集一般可以回收 70%~ 95%的空间,而永久代垃圾收集效率远低于此。永久代的垃圾收集主要收集主要回收两部分内容:废弃常量或者无用的类。判断常量不可用较简单(没有任何对象引用常量池中的这个常量),而判定一个类是无用的类则条件严苛的多:
(1).该类的所有实例均被回收
(2).加载该类的 ClassLoader 已经被回收
(1).该类对应的 java.lang.Class 对象没有在任何地方被使用,无法再任何地方通过反射访问该类的方法。
6.垃圾回收算法:
标记-清除算法:分为标记,清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。此算法有两个不足:一是两个过程的效率都不高,二是标记清除后会产生大量不连续的内存碎片(当需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作)。
复制算法:将可用内存分为大小相等的两块,每次只使用其中的一块,当一块的内存用完了就把存活的对象复制到另一块,然后将已使用的内存空间一次清理掉。这种算法简单高效,但是不足在于将内存缩小了一般。
标记-整理算法:标记过程和标记-清除算法一样,但是后续不是直接清除内存,而是让存活的对象都向一边移动,然后清除掉边界意外的内存。
分代收集算法:根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代。现在的商业虚拟机普遍使用复制算法回收新生代,由于新生代的对象绝大部分都是“朝生夕死”的,所以并不需要按1:1的比例划分内存空间,而是分为较大的Eden区和两个较小的Survivor区,每次使用Eden和一块Survivor,GC时将存活的对象复制到另外一块Survivor(From Survivor),最后清理掉刚才使用的Eden和Survivor(To Survivor).HotSpot虚拟机默认Eden和Survivor比例为8:1,也就是每次新生代中可用的内存空间为整个新生代的90%(80%+10%)。当Survivor(To Survivor)内存不够用时,需要依赖其他内存进行分配担保(老年代)。复制算法在对象存活率较高时效率低下而且如果不想浪费50%内存还需要额外的空间惊醒分配担保,因此不适合老年代这种存在大量存活对象的区域,所以一般使用标记-整理算法回收老年代。
7.内存分配与回收策略:
1.Java 体系中所提倡的自动内存管理最终可以归结为两个问题:给对象分配内存以及回收分配给对象的内存。对象主要分配在新生代的Eden区上,少数情况下也可能直接分配在老年代中。
(1)大对数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC.
(2).大对象直接进入老年代,所谓的大对象是指需要大量连续内存空间的对象,最典型的大对象就是那种很长的字符串以及数组
(3)长期存活的对象将进入老年代。如果对象在Eden出生经过第一次 Minor GC 后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,对象每熬过一次 Minor GC,年龄就增加1岁,当他的年龄增加到一定程度(默认15岁)就会晋升到老年代中。虚拟机并不是永远要求对象的年龄达到 Max Tenuring Threshhold 才能晋升老年代,如果在Survivor空间中相同年龄所有对象的大小的总和等于Survivor空间的一般,年龄大于等于该年龄的对象就可以直接进入老年代,无需等到Max Tenuring Threshhold 中要求的年龄。
(4)在发生 Minor GC 之前,虚拟机会先检查老年代最带可用的连续空间是是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。