带你整理面试过程中关于 JVM 的运行内存划分垃圾回收算法和 4种引用类型的相关知识点
Posted 南淮北安
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你整理面试过程中关于 JVM 的运行内存划分垃圾回收算法和 4种引用类型的相关知识点相关的知识,希望对你有一定的参考价值。
文章目录
一、JVM的运行时内存
在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域
JVM堆从GC的角度可以将JVM堆分为新生代、老年代和永久代。
其中新生代默认占1/3堆空间,老年代默认占2/3堆空间,永久代占非常少的堆空间。新生代又分为Eden区、ServivorFrom区和ServivorTo区,Eden区默认占8/10新生代空间,ServivorFrom区和ServivorTo区默认分别占1/10新生代空间
关于新生代、老年代和永久代的理解:点击参考
1. 新生代
JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间。
由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
新生代又分为Eden区、ServivorTo区和ServivorFrom区
- Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为2KB~128KB,可通过XX:PretenureSizeThreshold设置其大小。在Eden区的内存空间不足时会触发MinorGC,对新生代进行一次垃圾回收。
- ServivorTo区:保留上一次MinorGC时的幸存者。
- ServivorFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者
新生代的GC过程叫作MinorGC,采用 复制算法 实现,具体过程如下:
- 把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由
XX:MaxTenuringThreshold
设置,默认为15),则将其复制到老年代,同时把这些对象的年龄加1;如果ServivorTo区的内存空间不够,则也直接将其复制到老年代;如果对象属于大对象(大小为2KB~128KB的对象属于大对象,例如通过XX:PretenureSizeThreshold=2097152
设置大对象为2MB,1024×1024×2Byte=2097152Byte=2MB),则也直接将其复制到老年代。 - 清空Eden区和ServivorFrom区中的对象。
- 将ServivorTo区和ServivorFrom区互换,原来的 ServivorTo 区成为下一次 GC 时的 ServivorFrom 区。
2. 老年代
老年代主要存放有长生命周期的对象和大对象。老年代的GC过程叫作MajorGC。在老年代,对象比较稳定,MajorGC不会被频繁触发。在进行MajorGC前,JVM会进行一次MinorGC,在MinorGC过后仍然出现老年代空间不足或无法找到足够大的连续空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM的内存空间。
MajorGC采用 标记清除算法 ,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间。
因为要先扫描老年代的所有对象再回收,所以MajorGC的耗时较长。MajorGC的标记清除算法容易产生内存碎片。在老年代没有内存空间可分配时,会抛出Out Of Memory异常。
3. 永久代
永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。
Class在类加载时被放入永久代。
永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出Out Of Memory异常,比如Tomcat引用Jar文件过多导致JVM内存不足而无法启动。
需要注意的是,在Java 8中永久代已经被元数据区(也叫作元空间)取代。
元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。
因此,元空间的大小不受JVM内存的限制,只和操作系统的内存有关
在Java 8中,JVM将类及类加载的元信息放入本地内存(Native Memory)中,将常量池和类的静态变量放入Java堆中,这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存(MaxPermSize)空间决定,而由操作系统的实际可用内存空间决定。
二、垃圾回收与算法
详细内容可参考:一篇文章带你深入了解 JVM 中的垃圾收集算法
1. 如何确定垃圾对象
Java采用引用计数法和可达性分析来确定对象是否应该被回收,其中,引用计数法容易产生循环引用的问题,可达性分析通过根搜索算法(GC Roots Tracing)来实现。
根搜索算法以一系列GC Roots的点作为起点向下搜索,在一个对象到任何GC Roots都没有引用链相连时,说明其已经死亡。根搜索算法主要针对栈中的引用、方法区中的静态引用和本地方法区中的引用展开分析
具体的可参考:一篇文章带你深入了解 Java 虚拟机中的 GC Roots 及垃圾回收如何确定垃圾
(1)引用计数法
在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。
在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引进计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收。
引用计数法容易产生循环引用问题。循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收,如图1-7所示,Object1与Object2互为引用,如果采用引用计数法,则Object1和Object2由于互为引用,其引用计数一直为1,因而无法被回收。
(2)可达性分析
为了解决引用计数法的循环引用问题,Java还采用了可达性分析来判断对象是否可以被回收。
具体做法是首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果在GC roots和一个对象之间没有可达路径,则称该对象是不可达的。
不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然是不可达的,则将被垃圾收集器回收
2. Java中常用的垃圾回收算法
Java中常用的垃圾回收算法有标记清除(Mark-Sweep)、复制(Copying)、标记整理(Mark-Compact)和分代收集(Generational Collecting)这4种垃圾回收算法,如图1-8所示。
(1)标记清除算法
标记清除算法是基础的垃圾回收算法,其过程分为标记和清除两个阶段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间,如图1-9所示。
由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用空间的问题
(2)复制算法
复制算法是为了解决标记清除算法内存碎片化的问题而设计的。复制算法首先将内存划分为两块大小相等的内存区域,即区域1和区域2,新生成的对象都被存放在区域1中,在区域1内的对象存储满后会对区域1进行一次标记,并将标记后仍然存活的对象全部复制到区域2中,这时区域1将不存在任何存活的对象,直接清理整个区域1的内存即可,如图1-10所示。
复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量的内存浪费。
同时,在系统中有大量长时间存活的对象时,这些对象将在内存区域1和内存区域2之间来回复制而影响系统的运行效率。
因此,该算法只在对象为“朝生夕死”状态时运行效率较高。
(3)标记整理算法
标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存
(4)分代收集算法
无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收
因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法
分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。
新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;
老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。因此,JVM根据不同的区域对象的特点选择了不同的算法。
目前,大部分JVM在新生代都采用了复制算法,因为在新生代中每次进行垃圾回收时都有大量的对象被回收,需要复制的对象(存活的对象)较少,不存在大量的对象在内存中被来回复制的问题,因此采用复制算法能安全、高效地回收新生代大量的短生命周期的对象并释放内存
JVM将新生代进一步划分为一块较大的Eden
区和两块较小的Servivor
区,Servivor
区又分为ServivorFrom
区和ServivorTo
区。JVM在运行过程中主要使用Eden
区和ServivorFrom
区,进行垃圾回收时会将在Eden
区和ServivorFrom
区中存活的对象复制到ServivorTo
区,然后清理Eden
区和ServivorFrom
区的内存空间,如图1-12所示。
老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法
在JVM中还有一个区域,即方法区的永久代,永久代用来存储Class类、常量、方法描述等。
在永久代主要回收废弃的常量和无用的类
如何判断一个常量是废弃常量 ?
运行时常量池主要回收的是废弃的常量,假如在常量池中存在字符串"abc",如果当前没有任何String对象引用该字符串常量的话,就说明常量”abc“就是废弃 常量,如果这时发生内存回收的话而且有必要的话,”abc“会被系统清理出常量池。
如何判断一个类是无用的类 ?
(1)该类的所有实例皆已经被回收,java中不存在该类的任何实例。
(2)加载该类的 ClassLoader 已经被回收。
(3)该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM内存中的对象主要被分配到新生代的Eden区和ServivorFrom区,在少数情况下会被直接分配到老年代。在新生代的Eden区和ServivorFrom区的内存空间不足时会触发一次GC,该过程被称为MinorGC。
在MinorGC后,在Eden区和ServivorFrom区中存活的对象会被复制到ServivorTo区,然后Eden区和ServivorFrom区被清理。
如果此时在ServivorTo区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代。
若Servivor区的对象经过一次GC后仍然存活,则其年龄加1。
在默认情况下,对象在年龄达到15时,将被移到老年代。
三、Java中的4种引用类型
详细内容可参考:一篇文章带你搞懂 Java 虚拟机的垃圾回收机制
在Java中一切皆对象,对象的操作是通过该对象的引用(Reference)实现的,Java中的引用类型有4种,分别为强引用、软引用、弱引用和虚引用,如图1-13所示。
(1)强引用:在Java中最常见的就是强引用。在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成Java内存泄漏(Memory Link)的主要原因。
例如:我们使用new创建对象
(2)软引用:软引用通过SoftReference类实现。如果一个对象只有软引用,则在系统内存空间不足时该对象将被回收。
例如:有用但是非必须的对象,缓存,
(3)弱引用:弱引用通过WeakReference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收。
适用于对内存更加敏感的场景,也可以是缓存或者占用内存很大的map(WeakHashMap)
(4)虚引用:虚引用通过PhantomReference类实现,虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用的话,那么它就和没有任何的引用一样,在任何时候都可能被垃圾回收器回收。虚引用和引用队列联合使用,可以用于判断一个对象是否被垃圾回收
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue
以上是关于带你整理面试过程中关于 JVM 的运行内存划分垃圾回收算法和 4种引用类型的相关知识点的主要内容,如果未能解决你的问题,请参考以下文章
带你整理面试过程中关于 JVM 中分代收集算法分区收集算法和垃圾收集器的相关知识