简介
java堆在java虚拟机启动时创建,是java虚拟机所管理的内存中最大的一块,它是被所有线程共享的一块逻辑区域,在java虚拟机规范中,只要求其逻辑上是连续的即可,并不要求物理上的连续性(这可以结合操作系统内存管理的相关知识来理解)。java堆唯一的作用就是存储对象实例和数组。
从内存回收角度来看,现在的虚拟机大多数采用分代收集算法,因此,java堆还可以细分为新生代和老年代,更细致一点还可以分为Eden空间, From Survivor空间及To Survivor空间。
从内存分配角度来看,线程共享的Java堆还可能分出多个线程私有的分配缓冲区(TLAB)。
java堆与对象
- 对象创建过程
- 当虚拟机遇见一个new命令时,首先简称new指令的参数是否可以在常量池中定位到一个相应的类的符号引用,并检查这个类的符号引用所代表的类是否已经被加载,解析及初始化过,如果没有则必须执行相应的类加载过程,如果有执行2.
- 确定类已经被加载之后,虚拟机将为对象分配相应的内存空间,这个内存空间的大小在类加载后即可完全确定。
- 分配内存空间的方式可以分为”指针碰撞“和”空闲列表“两种,采用哪种方式由所采用的垃圾收集器是否带有”压缩整理功能“决定。
- 为保证在多线程情况下新对象创建的线程安全性,提供了两种方案:
- 第一种是对分配内存空间的动作进行同步处理(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性);
- 另一种是把内存分配的动作按照线程划分在不同的空间之中进行。每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。是否采用TLAB,可使用-XX:+/-UseTLAB参数来指定。
- 内存分配之后,虚拟机会将分配的内存空间初始化为零值。
- 接下来,虚拟机会根据当前的运行状态选择不同的设置方式,根据对象头,对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等。
- 对象内存布局
- 对象头: 对象头又可以拆分成两部分信息
- 存储对象自身的运行时数据(Mark Word): 哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
- 类型指针:即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。
- 当对象是数组时,对象头中还要存储一块用于记录数组长度的数据。
- 实例数据: 是对象真正存储的有效数据,也是程序代码段中定义的各种类型的字段内容。无论是父类,还是子类的数据都需要记录下来。这部分的存储顺序会收到虚拟机分配策略参数及字段在java代码中的定义顺序的影响。
- 对齐填充: HotSpot VM的自动内存管理系统要求对象的起始地址必须为8字节的整数倍,即对象的内存大小必须为8的倍数。
- 对象头: 对象头又可以拆分成两部分信息
- 对象的访问定位: 在java中,我们的程序在堆上存储对象实例,但是实际上我们要通过在虚拟机栈中的引用reference来操作这些对象。具体来说主流的访问方式有两种:
- 使用句柄:在内存中分出一块区域用于存放句柄池,reference中存储的值就是这块句柄池中对应的句柄地址,而句柄中存放对象的实例数据(在堆中)与类型数据(在方法区中)各自的具体地址信息。
- 直接指针:reference直接指向对象地址(堆中),然后在对象实例数据中包含指向对象类型数据(方法区中)的指针。(HotSpot 采用此种方式)
java堆与垃圾收集器
对于线程私有的虚拟机栈、本地方法栈及程序计数器所占用的内存来说,他们会随着线程的结束而被自动回收,而java堆因为是垃圾收集器的主要管理对象,常被称为GC堆。从内存管理角度来看,java堆又可以细分为新生代和老年代,进一步可以分为Eden, From Survivor 及 To Survivor。
- 判断对象是否存活的算法:
- 引用计数算法
- 算法思想:给对象添加一个引用计数器,每当有一个地方引用它,就将计数器加一;当引用失效计数器就减一。任何时刻当计数器为0时,就不可能再被引用。
- 优点:实现简单,判定效率也很高
- 缺点:主流的java虚拟机没有选用计数器算法的主要原因在于,它很难解决循环引用问题。
- 可达性计数算法: java, C#等语言所采用的判断对象是否存活的算法。
- 算法思想: 通过一系列称为”GC Roots"的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象没有任何引用链与GC roots相连时,则此对象是不可用对象,可被判定为可回收对象。
- 可作为GC Roots对象:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈中JNI引用的对象
- 引用:
- 强引用
- 定义:Object obj = new Object();
- 特点:只要强引用还在,垃圾回收器永远不会回收被引用的对象
- 软引用
- 定义:使用SoftReference类来实现软引用,软引用对象最常用于实现内存敏感的缓存。
- 特点:在系统将要发生内存溢出异常之前,会把软引用纳入回收范围中进行二次回收。
- 弱引用
- 定义:比软引用更弱,WeakReference来实现,弱引用最常用于实现规范化的映射。可以使用弱引用的isEnQueued方法返回对象监控对象是否已经被垃圾回收器标记为即将回收的垃圾。
- 特点:被弱引用关联的对象,只能生存到下一次垃圾收集发生之前。
- 虚引用
- 定义:也称为幽灵引用或幻影引用,是最弱的一种引用关系。PhantomReference来实现。主要用于检测对象是否已经从内存中删除。
- 特点:一个对象时候有虚引用的存在,完全不会对其生存时间产生任何影响,也无法通过虚引用来取得一个对象实例。
- 强引用
- 引用计数算法
- 垃圾收集算法:
- 标记-清除算法:
- 复制算法: 目前商业虚拟机采用这种方法来回收新生代。
- 主要思想:在内存中按容量分为大小相等的两块,每次值使用其中一块,当这一块的内存用完后,将还存活的对象复制到另一块,之后清空已使用过的这一块内存。因为98%的对象都是创建之后很快失效,因此不需要两块相等比例的内存,进而将内存分为一块较大的Eden区,和两块较小的Survivor区,每次使用Eden和一块Survivor。当回收时将Eden和该块Survivor的存活的对象复制到另一块Survivor,之后清空Eden和上一块Survivor。(Hotspot默认Eden:Survivor = 8 : 1)。
- 标记-整理算法: 复制算法在对象存活比例较高时效率将会降低,因此回收老年代所采用的算法是标记-整理算法。
- 分代收集算法: 当前商业虚拟机的垃圾回收器都是采用“分代收集”
- 根据对象存活周期的不同将内存划分为几块,一般是把java堆分为新生代和老年代,这样就可以根据每个年代不同的特点选择适当的收集方法。
- 总结:目前虚拟机均采用分代收集算法进行垃圾收集。将java对分为新生代和老年代。在新生代中,每次垃圾收集时都有大批对象已经死去,只有少量存活,故而选择复制算法进行垃圾收集;而在老年代,因为对象存活率高,没有额外空间对其进行分配担保,必须使用“标记-清理”或者“标记-整理”进行回收。
- 垃圾收集器:HotSpot(有连线表示可搭配)
- Serial收集器:是一个单线程收集器,在它进行垃圾回收时,必须暂停掉所有线程。
- 缺点:由虚拟机自动发起,在用户不可见的情况下停掉所有工作
- 优点:简单高效,适合运行在Client模式下的虚拟机。
- ParNew收集器: Serial的多线程版本。因为它是除了Serial唯一能与CMS配合使用的收集器,是许多运行在Server模式下的虚拟机首选的新生代收集器。
- Parallel Scavenge收集器: 也是一个多线程的新生代的收集器,但是它的关注点在于达到一个可控制的吞吐量,可以自适应调节。
- Serial Old收集器:Serial的收集老年代的版本。
- Parrallel Old收集器: parallel Scavenge 的老年代版本。在注意吞吐量和CPU资源敏感的场合,均可以考虑采用Parallel Scavenge 加上Parallel Old的组合
- Cms收集器: 是一种以获取最短回收停顿时间为目标的收集器,非常适合 应用集中在互联网网站或者B/S系统的服务端上的 这些尤其注重响应时间的应用。基于“标记-清除”算法,大致可分为四个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
- G1收集器: 是一款面向服务端应用的垃圾收集器。HotSpot希望它在未来能够完全替换掉CMS。它有着如下特点:并行与并发,分代收集,空间整合,可预测停顿。
- 使用G1收集器时,java堆内存的布局有很大变化,它将整个java堆内存分为多个大小相等的独立区域(Region),虽然仍旧有新生代和老年代的概念,但是两者不再物理隔离,它们都是一部分Region的集合。
- Serial收集器:是一个单线程收集器,在它进行垃圾回收时,必须暂停掉所有线程。
- 内存分配与回收
上文涉及的重要补充知识点
常量池:
java引用:
- 软引用SoftReference:(WeakReference与PhantomReference用法与之类似,详细的可以看看api)
- 此类的直接实例可用于实现简单缓存;该类或其派生的子类还可用于更大型的数据结构,以实现更复杂的缓存。只要软引用的指示对象是强可到达对象,即正在实际使用的对象,就不会清除软引用。例如,通过保持最近使用的项的强指示对象,并由垃圾回收器决定是否放弃剩余的项,复杂的缓存可以防止放弃最近使用的项。
-
Solution referent = new Solution(); SoftReference<Solution> s = new SoftReference<Solution>(referent); referent = null; s.get();
- 优秀的相关博客
Eden空间, From Survivor空间及To Survivor空间: