宝塔服务器面板,一键全能部署及管理,送你10850元礼包,点我领取

1、详解JVM内存模型 

JVM面试题及答案-风君雪科技博客

程序计数器:这里记录了线程执行的字节码的行号,在分支、循环、跳转、异常、线程恢复等都依赖这个计数器。如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值为空(Undefined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

java虚拟机栈:每个方法执行的时候都会创建一个栈帧(stack frame)用于存放 局部变量表、操作栈、动态链接、方法出口。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程。

其中虚拟机栈中的局部变量表部分是人们比较关心的部分。局部变量表存放了编译期可知的各种基本数据类型和returnAddress类型,需要注意的是其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

Java虚拟机栈有两种异常状况:StackOverflowError(超过栈深度)和OutOfMemoryError(动态扩展内存不足)异常。

本地方法栈:与虚拟机栈很类似,区别是一个shi是执行Java方法,一个是执行本地方法。有的虚拟机会把这2个栈合二为一。本地方法栈和虚拟机栈一样会出现StackOverflowError和OutOfMemoryError异常。

Java堆:Java堆是Java虚拟机所管理的内存最大的一块,被所有线程共享的一块内存区域,在虚拟机启动的时候就创建了。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(“几乎”是因为随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上也渐渐变得不那么绝对)。

Java堆是垃圾收集器管理的主要区域,有时候也被称为“GC堆”。因为现在收集器基本都采用分代收集算法,所有Java堆还可以细分为:新生代和老年代,再细致一点有Eden空间、From Survivor空间、To Survivor空间等。堆是可以固定大小也是可以扩展的,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛出OutOfMemoryError异常。

方法区:用于存储已被Java虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它有一个别名叫Non-Heap(非堆),目的应该是与Java堆区分开来。

需要注意的是很多在HotSpot上开发的人员把方法区称为“永久代”,但是两者并不等价(HotSpot设计团队选择把GC分代手机扩展至方法区,或者说使用永久代来实现方法区而已)。在JDK1.7的HotSpot中,已经把原本放在永久代的  字符串changliangc常量池移出。方法区无法满足内存分配需求时会抛出OutOfMemoryError异常。

运行时常量池:属于方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

相对于Class文件常量池,运行时常量池具备动态性,运行期间也可以将新的常量放入池中,平时利用较多的是String类的intern()方法。

直接内存:Deirect Memory并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用,而已也可能导致OutOfMemoryError异常,所以需要注意。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

2、讲讲什么情况下会出现内存溢出,内存泄漏

1、内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。 
2、内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。

Java内存泄漏的根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。具体主要有如下几大类:

1、静态集合类引起内存泄漏:

像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

例如

Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}

在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。

2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。

例如:

public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}

3、监听器

在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

4、各种连接

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

5、内部类和外部模块的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:

public void registerMsg(Object b);

这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。

6、单例模式

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏,考虑下面的例子:

class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B类采用单例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}

显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发生什么情况。

java内存溢出常见的有:

第一种OutOfMemoryError: PermGen space

发生这种问题的原意是程序中使用了大量的jar或class,使java虚拟机装载类的空间不够,与Permanent Generation space有关。解决这类问题有以下两种办法:

增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大 小,XX:MaxPermSize是最大永久保存区域大小。如针对tomcat6.0,在catalina.sh 或catalina.bat文件中一系列环境变量名说明结束处(大约在70行左右) 增加一行: JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m" 如果是windows服务器还可以在系统环境变量中设置。感觉用tomcat发布sprint+struts+hibernate架构的程序时很容易发生这种内存溢出错误。使用上述方法,我成功解决了部署ssh项目的tomcat服务器经常宕机的问题。
清理应用程序中web-inf/lib下的jar,如果tomcat部署了多个应用,很多应用都使用了相同的jar,可以将共同的jar移到 tomcat共同的lib下,减少类的重复加载。这种方法是网上部分人推荐的,我没试过,但感觉减少不了太大的空间,最靠谱的还是第一种方法。

第二种OutOfMemoryError:  Java heap space

发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap space有关。解决这类问题有两种思路:

检查程序,看是否有死循环或不必要地重复创建大量对象。找到原因后,修改程序和算法。 我以前写一个使用K-Means文本聚类算法对几万条文本记录(每条记录的特征向量大约10来个)进行文本聚类时,由于程序细节上有问题,就导致了 Java heap space的内存溢出问题,后来通过修改程序得到了解决。
增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。如:set JAVA_OPTS= -Xms256m -Xmx1024m

第三种OutOfMemoryError:unable to create new native thread

在java应用中,有时候会出现这样的错误:OutOfMemoryError: unable to create new native thread.这种怪事是因为JVM已经被系统分配了大量的内存(比如1.5G),并且它至少要占用可用内存的一半。有人发现,在线程个数很多的情况下, 你分配给JVM的内存越多,那么,上述错误发生的可能性就越大。

那么是什么原因造成这种问题呢?

每一个32位的进程最多可以使用2G的可用内存,因为另外2G被操作系统保留。这里假设使用1.5G给JVM,那么还余下500M可用内存。这 500M内存中的一部分必须用于系统dll的加载,那么真正剩下的也许只有400M,现在关键的地方出现了:当你使用Java创建一个线程,在JVM的内 存里也会创建一个Thread对象,但是同时也会在操作系统里创建一个真正的物理线程(参考JVM规范),操作系统会在余下的400兆内存里创建这个物理 线程,而不是在JVM的1500M的内存堆里创建。在jdk1.4里头,默认的栈大小是256KB,但是在jdk1.5里头,默认的栈大小为1M每线程, 因此,在余下400M的可用内存里边我们最多也只能创建400个可用线程。

这样结论就出来了,要想创建更多的线程,你必须减少分配给JVM的最大内存。还有一种做法是让JVM宿主在你的JNI代码里边。

3、说说Java线程栈 

Java线程栈从线程创建时存在,并且是私有的。线程栈用户存储栈帧,栈帧用于存储局部变量、中间运算结果。所以局部是不存在并发的问题,因为每个栈是私有的。虚拟机只会对Java栈进行二种操作:以栈帧为单位的压栈和出栈。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。(栈帧中各个部分的作用和数据结构详见《深入理解虚拟机》第8章)。

4、JVM 年轻代到年老代的晋升过程的判断条件是什么呢

虚拟机给每个对象定义一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

还有一种方式是动态对象年龄判定。为了适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到看MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

5、JVM 出现 fullGC 很频繁,怎么去线上排查问题

https://blog.csdn.net/wilsonpeng3/article/details/70064336  这篇文章说的很好

6、类加载为什么要使用双亲委派模式,有没有什么场景是打破了这个模式

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,有各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将变得一片混乱。

双亲委派模型的实现很简单,实现代码都集中在java.lang.ClassLoader的loadClass()方法之中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

破坏双亲委派模式的场景:

1、JDK1.2之前还没有引入双亲委派模式,为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

2、JNDI服务的代码有启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用有独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,单启动类加载器不可能“认识”这些代码。为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这个行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都是采用这种方式,例如:JNDI、JDBC、JCE、JAXB、和JBI等。

3、业界“事实上”Java模块化标准的OSGi,它实现模块化热部署的关键就是它自定义的类加载器机制的实现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。具体详见《深入理解虚拟机》第7章。

 7、类的实例化顺序

1.首先是父类的静态变量和静态代码块(看两者的书写顺序);

2.第二执行子类的静态变量和静态代码块(看两者的书写顺序);

3.第三执行父类的成员变量赋值

4.第四执行父类类的构造代码块

5.第五执行父类的构造方法()

6.执行子类的构造代码块

7.第七执行子类的构造方法();

总结,也就是说虽然客户端代码是new 的构造方法,但是构造方法确实是在整个实例创建中的最后一个调用。切记切记!!!

**先是父类,再是子类; 
先是类静态变量和静态代码块,再是对象的成员变量和构造代码块–》构造方法。**

记住,构造方法最后调用!!!!成员变量优先构造代码块优先构造方法!!

8、JVM垃圾回收机制,何时触发MinorGC等操作

当JVM创建对象遇到内存不足的时候,JVM会自动触发垃圾回收garbage collecting(简称GC)操作,将不再使用但仍存在JVM内存中的对象当做垃圾一样直接清理掉,释放被占用的内存空间,供新创建的对象使用。

针对HotSpot VM的的GC其实准确分类只有两大种:

1)Partial GC:部分回收模式

Young GC:只收集young gen的GC。和Minor GC一样。
Old GC:只收集old gen的GC。只有CMS的concurrent – collection是这个模式
Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

2)Full GC:收集整个堆,包括young gen、old gen,还有永久代perm gen(如果存在的话)等所有部分的模式。同Major GC。

3)触发时机
HotSpot VM的串行GC的触发条件是:
young GC:当young gen中的eden区分配满的时候触发。

full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC;或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

并发GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集。

年轻代GC过程

当需要在堆中创建一个新的对象,而年轻代内存不足时触发一次GC,在年轻代触发的GC称为普通GC,Minor GC。注意到年轻代中的对象都是存活时间较短的对象,所以适合使用复制算法。这里肯定不会使用两倍的内存来实现复制算法了,牛人们是这样解决的,把年轻代内存组成是80%的Eden、10%的From Space和10%的To Space,然后在这些内存区域直接进行复制。

刚开始创建的对象是在Eden中,此时Eden中有对象,而两个survivor区没有对象,都是空闲区间。第一次Minor GC后,存活的对象被放到其中一个survivor,Eden中的内存空间直接被回收。在下一次GC到来时,Eden和一个survivor中又创建满了对象,这个时候GC清除的就是Eden和这个放满对象的survivor组成的大区域(占90%),Minor GC使用复制算法把活的对象复制到另一个空闲的survivor区间,然后直接回收之前90%的内存。周而复始。始终会有一个10%空闲的survivor区间,作为下一次Minor GC存放对象的准备空间。

要完成上面的算法,每次Minor GC过程都要满足:
存活的对象大小都不能超过survivor那10%的内存空间,不然就没有空间复制剩下的对象了。但是,万一超过了呢?前面我们提到过年老代,对,就是把这些大对象放到年老代。

年老代GC

什么样的对象可以进入年老代呢?如下:

在年轻代中,如果一个对象的年龄(GC一次后还存活的对象年岁加1)达到一个阈值(可以配置),就会被移动到年老代。
Survivor中相同年龄的对象大小总和超过survivor空间的一半,则不小于这个年龄的对象都会直接进入年老代。
创建的对象的大小超过设定阈值,这个对象会被直接存进年老代。
年轻代中大于survivor空间的对象,Minor GC时会被移进年老代。

年老代中的对象特点就是存活时间较长,而且没有备用的空闲空间,所以显然不适合使用复制算法了,这个时候使用标记-清除算法或者标记-整理算法来实现GC。负责年老代中GC操作的是全局GC,Major GC,Full GC。

什么时候触发Major GC呢?
在Minor GC时,先检测JVM的统计数据,查看历史上进入老年代的对象平均大小是否大于目前年老代中的剩余空间,如果大于则触发Full GC。

9、JVM 中一次完整的 GC 流程(从 ygc 到 fgc)是怎样的

10、各种回收器,各自优缺点,重点CMS、G1

上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。

下面2个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下:

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

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

Serial(串行GC)收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,即Client模式下的虚拟机来说是一个很好的选择。

ParNew(并行GC)收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。它是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。ParNew在单CPU环境下绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分百保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对GC时系统资源的有效利用还是很有好处的。

Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。由于于吞吐量关系密切,Parallel Scavenge收集器也经常被称为“吞吐量优先”收集器。Parallel Scavenge收集器有一个参数-XX:UseAdaptiveSizePolicy,当这个参数打开,虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整一些如新生代大小、Eden与Survivor区的比例等等细节参数。这种自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

Serial Old(串行GC)收集器

Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。如果在Server模式下,那么它还有两大用途:一种用途是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备预案,在并并发手机发生Concurrent Mode Failure时使用。

Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器加Parallel Old收集器。

CMS(并发GC)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,适用于集中在互联网站或者B/S系统的服务端的Java应用。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

①.初始标记(CMS initial mark)

②.并发标记(CMS concurrenr mark)

③.重新标记(CMS remark)

④.并发清除(CMS concurrent sweep)

     其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
     由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,主要有三个显著缺点:
  CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
  CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full  GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,
即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
  最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full  GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full  GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full  GC之后,跟着来一次碎片整理过程。

G1收集器

