面试篇【三】

  Java   36分钟   349浏览   0评论

哪些情况下的对象会被垃圾回收机制处理掉?

利用可达性分析算法,虚拟机会将一些对象定义为 GCRoots,从 GCRoots 出发沿着引用链向下寻找,如果某个对象不能通过 GCRoots 寻找到,虚拟机就认为该对象可以被回收掉。

  • 哪些对象可以被看做是 GCRoots 呢?

1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

2)方法区中的类静态属性引用的对象,常量引用的对象;

3)本地方法栈中 JNI(Native 方法)引用的对象;

  • 对象不可达,一定会被垃圾收集器回收么?

即使不可达,对象也不一定会被垃圾收集器回收,1)先判断对象是否有必要执行 finalize()方法,对象必须重写 finalize()方法且没有被运行过。2)若有必要执行,会把对象放到一个队列中,JVM 会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。

讲一下常见编码方式?

编码的意义:计算机中存储的最小单元是一个字节即 8bit,所能表示的字符范围是 255 个,而人类要表示的符号太多,无法用一个字节来完全表示,固需要将符号编码,将各种语言翻译成计算机能懂的语言。

  • ASCII 码:总共 128 个,用一个字节的低 7 位表示,0〜31 控制字符如换回车删除等;32~126 是打印字符,可通过键盘输入并显示出来;

  • ISO-8859-1:用来扩展 ASCII 编码,256 个字符,涵盖了大多数西欧语言字符。

  • GB2312:双字节编码,总编码范围是 A1-A7,A1-A9 是符号区,包含 682 个字符,B0-B7 是 汉字区,包含 6763 个汉字;

  • GBK :为了扩展 GB2312,加入了更多的汉字,编码范围是 8140~FEFE,有 23940 个码位,能 表示 21003 个汉字。

  • UTF-16: ISO 试图想创建一个全新的超语言字典,世界上所有语言都可通过这本字典Unicode 来相互翻译,而 UTF-16 定义了 Unicode 字符在计算机中存取方法,用两个字节来表 示 Unicode 转化格式。不论什么字符都可用两字节表示,即 16bit,固叫UTF-16。

  • UTF-8:UTF-16 统一采用两字节表示一个字符,但有些字符只用一个字节就可表示,浪费存储空间,而 UTF-8 采用一种变长技术,每个编码区域有不同的字码长度。 不同类型的 字 符 可 以 由 1~6 个字节组成。

utf-8 编码中的中文占几个字节;int 型几个字节?

utf-8 是一种变长编码技术,utf-8 编码中的中文占用的字节不确定,可能 2 个、3 个、4 个,int 型占 4 个字节。

静态代理和动态代理的区别,什么场景使用?

代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问,将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类的方法。

区别:

  • 静态代理:由程序员创建或是由特定工具生成,在代码编译时就确定了被代理的类是哪一个是静态代理。静态代理通常只代理一个类;

  • 动态代理:在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口下的多个实现类;

实现步骤:

a.实现 InvocationHandler 接口创建自己的调用处理器;

b.给 Proxy 类提供 ClassLoader 和代理接口类型数组创建动态代理类;

c.利用反射机制得到动态代理类的构造函数;

d.利用动态代理类的构造函数创建动态代理类对象;

使用场景:Retrofit 中直接调用接口的方法;Spring 的 AOP 机制;

简述下 Java 的异常体系。

Java 中 Throwable 是所有异常和错误的超类,两个直接子类是 Error(错误)和 Exception(异常):

  • Error 是程序无法处理的错误,由 JVM 产生和抛出,如 OOM、ThreadDeath 等。这些异常 发生时,JVM 一般会选择终止程序。

  • Exception 是程序本身可以处理的异常,又分为运行时异常(RuntimeException)(也叫Checked Eception)和 非 运 行 时 异 常(不 检 查 异 常 Unchecked Exception)。 运行 时 异 常 有 NullPointerException\IndexOutOfBoundsException 等,这些异常一般是由程序逻辑错误引起 的,应尽可能避免。非运行时异常有IOException\SQLException\FileNotFoundException 以及 由用户自定义的 Exception 异常等。

谈谈你对解析与分派的认识。

解析指方法在运行前,即编译期间就可知的,有一个确定的版本,运行期间也不会改变。解析是静态的,在类加载的解析阶段就可将符号引用转变成直接引用。

分派可分为静态分派和动态分派,重载属于静态分派,覆盖属于动态分派。静态分派是指在重载时通过参数的静态类型而非实际类型作为判断依据,在编译阶段,编译器可根据参数的静态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。

修改对象 A 的 equals 方法的签名,那么使用 HashMap 存放这个对象实例的时候,会用哪个 equals 方法?

会调用对象的 equals 方法。

“==”如果是基本类型的话就是看他们的数据值是否相等就可以。 如果是引用类型的话,比较的是栈内存局部变量表中指向堆内存中的指针的值是否相等。 “equals”如果对象的equals 方法没有重写的话,equals 方法和“==”是同一种。hashcode 是返回对象实例内存地址的 hash 映射。 理论上所有对象的 hash 映射都是不相同的。

Java 中实现多态的机制是什么?

多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用修改源程序,就可以让引用变量绑定到各种不同的类实现上。Java 实现多态有三个必要条件: 继承、重写、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。

如何将一个 Java 对象序列化到文件里?

//ObjectOutputStream.writeObject()负责将指定的流写入,ObjectInputStream.readObject() 从指定流读取序列化数据。
try {
    ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("D:/student.txt"));
    os.writeObject(studentList);
    os.close();
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

说说你对 Java 反射的理解。

在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能称为 java 语言的反射机制。

反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只对系统应用开放,这里就可以利用 java 的反射机制通过反射来获取所需的私有成员或是方法。

  • 获取类的 Class 对象实例 Classclz=Class.forName("com.zhenai.api.Apple");

  • 根据 Class 对象实例获取 Constructor 对 象 Constructor appConstructor = clz.getConstructor();

  • 使用 Constructor 对 象 的 newInstance 方 法 获 取 反 射 类 对 象 Object appleObj = appConstructor.newInstance();

  • 获取方法的 Method 对象 MethodsetPriceMethod=clz.getMethod("setPrice",int.class);

  • 利用 invoke 方法调用方法 setPriceMethod.invoke(appleObj,14);

  • 通过 getFields()可以获取 Class 类的属性,但无法获取私有属性,而getDeclaredFields()可以获取到包括私有属性在内的所有属性。带有 Declared 修饰的方法可以反射到私有的方法, 没有 Declared 修饰的只能用来反射公有的方法,其他如 Annotation\Field\Constructor 也是如此。

说说你对 Java 注解的理解。

注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@

public @interface TestAnnotation{ 

}

使用时@TestAnnotation 来引用,要使注解能正常工作,还需要使用元注解,它是可以注解到注解上的注解。元标签有@Retention、@Documented、@Target、@Inherited 和@Repeatable 五种。

@Retention 说明注解的存活时间,取值有 RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候,并不会被加载到 JVM 中。RetentionPolicy.RUNTIME 可以留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

@Documented 注解中的元素包含到 javadoc 中去。

@Target 限定注解的应用场景,ElementType.FIELD 给属性进行注解;ElementType.LOCAL_VARIABLE 可以给局部变量进行注解;ElementType.METHOD 可以给方法进行注解;ElementType.PACKAGE 可以给一个包进行注解 ElementType.TYPE可以给一个类型进行注解,如类、接口、枚举。

@Inherited 若一个超类被@Inherited 注解过的注解进行注解,它的子类没有被任何注解应用的话,该子类就可继承超类的注解;

注解的作用

  • 提供信息给编译器:编译器可利用注解来探测错误和警告信息

  • 编译阶段:软件工具可以利用注解信息来生成代码、html 文档或做其它相应处理;

  • 运行阶段:程序运行时可利用注解提取代码

注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了某个注解,再通过 getAnnotation()方法获取 Annotation 对象

说一下泛型原理,并举例说明。

泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。Java 泛型是在Java1.5以后出现的,为保持对以前版本的兼容,使用了擦除的方法实现泛型。擦除是指在一定程度无视类型参数 T,直接从 T 所在的类开始向上 T 的父类去擦除,如调用泛型方法, 传入类型参数 T 进入方法内部,若没在声明时做类似public T methodName(T extends Father t){},Java 就进行了向上类型的擦除,直接把参数 t 当做 Object 类来处理,而不是传进去的 T。 即在有泛型的任何类和方法内部,它都无法知道自己的泛型参数,擦除和转型都是在边界上发生,即传进去的参在进入类或方法时被擦除掉,但传出来的时候又被转成了我们设置的 T。在泛型类或方法内,任何涉及到具体类型(即擦除后的类型的子类)操作都不能进行,如 newT(),或者 T.play()(play 为某子类的方法而不是擦除后的类的方法)。

