目录
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
System.gc() 和 Runtime.gc() 会做些什么?
如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?
串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
Java运行时数据区
程序计数器
是一块较小内存,可以看作是当前线程所执行的字节码的行号指示器。每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响。
Java虚拟机栈
Java虚拟机栈也是线程私有,他的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量、操作数、操作数栈、动态链接、方法出口等信息。每一个方法的调用过程直至执行完成的过成,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
与虚拟机栈类似,他们之间的区别是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
Java堆
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在此内存区域中唯一目的就是存放对象实例,几乎所有的对象都在这里分配内存。
Java堆是垃圾收集器管理的主要区域。也叫“GC堆”。从内存回收的角度来看,由于现在的收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;在细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的家读来看,线程共享的Java堆中可能划分出多个线程私有分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论那个区域,存储的都是对象实例,进一步划分的目的是为了更好地回收内存,或者更快的分配内存。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
方法区(Method Area)
和Java堆一样也是各个线程共享的内存区域,他用于存储已被虚拟机加载的u类信息、v常量、w静态变量、x即时编译后的代码等数据。使用永久代实现方法区。
堆和栈的区别
- 堆是由低地址向高地址扩展;栈是由高地址向低地址扩展
- 堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存
- 堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片
- 栈使用的是一级缓存, 通常都是被调用时处于存储空间中,调用完毕立即释放。堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
- 堆的分配效率较低,而栈的分配效率较高
- 栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;
- 而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。
Java 堆的结构是什么样子的?
Java虚拟机将堆内存划分为新生代、老年代和永久代,永久代是HotSpot虚拟机特有的概念(JDK1.8之后为metaspace替代永久代),它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且HotSpot也有取消永久代的趋势,在JDK 1.7中HotSpot已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
新生代(Young Generation)
新生成的对象优先存放在新生代中,新生代对象存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。
HotSpot将新生代划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代)。
老年代(Old Generationn)
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
永久代(Permanent Generationn)
永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
为什么移除持久代
- 大小是在启动时固定好的——很难进行调优。
- HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)。
对象的创建
- 虚拟机遇到一条new指令时先去检查这个指令的参数是否能在常量池中定位到一个类的符号的引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。
- 如果没有,那必须先执行相应的类的加载过程。在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需要的内存在类加载完成之后便可完全确定。
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
指针碰撞:假设Java堆是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放一个指针作为分界点的指示器。为对象分配内存时把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表:Java堆中的内存不是规整的,以使用的内存和空闲的内存相互交错,无法使用指针碰撞。这时虚拟机就必须维护一个列表,记录上哪些内存是可用的,在分配内存的时候从列表中找一块足够大的空间划分给对象实例,并更新列表上的记录。
如果只是修改一个指针所指向的位置,在并发情况下并不是线程安全,可能出现正在给对象A分配内存,指针还没来的及修改,对象B又同时使用了原来的指针来分配内存的情况。两种解决方案,
- 一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
- 另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。那个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
对象的内存布局
对象在内存存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头包括两部分信息,
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等.这部分数据在长度32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为"Mark Word".考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,他会根据对象的状态复用自己的存储空间。
- 对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.并不是所有的虚拟机实现都必须在对象数据上保留类型指针.另外,如果对象是一个Java数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小。
实例数据部分是对象真正存储的有效信息,也是在程序代码中多定义的各种类型字段内容.无论是从父类继承下来的,还是在子类定义的,都需要记录起来.
对其填充不是必然存在的,只是起着占位符的作用.由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象大小必须是8字节的证书倍.对象头部分正好是8字节的倍数(1倍或者2倍),因此当实例数据部分没有对齐时,就需要通过对齐填充来补全.
对象访问定位
目前主流的访问方式有使用句柄和直接指针两种。
- 句柄访问:Java堆将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
- 直接指针访问:Java堆对象的布局中就须考虑如何放置访问类型数据相关信息,而reference中存储的就是对象地址。
使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问的最大好处就是速度更快,他节省了一次指针定位时间的开销。HotSpot采用的是直接指针进行对象访问的。
Java 中会存在内存泄漏吗,简述一下?
内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。java中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象变成了孤儿的时候,对象将自动被垃圾回收器从内存中清除掉。由于Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的
java中的内存泄露的情况:
- 长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
- 检查java中的内存泄露,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄露。
- 如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
- 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。
Java 类加载过程?
类加载过程主要包含加载、验证、准备、解析、初始化、使用、卸载七个方面,下面一一阐述。
加载
在加载阶段,虚拟机主要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口
验证
验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段
- 文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
- 元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
- 字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
- 符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。
准备
准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:
pirvate static int size = 12;
那么在这个阶段,size的值为0,而不是12。 final修饰的类变量将会赋值成真实的值。
解析
解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
初始化
在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
至于使用和卸载阶段阶段,这里不再过多说明,使用过程就是根据程序定义的行为执行,卸载由GC完成。
描述一下 JVM 加载 Class 文件的原理机制?
#加载必要性
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。
#加载方式
类装载方式,有两种
- 隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
- 显式装载, 通过class.forname()等方法,显式加载需要的类 。
隐式加载与显式加载的区别:
- new是硬编码,然而在大部分情况下,new这种硬编码够用了。
- Class.forName(String)可以动态加载,这增强了java的动态性。
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
#类加载器
Java的类加载器有三个,对应Java的三种类:(java中的类大致分为三种: 1.系统类 2.扩展类 3.由程序员自定义的类 )
Bootstrap Loader // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)
|
- - ExtClassLoader // 负责加载扩展类(就是继承类和实现类)
|
- - AppClassLoader // 负责加载应用类(程序员自定义的类)
Java采用了委托模型机制协调三个加载器的工作。
委托模型机制的工作原理很简单:当类加载器需要加载类的时候,先请示其Parent(即上一层加载器)在其搜索路径载入,如果找不到,才在自己的搜索路径搜索该类。这样的顺序其实就是加载器层次上自顶而下的搜索,因为加载器必须保证基础类的加载。之所以是这种机制,还有一个安全上的考虑:如果某人将一个恶意的基础类加载到jvm,委托模型机制会搜索其父类加载器,显然是不可能找到的,自然就不会将该类加载进来。
BootstrapKLoader是用C++编写的,所以打印其内容将会得到null。
什么是类加载器?
类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。
类加载器有哪些?
- Bootstrap ClassLoader(系统类加载器)
- Extension ClassLoader(扩展类加载器)
- Application ClassLoader(应用程序类加载器)
- Customer ClassLoader(自定义加载器)
什么是GC? 为什么要有 GC?
GC是垃圾收集器,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。
Java 程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:
System.gc()
Runtime.getRuntime().gc()
Java有了GC,就不需要程序员去人工释放内存空间,程序员所要做的应该是实现所需要的程序功能,而不是耗费大量精力在内存的分配释放上。当Java虚拟机发觉内存资源紧张的时候,就会自动地去清理无用变量所占用的内存空间。当然,如果需要,程序员可以在Java程序中显式地使用System.gc()来强制进行一次立即的内存清理。显式声明是做堆内存全扫描,也就是 Full GC,需要停止所有的活动的。显示调用System.gc()只是给虚拟机一个建议,不一定会执行,因为System.gc()在一个优先级很低的线程中执行。
简述一下Java 垃圾回收机制?
如何判断一个对象是否存活?
引用计数法
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
优点:实现简单,判断效率高
缺点:很难解决对象之间的相互循环引用,所以java语言并没有选用引用计数法管理内存
可达性分析算法(根搜索算法)
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈中引用的对象(栈帧中的本地变量表);
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象。
对象被回收前的最后一次挣扎
通过可达性分析,那些不可达的对象并不是立即被销毁,他们还有被拯救的机会。如果要回收一个不可达的对象,要经历两次标记过程。
首先是第一次标记,并判断对象是否覆写了 finalize 方法,如果没有覆写,则直接进行第二次标记并被回收。
如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
基本原理(对象引用遍历方式):
对于GC(垃圾收集)来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。
通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。
当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
垃圾回收器不可以马上回收内存。
垃圾收集器不可以被强制执行,但程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
- 通过编程影响垃圾回收
- 将无用对象赋值null
- 重新为引用变量赋值
- 让相互联系的对象成为“孤岛”对象
深拷贝和浅拷贝?
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
System.gc() 和 Runtime.gc() 会做些什么?
System.gc()就是通过调用Runtime.gc()来完成的。
调用 gc 方法暗示着 Java 虚拟机做了一些努力来回收未用对象,以便能够快速地重用这些对象当前占用的内存。当控制权从方法调用中返回时,虚拟机已经尽最大努力从所有丢弃的对象中回收了空间。
如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?
不会立即释放对象占用的内存。
如果对象的引用被置为null,只是断开了当前线程栈帧中对该对象的引用关系,而 垃圾收集器是运行在后台的线程,只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系,扫描到对象没有被引用则会标记对象,这时候仍然不会立即释放该对象内存,因为有些对象是可恢复的(在 finalize方法中恢复引用 )。只有确定了对象无法恢复引用的时候才会清除对象内存。
也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。
什么是分布式垃圾回收(DGC)?它是如何工作的?
DGC 叫做分布式垃圾回收。RMI 使用 DGC 来做自动垃圾回收。因为 RMI 包含了跨虚拟机的远程对象的引用,垃圾回收是很困难 的。
DGC 使用引用计数算法来给远程对象提供自动内存管理。
串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?
吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模和大规模数据的应用程序。而串行收集器对大多数的小应用(在现代处理器上需要大概100M左右的内存)就足够了。
在 Java 中,对象什么时候可以被垃圾回收?
当一个对象到GC Roots不可达时,在下一个垃圾回收周期中尝试回收该对象,如果该对象重写了finalize()方法,并在这个方法中成功自救(将自身赋予某个引用),那么这个对象不会被回收。但如果这个对象没有重写finalize()方法或者已经执行过这个方法,也自救失败,该对象将会被回收。
简述Minor GC 和 Major GC?
MinorGC
触发条件:Eden区没有足够的空间进行分配
GC算法:复制算法
GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。
GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。
接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。
接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。
*GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
MajorGC
触发条件:老年代空间不足或者无法找到足够大的连续空间分配给新创建的大对象(MajorGC前进行一次MinorGC)
GC算法:标记清除算法
扫描所有对象,并标记存活的对象,然后回收未被标记的对象,并释放内存空间。
FullGC
FullGC是针对新生代,老年代和方法区(元空间)的垃圾收集。FullGC产生的条件:
调用System.gc时,系统建议执行Full GC,但是不一定会执行 。
老年代空间不足。
方法区空间不足,类卸载(类卸载三个条件)。
通过 Minor GC 后进入老年代的空间大于老年代的可用内存
内存空间担保。
JVM 的永久代中会发生垃圾回收么?
如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
加载该类的ClassLoader已经被回收;
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
当满足这3个条件时,是可以回收,但回不回收还得看jvm。
Java中垃圾收集方法有哪些?
标记-清除算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,此算法一般没有虚拟机采用。
优点1:解决了循环引用的问题
优点2:与复制算法相比,不需要对象移动,效率较高,而且还不需要额外的空间
不足1:每个活跃的对象都要进行扫描,而且要扫描两次,效率较低,收集暂停的时间比较长。
不足2:产生不连续的内存碎片
复制算法
将内存分成两块容量大小相等的区域,每次只使用其中一块,当这一块内存用完了,就将所有存活对象复制到另一块内存空间,然后清除前一块内存空间。这样一来就不容易出现内存碎片的问题。
1、复制的代价较高,所以适合新生代,因为新生代的对象存活率较低,需要复制的对象较少;
2、需要双倍的内存空间,而且总是有一块内存空闲,浪费空间
标记-整理算法
思想:在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
不会产生内存碎片,但是依旧移动对象的成本。
分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。
它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
Java 中垃圾收集器有哪些?
Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
Parallel Old收集器(停止-复制算法)
Parallel Scavenge
收集器的老年代版本,并行收集器,吞吐量优先。
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。