JVM知识点归纳(含内存模型、垃圾回收)

恭喜发现宝藏!微信搜索公众号【TechGuide】关注更多新鲜好文和互联网大厂的笔经面经。
作者@TechGuide【全网同名】
点赞再看,养成习惯,您动动手指对原创作者意义非凡🤝

当你的才华还撑不起你的野心时,你应该静下心去学习 。🤝点赞再看,养成习惯🤝

前言

JVM是Java Virtual Machine(Java虚拟机)的缩写,引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。这就是所谓的“一次编译,处处运行”。

以下内容已经涵盖了JVM中绝大部分的知识点,对于没有完全理解的部分,读者还需要结合推荐文章和相关书籍反复消化琢磨。

正文

0. 类加载机制

程序员编写的.java文件经过javac编译后生成对应的.class文件。.class文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM 把描述类的数据从.class 文件加载到内存,并对数据进行验证、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程称为虚拟机的类加载机制
在这里插入图片描述
与编译时需要连接的语言不同,Java 中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的。

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分称为连接。

具体过程:

1)加载

该阶段虚拟机需要完成三件事:

① 通过一个类的全限定类名获取定义类的二进制字节流。
② 将字节流所代表的静态存储结构转化为方法区的运行时数据区。
③ 在内存中生成对应该类的Class 实例,作为方法区这个类的数据访问入口。

2)连接

验证

确保 Class 文件的字节流符合约束。如果虚拟机不检查输入的字节流,可能因为载入有错误或恶意企图的字节流而导致系统受攻击。验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

验证重要但非必需,因为只有通过与否的区别,通过后对程序运行期没有任何影响。如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验证缩短类加载时间。

准备

类静态变量分配内存并设置零值,该阶段进行的内存分配仅包括类变量,不包括实例变量。如果变量被 final 修饰,编译时 Javac 会为变量生成 ConstantValue 属性,准备阶段虚拟机会将变量值设为代码值。

解析

将常量池内的符号引用替换为直接引用。

符号引用以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。

直接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局相关,引用目标必须已在虚拟机的内存中存在。

3)初始化

直到该阶段 JVM 才开始执行类中编写的代码。准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是执行类构造方法中的 <clinit>() 方法,该方法是 Javac 自动生成的,在初始化阶段用来组织类变量和静态代码块中的赋值动作。

在这里插入图片描述
只有类被主动使用时才会初始化。以下是主动使用的七种情况。
在这里插入图片描述

1. 年轻代出现OOM怎么处理?老年代出现OOM怎么处理?方法区出现OOM怎么处理?本地方法栈出现OOM怎么处理?

反向思考,如何让这些分区OOM。扩展:线上OOM怎么排查问题

1)堆溢出

原因:

堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会 OOM,例如在 while 死循环中一直 new 创建实例。

解决:

堆 OOM 是实际应用中最常见的 OOM,处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存泄漏还是内存溢出

如果是内存泄漏,通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联才导致无法回收,一般可以准确定位到产生内存泄漏代码的具体位置。

如果不是内存泄漏,即内存中对象都必须存活,应当检查 JVM 堆参数,与机器内存相比是否还有扩大的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

2)栈溢出

原因:

由于 HotSpot 不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss 参数来设定,存在两种异常:

StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。

OutOfMemoryError: 如果 JVM 栈可以动态扩展,当扩展无法申请到足够内存时会抛出 OutOfMemoryError。HotSpot 不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OOM,否则在线程运行时是不会因为扩展而导致溢出的

3)运行时常量池(方法区一部分)溢出

原因:

String 的 intern 方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此 String 对象的字符串,则返回池中这个字符串的 String 对象的引用,否则将此 String 对象包含的字符串添加到常量池并返回此 String 对象的引用。

在 JDK6 及之前常量池分配在永久代,因此可以通过 -XX:PermSize-XX:MaxPermSize 限制永久代大小,间接限制常量池。在 while 死循环中调用 intern 方法导致运行时常量池溢出。在 JDK7 后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中。

4)方法区溢出

原因:

方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用 JDK 反射CGLib 直接操作字节码在运行时生成大量的类。很多框架如 SpringHibernate 等对类增强时都会使用 CGLib 这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。

JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施,例如 -XX:MetaspaceSize 指定元空间初始大小,达到该值会触发 GC 进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。

在这里插入图片描述
利用jvisualvm排查oom:

在这里插入图片描述
可以利用jvisualvm或者利用jvm参数命令:-XX:+HeapDumpOnOutOfMemoryError自动dump一个关于堆信息的hprof快照文件,之后用jvisualvm本身的系统去分析。(哪里溢出了,排查强弱引用,查看引用链等)【拓展

2. 类加载器

引申思考:如何保证某个类被指定的类加载器加载?相同的类被不同的类加载器加载了,这两个类相同吗?

  1. Java虚拟机自带的加载器
  • 根类加载器(Bootstrap):该加载器没有父加载器,由C/C++编写,它负责加载虚拟机中的核心类库。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有集成java.lang.ClassLoader类。
  • 扩展类加载器(Extension):它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的jar文件放在这个目录下,也会自动由扩展类加载器加载,扩展类加载器是纯java类,是java.lang.ClassLoader的子类。
  • 系统应用类加载器(AppClassLoader/System):也称为应用类加载器,它的父加载器为扩展类加载器,它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,他是用户自定义的类加载器的默认父加载器。系统类加载器时纯java类,是java.lang.ClassLoader的子类。
  1. 用户自定义的类加载器
  • java.lang.ClassLoader的子类
  • 用户可以定制类的加载方式
    在这里插入图片描述
    在这里插入图片描述

根类加载器–>扩展类加载器–>系统应用类加载器–>自定义类加载器

类加载器并不需要等到某个类被“首次主动使用”时再加载它

  1. 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类构成;
  2. 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;
  3. 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类;
  4. 同一命名空间内的类是互相可见的,非同一命名空间内的类是不可见的;
  5. 子加载器可以见到父加载器加载的类,父加载器不能见到子加载器加载的类。
  6. 任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性。
  7. 两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载,只要类加载器不同,这两个类就必定不相等

3. jvm虚拟机结构

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
运行时内存区

1)线程私有:程序计数器、Java 虚拟机栈、本地方法栈。
2)线程共享:Java 堆、方法区/堆外内存(元空间、代码缓存-jit编译器缓存的)。

  • 虚拟机栈(线程私有内存空间)

用来描述 Java 方法的内存模型。每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表操作栈动态链接方法出口等信息。每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程。

有两类异常

① 线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError

② 如果 JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出 OutOfMemoryError(HotSpot 不可动态扩展,因此不存在OOM问题)。

在这里插入图片描述

栈帧结构
在这里插入图片描述
a. 局部变量表
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
b. 操作数栈:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
c. 动态链接:
在这里插入图片描述
运行时常量池中的符号引用:
在这里插入图片描述
d. 方法返回地址:
在这里插入图片描述

  • 程序计数寄存器

pc(线程私有内存空间)。如果线程正在执行 Java 方法,计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法,计数器值为 Undefined。唯一一个没有OOM的内存区域。
在这里插入图片描述

  • 本地方法栈

处理本地(native)方法(和虚拟机栈区别不大,只不过实现主体非java语言)。

对象所在,被所有线程共享。垃圾收集器采用分代收集算法,堆空间由此分为新生代和老年代。Eden空间、FRom Survivor空间、To Survivor空间。(数据–对象有的实例数据和元数据–class数据(实际存在方法区中,再由堆中的指针指向))

  • 方法区

元信息,常量、静态变量、class固有信息。很少被垃圾回收。(permanent generation)永久代在jdk1.8后由元空间(meta space)代替。

方法区用于存储被虚拟机加载的类型信息常量静态变量即时编译器编译后的代码缓存等数据。
JDK8 之前使用永久代实现方法区,容易内存溢出,因为永久代有 -XX:MaxPermSize 上限,即使不设置也有默认大小。JDK7 把放在永久代的字符串常量池、静态变量等移出,JDK8 中永久代完全废弃,改用在本地内存中实现的元空间代替(元空间并不在虚拟机中,而是使用本地内存),把 JDK 7 中永久代剩余内容(主要是类型信息)全部移到元空间。

  • (运行时常量池:方法区一部分,字面值、符号引用、常量。)

  • 直接内存

Direct memory.堆外内存,不由jvm管理,与Java NIO相关,jvm通过在堆上的DirectByteBuffer来操作直接内存。

4. jvm的类加载机制

其他:反射的原理以及反射的应用 扩展:如何提升反射的性能。

Class.forName这个方法比较耗时,它实际上调用了一个本地方法,通过这个方法来要求JVM查找并加载指定的类。所以我们在项目中使用的时候,可以把Class.forName返回的Class对象缓存起来,下一次使用的时候直接从缓存里面获取,这样就极大的提高了获取Class的效率。同理,在我们获取Constructor、Method等对象的时候也可以缓存起来使用,避免每次使用时再来耗费时间创建。

在这里插入图片描述
类加载器双亲委托模型的好处:

(1)可以确保Java核心库的安全:所有的Java应用都会引用java.lang中的类(Obejct、String),也就是说在运行期java.lang中的类会被加载到虚拟机中,如果这个加载过程如果是由自己的类加载器所加载,那么很可能就会在JVM中存在多个版本的java.lang中的类,互相不兼容,而且这些类是相互不可见的(命名空间的作用)。借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动根加载器去加载,从而确保了Java应用所使用的的都是同一个版本的Java核心类库,他们之间是相互兼容的;

(2)确保Java核心类库中的类不会被自定义的类所替代

(3)不同的类加载器可以为相同名称的类(binary name)创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器去加载即可。不同加载器(同一类型不同对象都可)加载的类不兼容,相当于在Java虚拟机内部建立了一个又一个相互隔离的Java类空间(不同的命名空间相互不可见)。

(4)能够提高软件系统的安全性。因此在此机制下,用户自定义的类加载器不可能加载应该由父类加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父类加载器加载的可靠代码。例如,java.lang.Object类是由跟类加载器加载,其他任何用哪个户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。

类加载器本身也是类,类加载器又是谁加载的呢??(先有鸡还是现有蛋)
类加载器是由启动类加载器去加载的,启动类加载器是C++写的,内嵌在JVM中。
内嵌于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类。当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器以及系统类加载器,这块特殊的机器码叫做启动类加载器。
启动类加载器并不是java类,其他的加载器都是java类。
启动类加载器是特定于平台的机器指令,它负责开启整个加载过程,它还会加载提供JRE正常运行所需要的基本组件,包括java.util、java.lang包中的类。

5. java如何打破双亲委派

更多应用场景可以查看这篇文章

重写父类ClassLoader的loadClass或者findClass方法

应用

  1. Java中所有涉及SPI的加载动作基本都打破了双亲委派机制。例如JNDI,JDBC等。JNDI服务使用线程上下文加载器(Thread Context ClassLoader)去加载由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码。
  2. 为了实现热插拔,热部署,模块化,这样添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。
  3. Tomcat中不同的应用程序依赖同一个第三方类库的不同版本,加载时需要相互隔离等。

6. java的四种引用类型?以及jvm如何判断这个对象可回收?finalize方法的作用?

(1) 强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。

Object o=new Object();	//强引用

(2) 只被软引用指向的对象会在 JVM 在新建对象并且其可用的堆内存不足以保存这个对象时会被垃圾回收器回收。

// 软引用 
SoftReference<Obeject> softRef=new SoftReference<Object>(o);    

软引用可用来实现内存敏感的高速缓存

按后退时,显示的网页内容是重新进行请求还是从缓存中取出呢?

Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
prev = null;//必须手动置空,否则prev还会一直是强引用    
if(sr.get()!=null){ 
    rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}else{
    prev = new Browser();       // 由于内存吃紧,所以对软引用的对象回收了
    sr = new SoftReference(prev);       // 重新构建
}

(3) 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

如果一个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。ThreadLocal中的map中entry的key、WeakHashMap中的entry就是弱引用。

(4) 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的过程,能在这个对象被垃圾器回收时收到一个系统通知

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中,这样可以通知应用程序对象的回收情况。虚引用无法单独使用,也无法通过虚引用的get方法获取被引用的对象。

可用于回收直接内存中分配的内存,当堆中的directByteBuffer被回收时,系统会通知回收直接内存部分。

finalize()方法:

确定一个对象死亡至少要经过两次标记,如果对象在可达性分析后发现没有与 GC Roots 连接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行 finalize 方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。如果有必要执行,对象会被放置在 F-Queue 队列,由一条低调度优先级的 Finalizer 线程去执行。虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的 finalize 方法执行缓慢或发生死循环。只要对象在 finalize 方法中重新与引用链上的对象建立关联就会在第二次标记时被移出回收集合。由于运行代价高昂且无法保证调用顺序,在 JDK 9 被标记为过时方法,并不适合释放资源。

当对象不可达时(GC Roots), GC会判断对象是否覆盖了finalize(), 若未覆盖则直接将其回收。若对象覆盖并且未执行过(手动调用的不计此列)finalize(),。将其放入F-Queue队列, 由一个低优先级线程执行该队列中对象的finalize()方法。执行finalize()完毕后, GC会再次判断该对象是否可达, 若不可达, 则进行回收, 否则, 对象“复活”。

Finalize应用:

java垃圾回收器只能回收创建在堆中的java对象,对于不是这种方式创建的对象(例如JNI本地对象),只能通过finalize()保证使用之后进行销毁、释放内存。(可以理解为特别照顾这种对象,专门针对覆盖这种方法的对象判断可达性,并决定是否回收。)

充当保证使用之后释放资源的最后一道屏障, 比如使用数据库连接之后未断开,并且由于程序员的个人原因忘记了释放连接, 这时就只能依靠finalize()函数来释放资源。

对象再生finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的

7. 各种垃圾回收算法和垃圾回收器 (重点掌握CMS和G1垃圾收集器)

一、垃圾回收算法

(1) 标记-清除算法

分为标记和清除阶段,首先从每个 GC Roots 出发依次标记有引用关系的对象,最后清除没有标记的对象。

执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低。

存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易触发 Full GC。

(2) 标记-复制算法

为了解决内存碎片问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于进行新生代。

实现简单、运行高效,解决了内存碎片问题。 代价是可用内存缩小为原来的一半,浪费空间

HotSpot 把新生代划分为一块较大的 Eden 和两块较小的 Survivor,每次分配内存只使用 Eden 和其中一块 Survivor。垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor。HotSpot 默认Eden 和 Survivor 的大小比例是 8:1,即每次新生代中可用空间为整个新生代的 90%。

(3) 标记-整理算法

标记-复制算法在对象存活率高时要进行较多复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保,应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法。

老年代可以使用标记-整理算法,标记过程与标记-清除算法一样,但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。

标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式的。如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作,而且移动必须全程暂停用户线程。如果不移动对象就会导致空间碎片问题,只能依赖更复杂的内存分配器和访问器解决。

二、 垃圾回收器

新生代收集器多基于复制算法,老年代收集器多基于标记-整理算法或者标记-清除算法。

在这里插入图片描述
(1) Serial

最基础的收集器,使用复制算法、单线程工作,只用一个处理器或一条线程完成垃圾收集,进行垃圾收集时必须暂停其他所有工作线程。

Serial 是虚拟机在客户端模式的默认新生代收集器,简单高效,对于内存受限的环境它是所有收集器中额外内存消耗最小的,对于处理器核心较少的环境,Serial 由于没有线程交互开销,可获得最高的单线程收集效率。
在这里插入图片描述
(2) ParNew

Serial 的多线程版本,除了使用多线程进行垃圾收集外其余行为完全一致。

ParNew 是虚拟机在服务端模式的默认新生代收集器,一个重要原因是除了 Serial 外只有它能与 CMS 配合。自从 JDK 9 开始,ParNew + CMS 不再是官方推荐的解决方案,官方希望它被 G1 取代。
在这里插入图片描述
(3) Parallel Scavenge

新生代收集器,基于复制算法,是可并行的多线程收集器,与 ParNew 类似。

特点是它的关注点与其他收集器不同,Parallel Scavenge 的目标是达到一个可控制的吞吐量,吞吐量就是处理器用于运行用户代码的时间与处理器消耗总时间的比值。

(4) Serial Old

Serial 的老年代版本,单线程工作,使用标记-整理算法

Serial Old 是虚拟机在客户端模式的默认老年代收集器,用于服务端有两种用途:

① JDK5 及之前与 Parallel Scavenge 搭配。

② 作为CMS 失败预案。

(5) Parellel Old

Parallel Scavenge 的老年代版本,支持多线程,基于标记-整理算法。JDK6 提供,注重吞吐量可考虑 Parallel Scavenge 加 Parallel Old。

(6) CMS算法

以获取最短回收停顿时间为目标,基于标记-清除算法,占用内存比例达到阈值时就会触发一次老年代CMS垃圾回收。过程相对复杂,分为四个步骤:

初始标记、并发标记、重新标记、并发清除
在这里插入图片描述
初始标记和重新标记需要 STW(Stop The World,系统停顿),初始标记仅是标记 GC Roots 能直接关联的对象,速度很快。并发标记从 GC Roots 的直接关联对象开始遍历整个对象图,耗时较长但不需要停顿用户线程。重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那部分记录。并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象,该阶段也可与用户线程并发。

缺点:

① 对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量。
② 无法处理浮动垃圾,有可能出现并发失败而导致 Full GC
③ 基于标记-清除算法,产生空间碎片。
在这里插入图片描述
(7) G1收集器:

优点

基于复制算法,且G1可以只回收部分老年代,gc停顿时间可控前提下尽可能达到更高的吞吐量。
在这里插入图片描述
特点

  • heap被划分为一个个相等的不连续的内存区域(regions) ,每个region都有一个分代的角色: eden、 survivor、 old

  • 对每个角色的数量并没有强制的限定,也就是说对每种分代内存的大小,可以动态变化

  • G1最大的特点就是高效的执行回收,优先去执行那些大量对象可回收的区域(region)

  • G1使用了gc停顿可预测的模型,来满足用户设定的gc停顿时间,根据用户设定的目标时间,G1会自动地选择哪些region要清除,一次清除多少个region

  • G1从多个region中复制存活的对象,然后集中放入一个region中,同时整理、清除内存。

分区(Region):

  • G1采取了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控等问题一G1将整个堆分成相同大小的分区(Region)

  • 每个分区都可能是年轻代也可能是老年代,但是在某一时刻只能属于某个代。年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。

  • 在物理上不需要连续,则带来了额外的好处。有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会在指定停顿时间内优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。

  • 依然是在新生代满了的时候,对整个新生代进行回收整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小

  • G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。

G1 运作过程:

  1. 初始标记:标记 GC Roots 能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象。需要 STW 但耗时很短,在 Minor GC 时同步完成。
  2. 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理初始快照算法 SATB 记录的在并发时有变动的对象。
  3. 最终标记:对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量 SATB 记录。
  4. 筛选回收:对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成

初始标记、最终标记、筛选回收都需要STW,停止用户线程。

过程详解
在这里插入图片描述
过程一、年轻代GC阶段
在这里插入图片描述
过程二、并发标记阶段
在这里插入图片描述
过程三、混合回收阶段
在这里插入图片描述
过程四、Full GC阶段
在这里插入图片描述
与cms对比:

  • 对比使用mark sweep的CMS, G1使用的复制算法不会造成内存碎片;

  • 对比Parallel Scavenge(基于copying )、Parallel Old收集器(基于mark-compact-sweep),Parallel会对整个区域做整理导致gc停顿会比较长,而G1只是特定地整理几个region。

  • G1并非一个实时的收集器,与parallelScavenge-样,对gc停顿时间的设置并不绝对生效,只是G1有较高的几率保证不超过设定的gc停顿时间。与之前的gc收集器对比,G1会根据用户设定的gc停顿时间,智能评估哪几个region需要被回收可以满足用户的设定

G1相对于CMS的优势:

  • G1在压缩空间方面有优势。
  • G1通过将内存空间分成区域(Region) 的方式避免内存碎片问题。
  • Eden、Survivor、 Old区不再固定,在内存使用效率上来说更灵活。
  • G1可以通过设置预期停顿时间( Pause Time) 来控制垃圾收集时间,避免应用雪崩现象。
  • G1在回收内存后会马上同时做合并空闲内存的工作,而CMS默认是在STW ( stop the world) 的时候做。
  • G1会在Young GC中使用,而CMS只能在Old区使用

补充1:GC安全点与安全区域
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
补充2:记忆集与写屏障

关于Garage First的底层原理(卡表、Remembered Set等)可以参考这里【文章

记忆集RSet的提出
在这里插入图片描述

8. new创建对象

  1. 确认类元信息是否存在。当 JVM 接收到 new 指令时,首先在 metaspace 内检查需要创建的类元信息是否存在。 若不存在,那么在双亲委派模式下,使用当前类加载器以全限定类名 ClassLoader + 包名+类名为 Key 进行查找对应的 .class 文件。 如果没有找到文件,则抛出 ClassNotFoundException 异常 ,如果找到,则进行类加载(加载 - 验证 - 准备 - 解析 - 初始化),并生成对应的 Class 类对象
  2. 分配对象内存。 首先计算对象占用空间大小,如果实例成员变量是引用变量,仅分配引用变量空间即可,即 4 个字节大小,接着在堆中划分—块内存给新对象。 在分配内存空间时,需要进行同步操作,比如采用 CAS (Compare And Swap) 失败重试、 区域加锁等方式保证分配操作的原子性。
  3. 设定默认值。 成员变量值都需要设定为默认值, 即各种不同形式的零值。
  4. 设置对象头。设置新对象的哈希码GC 信息锁信息、对象所属的类元信息等。这个过程的具体设置方式取决于 JVM 实现。
  5. 执行 init 方法。 初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

9. 对象分配内存的方式有哪些?

对象所需内存大小在类加载完成后便可完全确定,分配空间的任务实际上等于把一块确定大小的内存块从 Java 堆中划分出来。

指针碰撞: 假设 Java 堆内存规整,被使用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。

空闲列表: 如果 Java 堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。

选择哪种分配方式由堆是否规整决定,堆是否规整由垃圾收集器是否有空间压缩能力决定。使用 Serial、ParNew 等收集器时,系统采用指针碰撞;使用 CMS 这种基于清除算法的垃圾收集器时,采用空间列表。

对象分配内存是否线程安全?

对象创建十分频繁,即使修改一个指针的位置在并发下也不是线程安全的,可能正给对象 A 分配内存,指针还没来得及修改,对象 B 又使用了指针来分配内存。

解决方法

① CAS 加失败重试保证更新原子性。

② 把内存分配按线程划分在不同空间,即每个线程在 Java 堆中预先分配一小块内存,叫做本地线程分配缓冲(Thread Local Allocation Buffer),哪个线程要分配内存就在对应的 TLAB 分配,TLAB 用完了再进行同步。

10. 对象的内存布局

对象在堆内存的存储布局可分为对象头实例数据对齐填充

1.对象头占 12B,包括对象标记和类型指针。对象标记存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等,这部分占 8B,称为 Mark Word。Mark Word 被设计为动态数据结构,以便在极小的空间存储更多数据,根据对象状态复用存储空间。

类型指针是对象指向它的类型元数据的指针,占 4B。JVM 通过该指针来确定对象是哪个类的实例。

2.实例数据是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。

3.对齐填充不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是 8B 的倍数,对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐,需要对齐填充补全。

11. 对象的访问方式

Java 程序会通过栈上的 reference 引用操作堆对象,访问方式由虚拟机决定,主流访问方式主要有句柄直接指针

句柄: 堆会划分出一块内存作为句柄池,reference 中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。优点是 reference 中存储的是稳定句柄地址,在 GC 过程中对象被移动时只会改变句柄的实例数据指针,而 reference 本身不需要修改

直接指针: 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 存储对象地址,如果只是访问对象本身就不需要多一次间接访问的开销。优点是速度更快,节省了一次指针定位的时间开销,HotSpot 主要使用直接指针进行对象访问。

12. 如何判断对象是否是垃圾?

引用计数×:在对象中添加一个引用计数器,如果被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。原理简单,效率高,但是在 Java 中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。

可达性分析√:主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。

在这里插入图片描述
Java中可以作为GC Roots的对象:

1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般说的native方法)中引用的对象

13. ZGC 了解吗?

拓展:美团技术团队《新一代垃圾回收器ZGC的探索与实践》

  1. JDK11 中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。

  2. 基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。

  3. ZGC 的 Region 具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。

14. 你知道哪些内存分配与回收策略?

在这里插入图片描述

对象优先在 Eden 区分配

大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC

大对象直接进入老年代

大对象指需要大量连续内存空间的对象,典型是很长的字符串或数量庞大的数组。大对象容易导致内存还有不少空间就提前触发垃圾收集以获得足够的连续空间。

HotSpot 提供了 -XX:PretenureSizeThreshold 参数,大于该值的对象直接在老年代分配,避免在 Eden 和 Survivor 间来回复制。

长期存活对象进入老年代

虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头。如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1。对象在 Survivor 中每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。对象晋升老年代的阈值可通过 -XX:MaxTenuringThreshold 设置。

动态对象年龄判定

为了适应不同内存状况,虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。

空间分配担保

Minor GC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。如果不满足,虚拟机会查看 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次 Minor GC,否则改成一次 FullGC。

冒险是因为新生代使用复制算法,为了内存利用率只使用一个 Survivor,大量对象在 Minor GC 后仍然存活时,需要老年代进行分配担保,接收 Survivor 无法容纳的对象。

补充Minor GCMajor GCFull GC的区别:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

15. 你知道哪些故障处理工具(JVM命令)?

jps:虚拟机进程状况工具

功能和 ps 命令类似:可以列出正在运行的虚拟机进程,显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(LVMID)。LVMID 与操作系统的进程 ID(PID)一致,使用 Windows 的任务管理器或 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,必须依赖 jps 命令。

jstat:虚拟机统计信息监视工具

用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。
-class -gc -compiler -gcCapacity -gcNew -gcOld