谈谈你对 Java 中 String 的了解。

  • String 类是 final 型,固 String 类不能被继承,它的成员方法也都默认为 final 方法。String 对象一旦创建就固定不变了,对 String 对象的任何改变都不影响到原对象,相关的任何改变 操作都会生成新的 String 对象。

  • String 类是通过 char 数组来保存字符串的,String 对 equals 方法进行了重写,比较的是值相等。

String a="test";String b="test";String c=newString("test");

a、b 和字面上的 test 都是指向 JVM 字符串常量池中的"test"对象,他们指向同一个对象。而 new 关键字一定会产生一个对象 test,该对象存储在堆中。所以 newString("test")产生了两个对象,保存在栈中的 c 和保存在堆中的 test。而在 java 中根本就不存在两个完全一模一样的字符串对象,故在堆中的 test 应该是引用字符串常量池中的 test。

例:

String str1="abc";//栈中开辟一块空间存放引用 str1,str1 指向池中 String 常量"abc"

String str2="def";//栈中开辟一块空间存放引用 str2,str2 指向池中 String 常量"def"

String str3=str1+str2;//栈中开辟一块空间存放引用 str3;str1+str2 通过 StringBuilder 的最后一步toString()方法返回一个新的 String 对象"abcdef" ;会在堆中开辟一块空间存放此对象,引用 str3 指向堆中的(str1+str2)所返回的新 String 对象。

System.out.println(str3=="abcdef");//返回 false 因为 str3 指向堆中的"abcdef"对象,而"abcdef"是字符池中的对象,所以结果为false。JVM 对 Stringstr="abc"对象放在常量池是在编译时做的 , 而 Stringstr3=str1+str2 是在运行时才知道的,new 对象也是在运行时才做的。

String 为什么要设计成不可变的?

  • 字符串常量池需要 String 不可变。因为 String 设计成不可变,当创建一个 String 对象时, 若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。 如果字符串变量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个 独立对象。

  • String 对象可以缓存 hashCode。字符串的不可变性保证了 hash 码的唯一性,因此可以缓存 String 的 hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较 hashCode,提高了比较性能;

  • 安全性。String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反射机制所需的 String 参数等,若 String 可变,将会引起各种安全隐患。

Redis 常见的几种数据结构说一下?各自的使用场景?

string

介绍:string 数据结构是简单的 key-value 类型。

使用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。

list

介绍:list 即是 链表

使用场景:发布与订阅或者说消息队列、慢查询。

hash

介绍:hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。

使用场景:系统中对象数据的存储。

set

介绍:set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不 能提供的。可以基于 set 轻易实现交集、并集、差集的操作

使用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。

sorted set(zset)

介绍:和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按score进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中HashMap 和 TreeSet 的结合体。

使用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。

bitmap

介绍:bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个byte,所以 bitmap 本身会极大的节省储存空间。

使用场景:适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。

谈一谈缓存穿透、缓存击穿和缓存雪崩,以及各自的解决方案?

参考:https://www.hqxiaozou.top/post/KnuAoPwhWyc

讲下 Kafka、RabbitMQ、RocketMQ 之间的区别是什么?

性能

消息中间件的性能主要衡量吞吐量,Kafka 的吞吐量比 RabbitMQ 要高出 1~2 个数量级,RabbitMQ 的单机 QPS 在万级别,Kafka 的单机 QPS 能够达到百万级别。RocketMQ单机写入 TPS 单实例约 7 万条/秒,单机部署 3 个 Broker,可以跑到最高 12 万条/秒,消息大小 10 个字节,Kafka 如果开启幂等、事务等功能,性能也会有所降低。

数据可靠性

Kafka 与 RabbitMQ 都具备多副本机制,数据可靠性较高。RocketMQ 支持异步实时刷盘,同步刷盘,同步 Replication,异步Replication。

服务可用性

Kafka 采用集群部署,分区与多副本的设计,使得单节点宕机对服务无影响,且支持消息容量的线性提升。RabbitMQ 支持集群部署,集群节点数量有多种规格。RocketMQ 是分布式架构,可用性高。

功能

Kafka 与 RabbitMQ 都是比较主流的两款消息中间件,具备消息传递的基本功能,但在一些特殊的功能方面存在差异,RocketMQ 在阿里集团内部有大量的应用在使用。

Kafka 的架构说一下?

整个架构中包括三个角色。

  • 生产者(Producer):消息和数据生产者。

  • 代理(Broker):缓存代理,Kafka 的核心功能。

  • 消费者(Consumer):消息和数据消费者。

Kafka 给 Producer 和 Consumer 提供注册的接口,数据从 Producer 发送到 Broker,Broker 承担一个中间缓存和分发的作用,负责分发注册到系统中的 Consumer。

