Java面试题及答案(三)

目录

 

Java面试题及答案(三)

目录

    21.请列出 5 个运行时异常。

    22.在自己的代码中,如果创建一个 java.lang.String 对象,这个对象是否可以被类加载器加载?为什么?

    23.在 jdk1.5 中,引入了泛型,泛型的存在是用来解决什么问题。

    24.这样的 a.hashcode() 有什么用,与 a.equals(b)有什么关系。

    25.有没有可能 2 个不相等的对象有相同的 hashcode。

    26.Java 中的 HashSet 内部是如何工作的。

                       27.什么情况下会发生栈内存溢出。

                       28.JVM 的内存结构,Eden 和 Survivor 比例

                       29.jvm 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的jvm 参数。

                       30.你知道哪几种垃圾收集器,各自的优缺点,重点讲下 cms,包括原理,流程,优缺点

 

    21.请列出 5 个运行时异常。

NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IllegalArgumentException - 传递非法参数异常。
ArithmeticException - 算术运算异常
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException - 下标越界异常
NegativeArraySizeException - 创建一个大小为负数的数组错误异常
NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常

    22.在自己的代码中,如果创建一个 java.lang.String 对象,这个对象是否可以被类加载器加载?为什么?

可以加载,为了使spring使用自定义的类加载器进行加载,需要开一个线程,将这个线程的类加载器设置为自定义类加载器
因为spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean
可参考:

    23.在 jdk1.5 中,引入了泛型,泛型的存在是用来解决什么问题。

第一是泛化。可以用T代表任意类型Java语言中引入泛型是一个较大的功能增强不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了,这带来了很多好处。

第二是类型安全。泛型的一个主要目标就是提高Java程序的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果不用泛型,则必须使用强制类型转换,而强制类型转换不安全,在运行期可能发生ClassCast Exception异常,如果使用泛型,则会在编译期就能发现该错误。

第三是消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。

