目录
1.Java内存区域
1.1.程序计数器
1.2.Java虚拟机栈
1.3.本地方法栈
1.4.Java堆
1.5.方法区
2.垃圾收集器与内存分配策略
2.1.对象已死吗
2.1.1.引用计数法
2.1.2.可达性分析算法
2.1.3.再谈引用
2.2.垃圾回收算法
2.2.1.标记-清除算法
2.2.2.复制算法
2.2.3.标记-整理算法
2.2.4.分代收集算法
2.3.垃圾回收器
2.3.1.Serial收集器
2.3.2.ParNew收集器
2.3.3.Parallel Scavenge/Parallel Old收集器
2.3.4.CMS收集器
4.内存分配与回收策略
3.总结:
1.Java内存区域
对Java程序员来说,在虚拟机自动内存管理机制的帮助下,无需为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,但一旦出现,排查将较为困难,需要掌握一些的基础。
Java虚拟机运行时数据区
1.1.程序计数器
线程私有,程序计数器是正在执行的虚拟机字节码指令的地址,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果正在执行的是Native方法,这个计数器值则为空。
1.2.Java虚拟机栈
线程私有,每个线程都有自己的虚拟机栈。在方法执行时,虚拟机会为每个方法分配一个栈帧,栈帧中包含了方法的局部变量表、操作数栈、动态链接和方法出口等信息。当方法执行完毕后,虚拟机会弹出该方法的栈帧。
具体来说,虚拟机栈保存的主要内容包括:
1. 局部变量表:用于存储方法参数和方法内部定义的局部变量,包括基本数据类型和对象引用。局部变量表的容量在编译期间确定,并且是不可变的。
2. 操作数栈:用于保存操作数和中间结果,以及执行算术运算、逻辑运算、类型转换等操作时所需要的操作数。操作数栈的容量可以在运行时动态改变。
3. 动态链接:用于支持方法动态绑定,即在运行时根据对象的实际类型确定调用的方法。
4. 方法出口:用于保存方法执行完毕后的返回值和异常信息。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈扩展栈时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
1.3.本地方法栈
线程私有,本地方法栈是Java虚拟机执行本地方法时使用的一块内存区域,本地方法是指使用非Java语言编写的方法,如使用C或C++编写的本地方法。Java虚拟机通过本地方法接口(JNI)调用本地方法。
1.4.Java堆
Java堆是Java虚拟机中用于存储对象实例的一块内存区域。Java堆是所有线程共享的内存区域,用于存储在Java程序中动态创建的对象和数组。Java堆是Java虚拟机内存管理的核心。
Java堆的大小可以通过虚拟机启动参数来指定,也可以在运行时动态调整。Java堆在启动时被划分为一块连续的内存空间,其中包含了新生代和老年代两个区域。
新生代是Java堆的一部分,用于存储新创建的对象。新生代又被分为三个区域:Eden区、Survivor 0区和Survivor 1区。当新创建的对象被分配到新生代时,它会首先被放入Eden区。当Eden区满时,会触发一次Minor GC(新生代垃圾回收),将所有存活的对象移动到Survivor区。Survivor区也会随着时间不断地被填满,当Survivor区也满了时,会将所有存活的对象移动到另外一个Survivor区,同时清空原来的Survivor区。这个过程被称为“对象年龄判断”,根据Java虚拟机规范,对象在Survivor区中存活了一定次数后,就会被晋升到老年代。
老年代是Java堆的另一部分,用于存储较长时间存活的对象。在老年代中进行垃圾回收的过程被称为Full GC(全局垃圾回收),其时间开销相对较大。因此,尽可能减少Full GC的发生对于Java应用程序的性能和稳定性都非常重要。
1.5.方法区
方法区是Java虚拟机中用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据的一块内存区域。方法区是所有线程共享的内存区域。
方法区中的数据都是在类加载过程中被加载进来的,并且在程序运行过程中是不能被卸载的,直到虚拟机退出为止。由于方法区中存储的是类信息、常量、静态变量和方法信息等数据,因此方法区的大小一般要比Java堆大得多。方法区一般不会发生OutOfMemoryError(内存溢出)异常。
2.垃圾收集器与内存分配策略
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,当方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆和方法区则不一样,其内存的分配和回收都是动态的,垃圾回收器会关注此部分。
2.1.对象已死吗
2.1.1.引用计数法
引用计数法是一种垃圾回收算法。为每个对象维护一个计数器,记录当前有多少个引用指向该对象。当计数器变为0时,该对象就可以被回收。
引用计数法的优点是回收对象的时机比较精确,对象一旦变得不可达,就可以立即回收,不需要等待垃圾回收器扫描整个堆。但是,该算法也存在一些缺点:
1. 无法处理循环引用:如果两个或多个对象之间存在循环引用,即相互引用对方,那么它们的计数器永远不会变为0,因此无法被回收。
2. 计数器的维护开销比较大:每次对象被引用或取消引用时,都需要更新计数器,这会带来一定的开销。
因此,在实际应用中,引用计数法并不是一种常用的垃圾回收算法,而更多地采用基于可达性分析的算法,例如标记-清除算法、标记-整理算法和复制算法等。
2.1.2.可达性分析算法
可达性分析算法是是Java虚拟机中默认的垃圾回收算法。其基本思想是通过一系列的根对象(GC Roots)来遍历所有对象,并标记所有被引用的对象为“存活”,未被标记的对象则被视为“垃圾”并进行回收。
在Java虚拟机中,根对象包括以下几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
2. 方法区中类静态属性引用的对象。
3. 方法区中常量引用的对象。
4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。
2.1.3.再谈引用
引用(Reference)是指一个对象对另一个对象的间接访问。Java中的引用有四种,分别是强引用、软引用、弱引用和虚引用(强软弱虚)。
1. 强引用(Strong Reference):如果一个对象具有强引用,那么垃圾回收器就不会回收这个对象。即使Java堆空间不足,垃圾回收器也不会回收具有强引用的对象。强引用通常是通过赋值操作来创建的。
2. 软引用(Soft Reference):如果一个对象具有软引用,那么垃圾回收器只有在Java堆空间不足的情况下才会回收这个对象。可以通过SoftReference类来创建软引用。
3. 弱引用(Weak Reference):如果一个对象具有弱引用,那么垃圾回收器会在下一次进行垃圾回收时回收这个对象,无论Java堆空间是否足够。可以通过WeakReference类来创建弱引用。
4. 虚引用(Phantom Reference):如果一个对象具有虚引用,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。可以通过PhantomReference类来创建虚引用。
Java中的引用机制可以方便地控制内存的使用和回收,提高应用程序的性能和稳定性。在实际应用中,可以根据具体的内存使用情况来选择不同类型的引用,以达到最优的内存利用效果。
2.2.垃圾回收算法
2.2.1.标记-清除算法
基本思想是标记所有被引用的对象,然后清除所有未被标记的对象。可以处理循环引用的情况,但存在:标记和清除的效率比较低、产生大量不连续内存碎片等问题。
”标记-清除”算法示意图
2.2.2.复制算法
基本思想是将存活对象复制到另一个内存空间中,然后清除原来的内存空间。
复制算法的优点是实现简单、复制效率高、不容易产生内存碎片等。但是,该算法也存在一些缺点,例如需要两倍的内存空间、复制大对象的效率比较低等。
在实际应用中,复制算法通常与标记-清除算法或标记-整理算法结合使用,以达到更好的垃圾回收效果。
复制算法示意图
2.2.3.标记-整理算法
基本思想是在标记阶段标记所有存活的对象,然后将它们整理到堆的一端,然后将堆的另一端全部清空。
标记-整理算法的优点是可以避免空间碎片化的问题,使得堆空间的使用更加高效。但是,该算法的缺点是需要移动存活对象,因此效率较低,尤其是对于大对象的处理会更加困难。
标记-整理算法
2.2.4.分代收集算法
新生代复制算法,老年代"标记-清除"或者"标记-清理"算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
2.3.垃圾回收器
收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
2.3.1.Serial收集器
单线程的收集器,所谓“单线程”,不仅仅是它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。优点是简单高效,缺点是暂停所有用户线程。
2.3.2.ParNew收集器
Serial收集器的多线程版本,除了新生代使用多条线程进行垃圾收集之外,其他无二。
2.3.3.Parallel Scavenge/Parallel Old收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器;Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
2.3.4.CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。集中应用在互联网站等场景,尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:初始标记、并发标记、重新标记和并发清除。
初始标记、重新标记这两个步骤仍然需要“Stop The World”。并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
优点:并发标记、低停顿;缺点:大量碎片空间产生、对CPU敏感。
4.内存分配与回收策略
自动内存管理归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。上面聊到了回收,现在我们聊聊分配。
1.对象优先在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
2.大对象直接进入老年代:最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象)。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
3.长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。
3.总结:
Java内存区域有程序计数器(正在执行的字节码指令地址)、Java虚拟机栈(局部变量表、操作数栈和方法出口等)、本地方法栈、堆(动态创建的对象和数组)和方法区(类信息、常量和静态变量等)。前三者为线程私有,后两者为线程共享。
垃圾收集器与内存分配策略聊了垃圾回收算法、垃圾回收器和分配内存的一些策略。