Kafka 怎么保证消息是有序的?

消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。发送消息的时候指定 key/Partition。

Kafka 怎么保证消息不丢失?

生产者丢失消息的情况

生产者(Producer) 调用 send 方法发送消息之后,消息可能因为网络问题并没有发送过去。为了确定消息是发送成功,我们要判断消息发送的结果,Kafka 生产者(Producer) 使用send 方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样也让它变为了同步操作,可以采用为其添加回调函数的形式,示例代码如下:

ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);
future.addCallback(result -> logger.info("生产者成功发送消息到 topic:{} partition:{}的消息", 
         result.getRecordMetadata().topic(), result.getRecordMetadata().partition()),
         ex -> logger.error("生产者发送消失败,原因:{}", ex.getMessage()));

Producer 的 retries(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动的话你 3 次一下子就重试完了

消费者丢失消息的情况

当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。

Kafka 弄丢了消息

试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。当我们配置了 unclean.leader.election.enable = false 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。

Kafka 怎么解决重复消费?

  • 生产者发送每条数据的时候,里面加一个全局唯一的 id,消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗,如果没有消费过,就处理,然后这个 id 写 Redis。如果消费过就别处理了。

  • 基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。

介绍下 MySQL 聚簇索引与非聚簇索引的区别(InnoDB 与 Myisam 引擎)?

参考:https://www.hqxiaozou.top/post/BAOVO2a4ash

然后给一个联合索引(a,b)和一个语句,select * from table where b = 'xxx',判断是否能命中索引?为什么?

不能命中

对于查询 SELECT * FROM TABLE WHERE a=xxx and b=xxx,显然是可以使用(a,b)这个联合索引的。

对于单个的 a 列查询 SELECT * FROM TABLE WHERE a=xxx,也可以使用这个(a,b)索引。

但对于 b 列的查询 SELECT *FROM TABLE WHERE b=xxx,则不可以使用这棵 B+树索引。

在 innoDb 数据引擎中,可以发现叶子节点上的 b 值为 1、2、1、4、1、2,显然不是排序的,因此对于 b 列的查询使用不到(a,b)的索引。

ConcurrentHashMap和 HashTable 的不同之处?

  • HashTable 就是实现了 HashMap 加上了 synchronized,而 ConcurrentHashMap底层采用分段的数组+链表实现,线程安全

  • ConcurrentHashMap 通过把整个 Map 分为 N 个 Segment,可以提供相同的线程安全,但是效率提升 N 倍,默认提升 16 倍。

  • 并且读操作不加锁,由于 HashEntry 的 value 变量是 volatile 的,也能保证读取到最新的值。

  • Hashtable 的 synchronized 是针对整张 Hash 表的,即每次锁住整张表让线程独占,ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术

  • 扩容:段内扩容(段内元素超过该段对应 Entry 数组长度的 75%触发扩容,不会对整个Map 进行扩容),插入前检测需不需要扩容,有效避免无效扩容

Java 怎么实现线程安全?

  • 使用同步代码块

  • 使用同步方法

  • 使用 Lock 锁机制, 通过创建 Lock 对象,采用 lock()加锁,unlock()解锁,来保护指定的代码块

描述 ThreadLocal(线程本地变量)的底层实现原理及常用场景。

实现原理:

  • 每个 Thread 线程内部都有一个 ThreadLocalMap;以线程作为 key,泛型作为 value,可以理解为线程级别的缓存。每一个线程都会获得一个单独的 map。

  • 提供了 set 和 get 等访问方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此 get 方法总是返回由当前执行线程在调用 set 时设置的最新值。

应用场景:

  • JDBC 连接

  • Session 管理

  • Spring 事务管理

  • 调用链,参数传递

  • AOP

ThreadLocal 是一个解决线程并发问题的一个类,用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。 但是当我们不想使用同步的时候,我们可以选择 ThreadLocal 变量。例如,由于 JDBC 的连接对象不是线程安全的,因此,当多线程应用程序在没有协同的情况下,使用全局变量时,就不是线程安全的。通过将 JDBC 的连接对象保存到 ThreadLocal 中,每个线程都会拥有属于自己的连接对象副本。

介绍下 Spring Bean 都有哪些作用域 ?

  • 单例 singleton : bean 在每个 Spring IOC 容器中只有一个实例。

  • 原型 prototype:一个 bean 的定义可以有多个实例。

  • request:每次 http 请求都会创建一个 bean。

  • session:在一个 HTTP Session 中,一个 bean 定义对应一个实例。

  • globalsession

  • application

注解 @Autowired 和 @Resource 有什么区别?

  • Resource 是 JDK 提供的,而 Autowired 是 Spring 提供的

  • Resource 不允许找不到 bean 的情况,而 Autowired 允许(@Autowired(required = false))

  • 指定 name 的方式不一样,@Resource(name = "baseDao"),@Autowired()@Qualifier("baseDao")

Resource 默认通过 name 查找,而 Autowired 默认通过 type 查找

(1)@Autowired 与@Resource 都可以用来装配 bean,都可以写在字段或 setter 方法上

(2)@Autowired 默认按类型装配,默认情况下必须要求依赖对象存在,如果要允许 null 值,可以设置它的 required 属性为 false。如果想使用名称装配可以结合@Qualifier 注解进行使用。

(3)@Resource,默认按照名称进行装配,名称可以通过 name 属性进行指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名进行名称查找。如果注解写在 setter方法上默认取属性名进行装配。当找不到与名称匹配的 bean 时才按照类型进行装配。但是 需要注意的是,如果 name 属性一旦指定,就只会按照名称进行装配。

RPC 的实现基础?

  • 需要有非常高效的网络通信,比如一般选择 Netty 作为网络通信框架;

  • 需要有比较高效的序列化框架,比如谷歌的 Protobuf 序列化框架;

  • 可靠的寻址方式(主要是提供服务的发现),比如可以使用 Zookeeper 来注册服务等等;

  • 如果是带会话(状态)的 RPC 调用,还需要有会话和状态保持的功能;

CMS,G1 垃圾回收器中的三色标记了解吗?

三色标记算法思想

三色标记法是一种垃圾回收法,它可以让 JVM 不发生或仅短时间发生 STW(Stop The World),从而达到清除 JVM 内存垃圾的目的。

三色标记法将对象的颜色分为了黑、灰、白,三种颜色。

黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象);

灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC 需要从此对象中去寻找垃圾);

白色:表示对象没有被垃圾收集器访问过,即表示不可达。

CMS 解决办法:增量更新

在应对漏标问题时,CMS 使用了增量更新(Increment Update)方法来做,在一个未被标记的对象(白色对象)被重新引用后,引用它的对象若为黑色则要变成灰色,在下次二次标记时让 GC 线程继续标记它的属性对象(但还是存在漏标的问题)。

CMS 另两个致命缺陷

CMS 采用了 Mark-Sweep 算法,最后会产生许多内存碎片,当到一定数量时,CMS 无法清理这些碎片了,CMS 会让 Serial Old 垃圾处理器来清理这些垃圾碎片,而 Serial Old 垃圾处理器是单线程操作进行清理垃圾的,效率很低。所以使用 CMS 就会出现一种情况,硬件升级了,却越来越卡顿,其原因就是因为进行Serial Old GC 时,效率过低。

解决方案:使用 Mark-Sweep-Compact 算法,减少垃圾碎片

调优参数(配套使用):

-XX:+UseCMSCompactAtFullCollection 开启 CMS 的压缩

-XX:CMSFullGCsBeforeCompaction 默认为 0,指经过多少次 CMS FullGC 才进行压缩

当 JVM 认为内存不够,再使用 CMS 进行并发清理内存可能会发生 OOM 的问题,而不得不进行 Serial Old GC,Serial Old 是单线程垃圾回收,效率低解决方案:降低触发 CMS GC 的阈值,让浮动垃圾不那么容易占满老年代

调优参数:

-XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让老年代占用率达到该值就进行 CMS GC

G1 解决办法:SATB

SATB(Snapshot At The Beginning), 在应对漏标问题时,G1 使用了 SATB 方法来做,具体流程:

  • 在开始标记的时候生成一个快照图标记存活对象

  • 在一个引用断开后,要将此引用推到 GC 的堆栈里,保证白色对象(垃圾)还能被 GC 线程扫描到(在write barrier(写屏障)里把所有旧的引用所指向的对象都变成非白的)

  • 配合 Rset,去扫描哪些 Region 引用到当前的白色对象,若没有引用到当前对象,则回收

G1 会不会进行 Full GC?

会,当内存满了的时候就会进行 Full GC;且 JDK10 之前的 Full GC,为单线程的,所以使用 G1 需要避免 Full GC 的产生。

解决方案

  • 加大内存;

  • 提高 CPU 性能,加快 GC 回收速度,而对象增加速度赶不上回收速度,则 Full GC 可以避免;

  • 降低进行 Mixed GC 触发的阈值,让 Mixed GC 提早发生(默认 45%)

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  0 条评论