第四是向后兼容。支持泛型的Java编译器(例如JDK1.5中的Javac)可以用来编译经过泛型扩充的Java程序(Generics Java程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译

    24.这样的 a.hashcode() 有什么用,与 a.equals(b)有什么关系。

作用是:用一个数字来标识对象。比如在HashMap、HashSet等类似的集合类中,如果用某个对象本身作为Key,即要基于这个对象实现Hash的写入和查找,那么对象本身如何实现这个呢?就是基于hashcode这样一个数字来完成的,只有数字才能完成计算和对比操作。

hashcode是否唯一 
hashcode只能说是标识对象,在hash算法中可以将对象相对离散开,这样就可以在查找数据的时候根据这个key快速缩小数据的范围,但hashcode不一定是唯一的,所以hash算法中定位到具体的链表后,需要循环链表,然后通过equals方法来对比Key是否是一样的。

equals与hashcode的关系 

hashcode是为了算法快速定位数据而存在的,而equals是为了对比真实值而存在的。

重写 equals(),就必须重写 hashCode()。
1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
2、如果两个对象不equals,他们的hashcode有可能相等。
3、如果两个对象hashcode相等,他们不一定equals。
4、如果两个对象hashcode不相等,他们一定不equals。

    25.有没有可能 2 个不相等的对象有相同的 hashcode。

有(同上)

    26.Java 中的 HashSet 内部是如何工作的。

HashSet存放的是哈希值,Hashset存储元素的顺序并不是按照存入时的顺序(和List显然不同),是按照哈希值来存的,所以取数据也是按照哈希值取的。HashSet不存入重复元素的规则:使用hashcode和equals。 那么HashSet是如何检查重复?其实原理:HashSet会通过元素的hashcode()和equals()方法进行判断,当试图将元素加入到Set集合中,HashSet首先会使用对象的hashcode来判断对象加入的位置。同时也会与其他已经加入的对象的hashcode进行比较,如果没有相等的hashcode,HashSet就认为这个对象之前不存在,如果之前存在同样的hashcode值,就会进一步的比较equals()方法,如果equals()比较返回结果是true,那么认为该对象在集合中的对象是一模一样的,不会将其加入;如果比较返回的是false,那么HashSet认为新加入的对象没有重复,可以正确加入。

JVM知识     27.什么情况下会发生栈内存溢出。

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

       28.JVM 的内存结构,Eden 和 Survivor 比例

eden 和 survior 是按8比1分配的
 

内存回收策略,主要关注如下五个方面:

1:Eden区分配

2:大对象直接进入老年代

3:长期存活的对象直接进入老年代

4:动态对象年龄判定

5:空间分配担保

 

首先明确新生代都是分配于Eden区的,所以Eden区是最重要也是内存回收最重要的管理区域,同时也是最频繁的内存替换区域。我们知道JVM将内存根据分代策略将内存分为三层,新生代所占据的内存、老年代所占据的内存以及永久代,我们这里不关注永久代,因为永久代是属于方法区内存的部分,而新生代和老年代都是属于堆内存区域的。

新生代中又继续分为三个子块,Eden区、Survivor from区、Survivor to区,实际上分为三个区的原因是为了方便采用复制-清除(详情请参考深入理解JVM中内存回收策略)策略而采用的策略,复制策略就是将原来存在的内存分为两个相等的区,使用一块进行新生代的内存分配,当要GC时,则将存活的对象复制进入另一块空闲的内存,然后将使用的内存进行清除,从而又有一个空闲区和一个使用区,并且不会有碎片问题。实际上并不需要两个1:1的分区比例,因为一般存活的对象很少,所以JVM聪明的讲新生代占据的总内存分为Eden:Survivor from:Survivor to = 8:1:1三部分,其中Eden就用来分配新的对象内存,Survivor from则用于GC时的复制,那为什么需要两个Survivor区呢,因为复制后Survivor from区虽然现在很整齐,没有碎片,当下一次进行回收时,Eden区和Survivor from区里都存在需要回收的对象,则Survivor from区也会出现碎片

    29.jvm 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的jvm 参数。

GC流程

java三年一般问什么问题 java三年面试题及答案_java三年一般问什么问题

# 挤满新生代的最后一个对象

我们应当知道,新创建的对象一般会被分配在新生代中。常用的新生代的垃圾回收器是 ParNew 垃圾回收器,它按照 8:1:1 将新生代分成 Eden 区,以及两个 Survivor 区。

某一时刻,我们创建的对象将 Eden 区全部挤满,这个对象就是「挤满新生代的最后一个对象」。此时,Minor GC 就触发了。

# 正式 Minor GC 前的检查

在正式 Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。**为什么要做这样的检查呢?**原因很简单,假如 Minor GC 之后 Survivor 区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。这样就有两种情况:

1.  老年代剩余空间大于新生代中的对象大小,那就直接 Minor GC,GC 完 survivor 不够放,老年代也绝对够放
2.  老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了「老年代空间分配担保规则」,具体来说就是看`-XX:-HandlePromotionFailure`参数是否设置了(一般都会设置)

老年代空间分配担保规则是这样的。如果老年代中剩余空间大小,大于历次 Minor GC 之后剩余对象的大小,那就允许进行 Minor GC。因为从概率上来说,以前的放的下,这次的也应该放的下。那就有两种情况:

1.  老年代中剩余空间大小,大于历次 Minor GC 之后剩余对象的大小,进行 Minor GC
2.  老年代中剩余空间大小,小于历次 Minor GC 之后剩余对象的大小,进行 Full GC,把老年代空出来再检查

# Minor GC 后的处境

前面说了,开启**老年代空间分配担保规则**只能说是大概率上来说,Minor GC 剩余后的对象够放到老年代,所以当然也会有万一,Minor GC 后会有这样三种情况:

1.  Minor GC 之后的对象足够放到 Survivor 区,皆大欢喜,GC 结束
2.  Minor GC 之后的对象不够放到 Survivor 区,接着进入到老年代,老年代能放下,那也可以,GC 结束
3.  Minor GC 之后的对象不够放到 Survivor 区,老年代也放不下,那就只能 Full GC

# 实在不行只能 OOM

前面都是成功 GC 的例子,还有 3 中情况,会导致 GC 失败,报 OOM:

1.  紧接上一节 Full GC 之后,老年代任然放不下剩余对象,就只能 OOM
2.  未开启老年代分配担保机制,且一次 Full GC 后,老年代任然放不下剩余对象,也只能 OOM
3.  开启老年代分配担保机制,但是担保不通过,一次 Full GC 后,老年代任然放不下剩余对象,也是能 OOM

晋升机制:所有的新生代首先会在Eden区进行内存分配,当Eden区满时会进行一次Minor GC操作,将Eden区进行回收,此时判断存活的对象会被复制进入Survivor from区(年龄加1),对于大对象直接进入老年代,实际上是为了保证Eden区具有充足的空间可用的一种策略,采用-XX:PretenureSizeThreshold参数可以设置多大的对象可以直接进入老年代内存区域。对于长期存活的对象直接进入老年代,实际上时对Eden区到Survivor区过度的一种策略,是为了保证Eden区到Survivor区不会频繁的进行复制一直存活的对象且对Survivor区也能保证不会具有太多的一直占据的内存,采用-XX:MaxTenuringThreshold=数字 参数可以设置对象在经过多少次GC后会被放入老年代(年龄达到设置值,默认为15)。对于动态对象年龄判断,实际上是对Survivor区的一种策略,是为了保证Survivor区具有充足的空间用于分配,动态对象年龄只判断Survivor区是否存在相等对象年龄的对象是否超过Survivor from/to的一半时,直接将超过的对象放入老年代。对于空间分配担保实际上是针对老年代,为了保证老年代的内存区域具有充足的空间,不至于内存溢出的情况出现,在发生MinorGC之前,JVM会判断之前每次晋升到老年代的平均大小是否大于老年代剩余空间的大小,若大于则进行full GC(即回收所有区域),若小于,则还需要查看一个参数HandlePromotionFailure,即是否允许担保失败,因为实际上进入老年代的对象大小在GC前是未知的,这也是为什么采用之前晋升的平均值来进行判断担保,也就是说只是一种预测,并不能代表真实就是有这么多对象晋升,所以若不允许担保失败,即保守的人为一定会有超过剩余老年代区域的对象存入,则还是进行Full GC,否则,进行Minor GC。

jvm参数详解

内存相关

选项

参数详解

默认值

-Xms

初始堆大小

--

-Xmx

最大堆大小

--

-Xmn

年轻代大小(1.4or lator)整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8

--

-XX:newSize

表示新生代初始内存的大小,应该小于 -Xms的值

--

-XX:NewRatio

设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

--

-XX:MaxNewSize

年轻代最大值(for 1.3/1.4)

--

-XX:PermSize

设置持久代(perm gen)初始值

--

-XX:MaxPermSize

设置持久代最大值

--

-Xss

每个线程的堆栈大小

--

-XX:ThreadStackSize

--

--

-XX:SurvivorRatio

Eden区与Survivor区的大小比值, 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

--

-XX:LargePageSizeInBytes

内存页的大小不可设置过大, 会影响Perm的大小,基本没用过

--

-XX:+UseFastAccessorMethods

原始类型的快速优化 1.7以后不建议使用,1.6之前默认打开的

--

-XX:+UseFastEmptyMethods

优化空方法,1.7以后不建议使用,1.6之前默认打开的

--

-XX:+DisableExplicitGC

关闭System.gc()

--

-XX:MaxTenuringThreshold

设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率

--

-XX:+AggressiveOpts

加快编译

--

-XX:+UseBiasedLocking

锁机制的性能改善, 有偏见的锁是使得锁更偏爱上次使用到它线程。在非竞争锁的场景下,即只有一个线程会锁定对象,可以实现近乎无锁的开销。

默认开启

-Xnoclassgc

禁用类垃圾回收

--

-XX:SoftRefLRUPolicyMSPerMB

每兆堆空闲空间中SoftReference的存活时间

默认是1S

-XX:PretenureSizeThreshold

对象超过多大是直接在旧生代分配,单位字节 新生代采用Parallel Scavenge GC时无效另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.

--

-XX:+CollectGen0First

FullGC时是否先YGC

false

收集器相关

选项

参数详解

默认值

-XX:+UseParallelGC

选择垃圾收集器为并行收集器。此配置仅对年轻代有效。可以同时并行多个垃圾收集线程,但此时用户线程必须停止。

--

-XX:+UseParNewGC

设置年轻代收集器ParNew

--

-XX:ParallelGCThreads

Parallel并行收集器的线程数

--

-XX:+UseParallelOldGC

设置老年代的并行收集器是ParallelOld

--

-XX:+UseG1GC

使用G1收集器

--

-XX:MaxGCPauseMillis

每次年轻代垃圾回收的最长时间(最大暂停时间)

--

-XX:+UseAdaptiveSizePolicy

设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开.

--

-XX:GCTimeRatio

设置垃圾回收时间占程序运行时间的,百分比公式为1/(1+n)

--

-XX:+ScavengeBeforeFullGC

Full GC前调用YGC

true

-XX:+UseConcMarkSweepGC

使用CMS内存收集

--

-XX:+AggressiveHeap

试图是使用大量的物理内存长时间大内存使用的优化,能检查计算资源(内存, 处理器数量)至少需要256MB内存大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升)

--

-XX:CMSFullGCsBeforeCompaction

由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理

--

-XX:+CMSParallelRemarkEnabled

降低CMS标记停顿

--

-XX+UseCMSCompactAtFullCollection

在FULL GC的时候, 对年老代的压缩,CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片

--

-XX:+UseCMSInitiatingOccupancyOnly

使用手动定义初始化定义开始CMS收集,禁止hostspot自行触发CMS GC

--

-XX:CMSInitiatingOccupancyFraction=70

使用cms作为垃圾回收使用70%后开始CMS收集

--

-XX:CMSInitiatingPermOccupancyFraction

设置Perm Gen使用到达多少比率时触发

--

-XX:+CMSIncrementalMode

设置为增量模式

--

-XX:CMSTriggerRatio

CMSInitiatingOccupancyFraction = (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100) 处罚cms收集的比例

--

-XX:MinHeapFreeRatio

java堆中空闲量占的最小比例

--

-XX:+CMSClassUnloadingEnabled

如果你启用了CMSClassUnloadingEnabled ,垃圾回收会清理持久代,移除不再使用的classes。这个参数只有在 UseConcMarkSweepGC 也启用的情况下才有用。参数如下:

--

辅助信息

 

选项

参数详解

默认值

-XX:+PrintGC

输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]Full GC 121376K->10414K(130112K), 0.0650971 secs]

--

-XX:+PrintGCDetails

--

--

-XX:+PrintGCTimeStamps

--

--

-XX:+PrintGC:PrintGCTimeStamps

--

--

-XX:+PrintGCApplicationStoppedTime

打印垃圾回收期间程序暂停的时间.可与上面混合使用

--

-XX:+PrintGCApplicationConcurrentTime

打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用

--

-XX:+PrintHeapAtGC

打印GC前后的详细堆栈信息

--

-Xloggc:filename

把相关日志信息记录到文件以便分析.与上面几个配合使用

--

-XX:+PrintClassHistogram

遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同

--

-XX:+PrintTenuringDistribution

查看每次minor GC后新的存活周期的阈值

--

-XX:PrintHeapAtGC

打印GC前后的详细堆栈信息

--

--

--

    30.你知道哪几种垃圾收集器,各自的优缺点,重点讲下 cms,包括原理,流程,优缺点

java三年一般问什么问题 java三年面试题及答案_Java_02

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

几个相关概念:

并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

一:Serial 收集器

Serial收集器是最基本的、发展历史最悠久的收集器。

特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。

应用场景:适用于Client模式下的虚拟机。

Serial / Serial Old收集器运行示意图

java三年一般问什么问题 java三年面试题及答案_泛型_03

 

二:ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。

除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

   和Serial收集器一样存在Stop The World问题

应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。

ParNew/Serial Old组合收集器运行示意图如下:

 

java三年一般问什么问题 java三年面试题及答案_Java_04

 

三:Parallel Scavenge 收集器

与吞吐量关系密切,故也称为吞吐量优先收集器。

特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)

GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
  • XX:GCRatio 直接设置吞吐量的大小。

四:Serial Old 收集器

Serial Old是Serial收集器的老年代版本。

特点:同样是单线程收集器,采用标记-整理算法。

应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。

Server模式下主要的两大用途(在后续中详细讲解···):

  1. 在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
  2. 作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。

Serial / Serial Old收集器工作过程图(Serial收集器图示相同):

java三年一般问什么问题 java三年面试题及答案_泛型_03

五:Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本。

特点:多线程,采用标记-整理算法。

应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。

Parallel Scavenge/Parallel Old收集器工作过程图:

java三年一般问什么问题 java三年面试题及答案_java三年一般问什么问题_06

六:CMS收集器

一种以获取最短回收停顿时间为目标的收集器。

特点:基于标记-清除算法实现。并发收集、低停顿。

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。

CMS收集器的运行过程分为下列4步:

初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。

并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。

重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。

并发清除:对标记的对象进行清除回收。

 CMS收集器的内存回收过程是与用户线程一起并发执行的。

 CMS收集器的工作过程图:

java三年一般问什么问题 java三年面试题及答案_泛型_07

CMS收集器的缺点:

  • 对CPU资源非常敏感。
  • 无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
  • 因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。

七:G1收集器

一款面向服务端应用的垃圾收集器。

特点如下:

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。

分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。

可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1为什么能建立可预测的停顿时间模型?

因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。

G1与其他收集器的区别

其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。

G1收集器存在的问题:

Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。

G1收集器是如何解决上述问题的?

采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。

如果不计算维护 Remembered Set 的操作,G1收集器大致可分为如下步骤:

初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)

并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)

最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set  Logs里面,把Remembered Set  Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)

筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)

G1收集器运行示意图:

java三年一般问什么问题 java三年面试题及答案_泛型_08

总的来说