Java创建对象的过程
①类加载检查 → \rightarrow →②分配内存 → \rightarrow →③初始化零值 → \rightarrow →④设置对象头 → \rightarrow →⑤执行init()方法
内存分配的两种方式:指针碰撞、空闲列表。
堆内存对象分配的基本策略
堆空间的基本结构:
上图所示的eden区、s0区、s1区都属于新生代,tentired区属于老年代。大部分情况,对象都首先在eden区分配,在一次新生代垃圾回收之后,如果对象依然存活,则会进入s0或者s1,并且对象年龄还会加1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代,这个阈值可以通过参数-XX:MaxTenuringThreshold
来设置(只能低于15,因为使用markword的四位来表示,四位二进制最多表示15)。
另外大对象和长期存活的对象会直接进入老年代。
S1和S2区大小相同,执行复制算法进行垃圾回收;老年代约占2/3内存大小,执行标记清除或者标记整理算法进行垃圾回收。关于垃圾回收算法,下面有详细说明。
Java堆不是数据结构意义上的堆(一种有序的树),而是JVM的堆,即运行时数据区。所有类的实例和数组都是在堆上分配内存,它在JVM启动时被创建,对象所占的内存是由自动内存管理系统也就是垃圾回收器回收。
堆内存是由存活的对象以及死亡的对象组成的。存活的对象不会被垃圾回收器回收;死亡的对象是应用不可访问而且还没有被垃圾回收期回收的对象,等下一个周期回收。
除了上述的新生代(Young Generation)、老年代(Old Generation)、还有永久代(Perm Gen)。
永久代用于存放类定义、字节码和常量等很少会变更的信息。永久代对垃圾回收没有影响,如果永久代满了或超过了临界值,会触发完全回收。
JDK1.8中,永久代已经从java堆中移除,String直接存放在堆中,类的元数据存储在meta space
中,不占用堆内存。可以说,Java8中,持久代已经更名为了元空间(meta space)。
Minor GC和Full GC有什么不同
大多数情况下,对象在新生代中eden区分配,当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。
如何判断对象是否死亡
- 引用计数法
- 可达性分析法(RA) (引用链法)
引用计数法
每当一个地方引用它,计数器加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不能再使用的对象。
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用对象A,那么此时A、B对象的引用计数器都不为零,也就无法无法完成垃圾回收,因此主流的虚拟机都没有采用这种算法。
Python
使用了引用计数法(Reference Counting)来进行垃圾回收,因为引用计数法的回收速度要更快。Python采用备份的方式来避免循环引用带来的问题。
可达性分析法(RA) (引用链法)
从被称为“GC Roots”的对象作为起点,从这些结点开始向下搜索,节点所走过的路程称为引用链,当一个对象到“GC Roots”没有任何引用链相连的话,则证明此对象是不可用的。
强引用,软引用,弱引用,虚引用
JDK1.2之前,Java中引用的定义很传统:如果reference类型的数值存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。
强引用(Strong Reference)
必不可少的生活用品
JDK1.2之前的引用,使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。
软引用(Soft Reference)
可有可无的生活用品
如果内存空间充足,垃圾回收器不会回收具有软引用的对象。只要垃圾回收器没有回收它,该对象就可以被程序引用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用(Weak Reference)
更可有可无的生活用品
如果一个对象只具有弱引用,那么它相较于软引用拥有更短的生命周期。在垃圾回收器扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间充足与否,都会回收它的内存。垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列。
虚引用(Phantom Reference)
故名思义,虚引用形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(Reference Queue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否需要垃圾回收。
特别注意:在程序设计中一般很少使用弱引用和虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。
废弃常量与无用的类
运行时常量池主要回收的是废弃的常量。
以字符常量为例,如果该字符串没有被任何String对象引用,则说明该常量是废弃常量,如果发生内存回收而且有必要的话,该字符常量会被系统清理出常量池。
"无用的类"判定
判定一个“无用的类”,需要满足以下三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射机制访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里仅仅是说可以,而不是和对象一样不使用了就必然会被回收。
垃圾收集算法
- 标记-清除算法
- 复制算法
- 分代收集算法
- 标记-整理算法
标记-清除算法:
算法分为标记和清除两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法。后续算法都是对其不足进行的改进。
存在的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
复制算法
为了解决效率问题,复制收集算法被提出。它将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完之后,就将还存活的对象复制到另一块,然后再把使用的空间一次性清理掉。
标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活对象向一端移动,然后直接清除掉边界以外的内存。
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将Java堆分为新生代和老年代这样就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以要选择复制算法,只需付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保。所以我们必须选择标记-清除或标记-整理算法。
即先分代,新生代采用复制算法,老年代选择标记-清除算法或者标记-整理算法。
垃圾回收器
收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- CMS收集器
- G1收集器
至今为止没有最好的收集器出现,也没有万能的收集器,我们需要根据具体的应用场景选择适合自己的垃圾收集器。
Serial收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。它是一个单线程收集器,这不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在垃圾收集工作的时候必须暂停其他所有的工作线程(“Stop The World”),直到它收集结束。
后续的垃圾收集器在设计中停顿的时间在不停缩短。
Serial收集器也有一些优于其他收集器的地方。它简单而高效(与其他收集器的单线程)相比。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
它是运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器)配合工作。
这里的并行和并发概念:
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程和垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。
Parallel Scavenge收集器
Parallel Scavenge收集器类似于ParNew收集器,是使用复制算法的多线程垃圾收集器。
-XX:+UseParallelGC
使用Parallel收集器+老年代串行
-XX:+UseParallelOldGC
使用Parallel收集器+老年代并行
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对收集器运作不太了解,且手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种是在JDK1.5以及之前版本中与Parallel Scavenge收集器搭配使用;另一种用途是作为CMS收集器的后备方案。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本。使用多线程和标记-整理算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑Parallel Scavenge收集器和Parallel Old收集器。
CMS收集器
CMS(Concurren Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了垃圾收集线程与用户线程(基本上)同时工作。
从名字可以看出,CMS收集器是基于标记-清除算法实现的。它的运作过程相比于前面几种垃圾收集器来说更复杂一些。整个过程分为四步:
- 初始标记:暂停所有其他线程,并记录下直接与root相连的对象,速度很快。
- 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。但是在这个阶段结束,这个闭包结构并不能保证包含所有当前可达对象,所以GC线程无法保证可达分析的实时性。这个算法会跟踪记录这些引用发生更新的地方。
- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
- 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫。
CMS收集器主要优点:
- 并发收集
- 低停顿
它有下面三个明显的缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 它使用的回收算法“标记-清除”会导致收集结束时会有大量空间碎片产生。
G1收集器
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量的特征。
被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备以下特点:
- 并行与并发
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(或CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。 - 分代收集
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但还是保留了分代的概念。 - 空间整合:
与CMS的标记-清理算法不同,G1从整体上来看是基于标记-整理算法实现的,从局部上看是基于复制算法实现的。 - 可预测的停顿:
这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
G1收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选收回
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这就是名字Garbage-First的由来)。这种使用Region划分空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
参考
Java Guide面试突击版,百度可得最新版本,这里有删减、扩充和修正。