参数含义:S0 和 S1 表示两个 Survivor,E 表示新生代,O 表示老年代,YGC 表示 Young GC 次数,YGCT 表示 Young GC 耗时,FGC 表示 Full GC 次数,FGCT 表示 Full GC 耗时,GCT 表示 GC 总耗时。

jinfo:Java 配置信息工具

实时查看和调整虚拟机各项参数,使用 jps 的 -v 参数可以查看虚拟机启动时显式指定的参数,但如果想知道未显式指定的参数值只能使用 jinfo 的 -flag 查询

jmap:Java 内存映像工具

用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。和 jinfo 一样,部分功能在 Windows 受限,除了生成堆转储快照的 -dump 和查看每个类实例的 -histo 外,其余选项只能在 Linux 使用。

jhat:虚拟机堆转储快照分析工具

JDK 提供 jhat 与 jmap 搭配使用分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后可以在浏览器查看。

jmap -heap 45129:这个命令直接就显示堆的信息信息。
jhap ***.dump

jstack:Java 堆栈跟踪工具

用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。线程出现停顿时通过 jstack 查看各个线程的调用堆栈,可以获知没有响应的线程在后台做什么或等什么资源。

jvisualvm排查oom
在这里插入图片描述
可以利用jvisualvm或者利用jvm命令:-XX:+HeapDumpOnOutOfMemoryError自动dump一个关于堆信息的hprof快照文件,之后用jvisualvm本身的系统去分析。(哪里溢出了,排查强弱引用,查看引用链等)【拓展】【牛客帖子调优

JMeter压测

JMeter可以用来模拟指定数量的用户来访问接口的高并发场景,它的界面如下:
在这里插入图片描述
给新建的线程组可以添加取样器,也就是访问的目标对象及相关设置,比如这里的HTTP。
在这里插入图片描述
模拟http请求并指定具体请求。
在这里插入图片描述
查看结果(结果树:成功数;汇总报告:请求时间,吞吐量等详情;聚合报告:分布统计情况:90%的请求。。。):
在这里插入图片描述
下面是关于百度压测的一些性能报告:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述






以下是关于JVM内容补充:

附录

1.引用计数算法/根搜索

  • 在实际的生产语言中(Java、 C#等),都是使用根搜索算法判定对象是否存活。

  • 算法基本思路就是通过一系列的称为“GCRoots"的点作为起始进行向下搜索,当一个对象到GCRoots没有任何引用链( Reference Chain)相连,则证明此对象
    是不可用的

  • 在Java语言中,GC Roots包括
    ●在VM栈(帧中的本地变量)中的引用
    ●方法区中的静态引用
    ●JNI (即一般说的Native方法) 中的引用

类回收需要满足如下3个条件:

  • 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例
  • 加载该类的ClassL oader已经被GC
  • 该类对应的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法

常见GC算法:

  • 标记-清除算法(Mark Sweep)
  • 标记-整理算法(Mark-Compact)
  • 复制算法(Copying)
  • 分代算法(Generational)【√】

标记一清除算法(Mark-Sweep)

  • 算法分为“标记”和“清除”两个阶段,
    首先标记出所有需要回收的对象,然后回
    收所有需要回收的对象

  • 缺点
    效率问题,标记和清理两个过程效率都不高
    空间问题,
    标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作

  • 效率不高,需要扫描所有对象。堆越大,GC越慢
    存在内存碎片问题。GC次数越多,碎片越为严重

复制(Copying) 搜集算法

  • 将可用内存划分为两块,每次只使用其中的一块,当一半区内存用完了,仅将还存活
    的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉,

  • 这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价高昂

  • 现在的商业虚拟机中都是用了这一种收集算法来回收新生代

  • 将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块
    survivor, 当回收时将eden和survivor还存活的对象一次性拷 贝到另外一块survivor空间上,然后清理掉eden和用过的survivor

  • Oracle Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的

  • 复制收集算法在对象存活率高的时候,效率有所下降

  • 如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法

在这里插入图片描述

标记一整理( Mark-Compact )算法

  • 标记过程仍然样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。

  • 没有内存碎片

  • 比Mark-Sweep耗费更多的时间进行compact

分代收集。( GenerationalCollecting)算法

  • 当前商业虚拟机的垃圾收集都是采用“分代收集”( Generational Collecting)算法,根据对象不同的存活周期将内存划分为几块。
  • 一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本,就可以完成收集。

Hotspot JVM 6中共划分为三个代:

  • 年轻代(Young Generation)

  • 老年代(Old Generation)和

  • 永久代( Permanent Generation)

  • 年轻代(Young Generation)
    新生成的对象都放在新生代。年轻代用复制算法进行GC (理论上年轻代对象的生命周期非常短,所以适合复制算法)

  • 年轻代分三个区。一个Eden区,两个Survivor区(可以通过参数设置Survivor个数)。对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到一个Survivor区,当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当第二个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。2个Survivor是完全对称,轮流替换。

  • Eden和2个Survivor的缺省比例是8:1:1,也就是10%的空间会被
    浪费。可以根据GClog的信息调整大小的比例

  • 老年代(Old Generation)

    • 存放了经过一次或多次GC还存活的对象
    • 一般采用Mark-Sweep或者Mark-Compact算法进行GC
    • 有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个GC算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器(追求吞吐量?追求最短的响应时间?)
  • 永久代

    • 并不属于堆(Heap).但是GC也会涉及到这个区域
    • 存放了每个Class的结构信息, 包括常量池、字段描述、方法描述。与垃圾收集要收集的Java对象关系不大

GC回收的时机

  • 在分代模型的基础上,GC从时机上分为两种: Scavenge GC和Full GC
    • Scavenge GC (Minor GC)
      触发时机:新对象生成时,Eden空间满了理论上Eden区大多数对象会在ScavengeGC回收,复制算法的执
      行效率会很高,ScavengeGC时间比较短。
    • Full GC
      对整个JVM进行整理,包括Young、Old 和Perm主要的触发时机: 1) Old满了2) Perm满了3) system.gc()效率很低,尽量减少Full GC。

