【六】GC、类加载机制,以及内存(GC部分)

这部分LZ会分成三节整理一些相关的面试连环炮。

【该部分仍参考回答阿里社招面试如何准备,顺便谈谈对于Java程序猿学习当中各个阶段的建议

 

LZ半个月前花了一个星期粗略的看了《深入浅出JVM虚拟机》这本书。将书中的内容做了摘要。这几天会将摘要部分整理出大体的面试概要。

 

【一】GC相关部分整理

 

这一部分首先你要知道什么是GC。GC就是所谓的垃圾回收,在JAVA语言中,不同于C语言,GC是有JVM虚拟机自主进行的。在程序运行的过程中,在我们的内存中会产生很多不同的对象。有的对象在内存中是长期被使用的,而有的对象则在使用了很短一段时间之后便不会再被使用。而这些存在在内存中不再被需要的对象,如若长时间不被清理,则积累到一定程度之后便会占据大量的内存,可能会导致内存消耗过大而使得程序会崩溃等等本不该发生的事故。而GC便是处理这些对象,好让内存得到缓解。

 

接下来引申出何种对象会被GC回收呢?

JVM虚拟机会在某一刻自动进行GC回收。用户也可以调用System.gc()触发垃圾回收。但是这里要注意的是,调用了这个方法并不会让GC立刻执行,而只是提醒虚拟机,我们希望进行一次垃圾回收,但是何时回收还是虚拟机自行决定的。被JVM回收的对象从表面来看是在内存中没有任何引用指向它时,便会被JVM纳入即将回收的队伍。判断对象是否可回收的方法通常有两种:第一种是引用计数法:即该对象有一个引用指向它,那该对象的引用数便会+1,而当引用计数为0时便是可回收对象。但这种方法会出现循环引用的对象的引用计数永远不会为零。故而这种方法一般不会使用。第二种方法是可达性分析方法:通过一个GCRoot,判断这个对象到GCRoot之间是否可达,如若在内存中没有任何一条路径可以从GCRoot连接到这个对象,那这个对象便会可回收对象。

 

GCRoot可以是方法区中的类静态属性引用的对象、常量对象、虚拟机栈中的引用对象以及本地方法栈中JNI引用的对象。

 

拓展:对象在可达性分析被标记为可回收对象之后,并不一定会被清除。在对象被宣告死亡的这一过程中,对象仍可以通过finalize方法进行自救,即通过这个方法重新获得一个引用。在对象被第一次标记为可回收对象之后,会进行一次筛选,判断该对象是否有必要执行finalize方法。当对象没有覆盖该方法或者该方法已被执行过一次,则视为没有必要执行。如若该对象有必要执行finalize方法,则会将对象放置于队列之中,随后由一个虚拟机自动创建的低优先级线程取执行该对象的finalize方法。如若对象在这个方法中重新获取引用指向,则该对象会在第二次标记时移除出可回收对象行列。【这里要注意,任意一个对象的finalize方法仅会调用一次】

 

这里会引申出如何创建一个对象:

创建一个对象主要有5种方法:

1、在Java中通过new关键字,调用类的构造函数显示的创建对象。

2、通过class类的newInstance反射。

3、通过java.lang.reflect.Constructor类的newInstance方法,实际上第二种方法也是调用类这个方法。

4、使用clone方法,某个类如若实现了Cloneable接口,则可以通过clone方法生成一个一样的对象。

5、通过序列化机制。

 

随后可能会引申出对象的引用类型有哪几种:

四种:强引用、弱引用、软引用、虚引用

强引用:指创建一个对象并将该对象赋给一个引用。此种类型除非程序运行结束,否则JVM虚拟机宁愿抛出OOM也不会回收该对象。

软引用:当内存不足时,可回收该类型的对象。

弱引用:无论内存大小,当进行垃圾回收时便会回收该对象。

虚引用:在任何时候都可被JVM回收。

【软引用和弱引用主要用于非必须对象。】

 