G1(Garbage First)收集器是JDK1.7提供的一个新收集器,是当今收集器技术发展的最前沿成果之一。G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK1.5中发布的CMS收集器。
与其他GC收集器相比,G1具备如下特点:
1、并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
2、分代手机:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能单独管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象已获得更好的手机效果。
3、空间整合:与CMS的“标记-清理”算法不同,G1收集器从整体上看是基于“标记-整理”算法实现的,从局部(两个Region之间)上看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序的长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
4、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾手机上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

11、各种回收算法

1.对象是否“已死”算法——引用计数器算法

  对象中添加一个引用计数器,如果引用计数器为0则表示没有其它地方在引用它。如果有一个地方引用就+1,引用失效时就-1。看似搞笑且简单的一个算法,实际上在大部分Java虚拟机中并没有采用这种算法,因为它会带来一个致命的问题——对象循环引用。对象A指向B,对象B反过来指向A,此时它们的引用计数器都不为0,但它们俩实际上已经没有意义因为没有任何地方指向它们。所以又引出了下面的算法。

2.对象是否“已死”算法——可达性分析算法

  这种算法可以有效地避免对象循环引用的情况,整个对象实例以一个树呈现,根节点是一个称为“GC Roots”的对象,从这个对象开始向下搜索并作标记,遍历完这棵树过后,未被标记的对象就会判断“已死”,即为可被回收的对象。

3、标记-清除算法

  这个方法是将垃圾回收分成了两个阶段:标记阶段和清除阶段。

            在标记阶段,通过跟对象,标记所有从跟节点开始的可达的对象,那么未标记的对象就是未被引用的垃圾对象。

            在清除阶段,清除掉所以的未被标记的对象。

            这个方法的缺点是,垃圾回收后可能存在大量的磁盘碎片,准确的说是内存碎片。因为对象所占用的地址空间是固定的。对于这个算法还有改进的算法,就是我后面要说的算法四。

4、标记-整理算法

  在算法三的基础上做了一个改进,可以说这个算法分为三个阶段:标记阶段,压缩阶段,清除阶段。标记阶段和清除阶段不变,只不过增加了一个压缩阶段,就是在做完标记阶段后,将这些标记过的对象集中放到一起,确定开始和结束地址,比如全部放到开始处,这样再去清除,将不会产生磁盘碎片。但是我们也要注意到几个问题,压缩阶段占用了系统的消耗,并且如果标记对象过多的话,损耗可能会很大,在标记对象相对较少的时候,效率较高。

  对于新生代,大部分对象都不会存活,所以在新生代中使用复制算法较为高效,而对于老年代来讲,大部分对象可能会继续存活下去,如果此时还是利用复制算法,效率则会降低。标记-压缩算法首先还是“标记”,标记过后,将不用回收的内存对象压缩到内存一端,此时即可直接清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。老年代的垃圾回收称为“Major GC”。

5、复制算法(Java中新生代采用)

  核心思想是将内存空间分成两块,同一时刻只使用其中的一块,在垃圾回收时将正在使用的内存中的存活的对象复制到未使用的内存中,然后清除正在使用的内存块中所有的对象,然后把未使用的内存块变成正在使用的内存块,把原来使用的内存块变成未使用的内存块。很明显如果存活对象较多的话,算法效率会比较差,并且这样会使内存的空间折半,但是这种方法也不会产生内存碎片。

  此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。

6、分代法(Java堆采用)

主要思想是根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率。

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

JVM面试题及答案-风君雪科技博客

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。新生代垃圾回收采用复制算法,清理的频率比较高。如果新生代在若干次清理(可以进行设置)中依然存活,则移入老年代,有的内存占用比较大的直接进入老年代。老年代使用标记清理算法,清理的频率比较低。

7、分区算法

这种方法将整个空间划分成连续的不同的小区间,每个区间都独立使用,独立回收,好处是可以控制一次回收多少个小区间。

12、OOM错误,stackoverflow错误,permgen space错误

1, OutOfMemoryError异常

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能,

Java Heap 溢出

一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess

java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。

出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。

如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。

2, 虚拟机栈和本地方法栈溢出

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

这里需要注意当栈的大小越大可分配的线程数就越少。

3, 运行时常量池溢出

异常信息:java.lang.OutOfMemoryError:PermGen space

如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

4, 方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

异常信息:java.lang.OutOfMemoryError:PermGen space

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。

5、本机直接内存溢出

直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,是jvm外部的内存区域,这部分区域也可能导致OutOfMemoryError异常。

由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。