Serial收集器

  • 最早的收集器,单线程进行GC, New和Old Generation都可以使用,
  • 在新生代,采用复制算法;
  • 在老年代,采用Mark-Compact算法
  • 因为是单线程GC,没有多线程切换的额外开销,简单实用
    Hotspot Client模式默认的收集器
    在这里插入图片描述

ParNew收集器

  • ParNew收集器就是Serial的多线程版本,除了使用多个收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Seria收集器一模一样。

  • server模式默认

  • 对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果

  • Serial收集器在新生代的多线程版本

  • 使用复制算法(因为针对新生代)只有在多CPU的环境下,效率才会比Serial收集器高

  • 可以通过-XX:ParallelGC Threads来控制GC线程数的多少。需要结合具体CPU的个数Server模式下新生代的缺省收集器
    在这里插入图片描述

Parallel Scavenge收集器

  • Parallel Scavenge收集器也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化

Parallel Old收集器

  • Parallel Old是老年代版本吞吐量优先收集器,使用多线程和标记-整理算法。

CMS (Concurrent Mark Sweep )收集器

  • CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,CMS收集器使用的是标记一清除算法(如下四个步骤)

  • 步骤:

    • 初始标记initial mark----stw,GC root直接关联的/年轻代引用的,并且在老年代中的对象
    • 并发标记concurrent mark-----,GC root tracing,并发的
    • concurrent preclean-------对象引用发生变化的区域会标记为dirty,preclean阶段会将从dirty对象到达的对象也标记上,此时就把dirty card标记清楚。
    • concurrent abortable preclean----尽量承担stw中final remark阶段的工作。
    • 重新标记final remark----stw(stop the world),再确认一次,修正
    • =================================================================
    • 至此,标记阶段结束。
    • =================================================================
    • 并发清除concurrent sweep----------并发的
    • concurrent reset-----重置cms中的数据结构
  • 特点:

    • 追求最短回收停顿时间(stw时间),非常适合Web应用
    • 只针对老年区,一般结合ParNew使用
    • Concurrent, GC线程和用户线程并发工作(尽量并发 )
    • Mark-Sweep
    • 只有在多CPU环境下才有意义
    • 使用-XX:+UseConcMarkSweepGC打开
  • CMS收集器的缺点

    • 无法处理浮动垃圾 (标记后引用发生变化的对象,只能等到下次GC再回收)
    • CMS以牺牲CPU资源的代价来减少用户线程的停顿。当CPU个数少于4的时候,有可能对吞吐量影响非常大
    • CMS在并发清理的过程中,用户线程还在跑。这时候需要预留一部分空间给用户线程
    • CMS用Mark-Sweep,会带来碎片问题。碎片过多的时候会容易频繁触发Full GC

-空间分配担保:老年代会对年轻代中survivor中无法容纳的对象直接纳入,如果老年代根据以往经验判断剩余空间不足则进行一次full GC。

拓展

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechGuide

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值