随后便引申出GC策略有哪些?(参考博客https://blog.csdn.net/mccand1234/article/details/52078645

 

GC策略会设计到采用哪种GC算法。

大体的GC算法有四种:标记-清除法(Mark—Sweep)、复制算法(Copying)、标记-整理法(Mark—Compact)、分代收集算法

1、标记-清除算法过程主要分为两个阶段,即将可回收对象标记之后进行回收,回收之后会出现不连续的内存碎片。当多次回收之后内存中容易出现大量的这种不连续内存,导致之后在程序的运行过程之中的某些占用内存较大的对象不足以存放时再次触发回收。

2、复制算法是将内存划分为等份的两部分,一次垃圾回收只对半区进行内存回收,将存活对象复制到另外一半区域。这种方法使得空间较为量费并且在对象的存活率较高的时候效率会变的较低。

3、标记-整理算法主要是在标记清除之后对内存空间进行一次整理,将存活对象移到同一边以避免不连续内存空间所带来的缺陷。

4、现在大多数商业虚拟机采用的都是分代收集算法。即将Java堆划分为新生代和老年代。新生代按照8:1:1的比例划分内存。


接下来将重点说明一下分代收集算法:

这种算法将Java堆划分为新生代和老年代。从名字就可以明白新生代中的对象存活的时间都是比较短的,所以这个区域采用的是复制算法。而老年代的对象存活率都比较高,采用标记清除或者标记整理。而方法区永久代采用的方法同老年代。

 

我们内存中的对象按照存活时间的长短可分为三类:短命对象、长寿对象、不死对象。

短命对象就比如说一个方法中或者循环中的局部变量等等。长寿对象就比如一些缓存中的对象、单例对象和数据库连接相关的一些对象等等。而不死对象比如加载了的类信息和String池中的对象等等。

 

而这三类对象前两类基本在Java堆的新生代和老年代中,而不死对象则在永久代中。

分代收集算法针对的是Java堆中的短命对象和长寿对象。新生代中的对象采用的是复制算法,然而它并不是将该区域分成等份的两部分,而是将其划分为8:1:1三部分。

复制算法:使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的活动区间与另外80%中存活的对象转移到10%的空闲区间,接下来,将之前90%的内存全部释放,以此类推

该图来自:https://blog.csdn.net/mccand1234/article/details/52078645

【六】GC、类加载机制,以及内存(GC部分)

那么如若新生代发生GC之后,10%的内存空间不足以存放剩余的存活对象时该如何处理?

这个时候我们便会使用到老年代这个“备用仓库”。

新生代的对象会在两种情况下将对象移至老年代。

第一种情况:新生代里的每一个对象都会有一个年龄。这个年龄即新生代中的对象所熬过的GC的次数。每熬过一次年龄就+1.而当这个年龄到达某一个值时,这个对象便会移动到老年代中。

第二种情况:当新生代的survivor区域不足以存放GC之后的剩余存活对象时,便会将多余的存活对象移至老年代。

在老年代中由于对象的存活周期都比较大,所以采用标记清除或者标记整理算法。


接下来GC算法讲完之后便是GC的回收时机了。

JVM在进行GC时并不会说将所有区域都进行一次GC。大多数时候会GC新生代。

因此GC又分为普通GC和全局GC

普通GC(minor GC):只针对新生代区域的GC。 
全局GC(major GC or Full GC):针对所有分代区域(新生代、年老代)的GC。 

由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC


随后便是内存分配和回收策略

首先我们的对象会优先分配在新生代的Eden中,而当Eden区域没有足够的内存中后,便会发生MinorGC,将可回收对象清除并将剩余存活对象移至Survivor区域。故而Eden区域会是一块连续的空白区域。在HotSpot虚拟机中使用两种技术来加快内存的分配。一种是bump-the-pointer。即由于Eden区域是连续的。故而只要追踪最后一个创建的对象之后的空间是否足够即可。还有一种是TLAB:Thread Local Allocation Buffers,即将Eden区域划分成若干段供每个线程使用,仍采用bump-the-pointer技术。

随后JVM会识别对象的年龄,上面提到当对象熬过GC的次数超过了某个设定的值,HotSpot默认是15次便会晋升到老年代。

再者大对象会直接进入到老年代。当某个对象需要大量连续的内存空间时,它将直接在老年代中分配。

也会动态的判定对象年龄,当Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半时,大于等于这个年龄的对象会直接进入老年代。

新生代一般采用的是停止-复制算法(这里的停止的意思是停止其他线程),在上面已经阐述过了。而老年代通常采用的是标记-整理算法。

因为老年代中的对象不乏大对象的存在,如果使用复制算法则效率会很低。当每次发生Minor GC时,都会去检查一次每次晋升到老年代的大小是否大于老年代的剩余空间大小。如若大于,则会触发一次Full GC。(允许担保失败,则不会出发Full GC)。

永久代中的回收对象主要是常量和类的加载信息。常量的回收就是这个常量没有引用指向它时即可回收,而类的加载信息要回收则需要满足三个条件:第一是类的所有实例对象均已被回收;第二是类的加载器已被回收;第三是类对象的class对象没有引用指向。

上面所说的均是理论上的GC,而实际操作GC则是垃圾收集器。

1、Serial收集器:单线程收集器,该方法采用的是复制算法,在新生代进行。会将其他线程暂停执行。

2、ParNew收集器:是Serial的多线程版本。

3、Paraller Scavenge收集器:新手代收集器,采用复制算法,并行多线程。关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)。

4、Serial Old收集器为Serial收集器的老年代版本,单线程收集器,采用标记-整理算法。

5、Paraller Old:是Paraller Scavenge的老年代版本,多线程收集器,采用标记-整理算法。

6、CMS收集器:Concurrent Mark Sweep:看名字就明白是并发的标记清除,老年代收集器。其以最短的停顿时间为目标。

     其分为四步:三步标记,一步清除。前三步是初始标记:Stop-The-World。停止其他线程,标记GC Root可直接到达的对象;并发标记:用GC Root查找引用的过程,进行可达性分析找出存活对象;重新标记:Stop-The-world,重新标记在并发标记期间程序继续运行而导致的标记变动;最后再执行并发清除过程。

故而这个收集器只会在初始标记和重新标记时会出现短暂的停顿,效率很高。

7、G1收集器:Garbage-First。

 

这里要知道并发和并行的区别:(参考:https://blog.csdn.net/java_zero2one/article/details/51477791

并发是一个处理器同时在处理多个任务,说是同时,还是单独的,只是停顿时间很短,看上去像是同时。

并行是多个处理器同时处理多个任务。

举个例子来说:并发就是一个人在吃三个馒头,并行是三个人在吃三个馒头。

 

并行:

【六】GC、类加载机制,以及内存(GC部分)

并发:

【六】GC、类加载机制,以及内存(GC部分)