java_JVM面试总结

JVM参数问题

1、问题:JVM怎样通过参数调整内存大小

来源:阿里巴巴

问题描述:如题 

 

解决方案:

 

-Xmx    最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定;

-Xms    堆最小值,Server端JVM最好将-Xms和-Xmx设为相同值,开发测试机JVM可以保留默认值;

-Xmn    新生代大小

-Xss    每个线程的Stack大小
-XXSurvivorRatio:Eden区和Survior区的占用比例.

 

-XX:PermSize=64M JVM初始分配的永久代内存

-XX:MaxPermSize=128M JVM最大允许分配的永久代内存

 

说明:

1.如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try...catch捕捉。

2.增加Heap的大小虽然会降低GC的频率,但也增加了每次GC的时间

3. 当栈中存储数据比较多时,需要适当调大-Xss这个值,否则会出现java.lang.StackOverflowError异常

 

2、问题:对于JVM内存配置参数-Xms 10240m  -Xmx 10240m -Xmn 5120m  -XXSurvivorRatio=3 Survivor区总大小分别为()、()?

来源: 

 

解决方法:

1:-Xms 10240m:指定JVM 堆(heap) 最小值为10240m.

2:-Xmx 10240m:指定 JVM 堆(heap) 最大值为 10240m. 即最小和最大值

  相同,指定堆不可扩展。

3:-Xmn 5120m:指定新生代(Young Generation)大小为 5120m。

4:-XX:SurvivorRatio = 3:指定新生代中Eden区和一个Survivor 区的空间比例为:3:1。

 

下图为JVM Heap简单的内存分布图

整个Java 堆分为三个区域:新生代,老年代,永久代(新版的Hotspot JVM去了永久代),本题只涉及新生代。新生代分为一个Eden 区和两个大小相同的Survivor区,用来实现复制GC算法。根据上面的参数,新生代为5120m,Eden区和一个Survivor区为 3 : 1。可算得一个Survivor区大小为: 5120/5 = 1024m。两个Survivor区的大小相同,答案为:1024m 1024m。

 

二,JVM垃圾回收(GC)问题

1GC算法

GC判断对象是否存活算法

 引用计数算法

 根搜索算法(GC Root)可达性分析法

 

GC垃圾收集算法

 复制算法(Copying)新生代Eden和两个存活区,一次Minor GC后会将伊甸园中存活的对象复制到存活区,将内存划分成大小相等的两块,每次只使用其中的一块,当需要清理时,就直接将存活的对象复制到另一块。这种方式实现简单效率高,也不会存在碎片问题,缺点:实际可用内存缩小为原来的一半。

 标记清除算法(Mark-Sweep)老生代:分为标记和清除两个阶段,首先标记出可以回收的对象,标记完后统一回收。缺点如下:

    a) 效率低:标记和清除过程效率都不高;

    b) 空间问题:清除之后产生大量不连续的内存碎片。

 标记整理算法(Mark-Compact):与标记清除相似,不相点在于整理对将存活的对象向一端进行移动,不会产生碎片。该算法主要是为了解决标记-清除,产生大量内存碎片的问题

 分代收集算法(Generational Collection):根据存活周期的不同将内存划分为几块。

 

JVM虚拟机GC回收算法

 分代收集:分为年青代和老年代

 复制收集:年青代使用了2个幸存区来实现复制(默认比例:8:1:1,其中1用于交换,因此实际可用为90%)

 标记整理:老年代由于对象的存活率高,所以适合使用标记整理或标记清除来进行回收

 

2、为什么要区分新生代和老生代?不同代采用的算法区别?

问题描述:

 

解决方法:

堆中区分的新生代和老年代是为了垃圾回收,新生代中的对象存活期一般不长,而老年代中的对象存活期较长,所以当垃圾回收器回收内存时,新生代中垃圾回收效果较好,会回收大量的内存,而老年代中回收效果较差,内存回收不会太多。

基于以上特性,新生代中一般采用复制算法,因为存活下来的对象是少数,所需要复制的对象少,而老年代对象存活多,不适合采用复制算法,一般是标记整理标记清除算法

因为复制算法需要留出一块单独的内存空间来以备垃圾回收时复制对象使用,所以将新生代分为eden区和两个survivor区,每次使用eden和一个survivor区,另一个survivor作为备用的对象复制内存区。

3、minor GC和Full GC的触发时机?

Minor GC触发条件:新生代中

(1)程序调用System.gc时可以触发;

(2)系统自身来决定GC触发的时机。

Minor GC触发条件:当Eden区满时,触发Minor GC(年轻代 )。

 

Full GC触发条件:全局下

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法区空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

4、为什么java要有垃圾回收

解决方法:它使java程序员在编程的时候不用再考虑内存空间的使用情况,垃圾回收(GC)会在不可预知的情况下对已经死亡或者长期未使用的对象进行清查和回收。

5、如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

不会,在下一个垃圾回收周期中,这个对象将是可被回收的。

6、如和判断一个对象是否存活?(或者GC对象的判定方法)

判断一个对象是否存活有两种方法:

(1)引用计数法

所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.

引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

 

(2)可达性算法(引用链法)

该算法的思想是:从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。

 

7、在java中可以作为GC Roots的对象有以下几种:

(1)虚拟机栈中引用的对象

(2)方法区类静态属性引用的对象

(3)方法区常量池引用的对象

(4)本地方法栈JNI引用的对象

虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象并不一定会被回收。当一个对象不可达GC Root时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记。如果对象在可达性分析中没有与GC Root的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。

如果该对象有必要执行finalize()方法,那么这个对象将会放在一个称为F-Queue的对队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()执行缓慢或者发生了死锁,那么就会造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。

9、简述java内存分配与回收策率以及Minor GC和Major GC

来源:

 

解决方法:

对象优先在堆的Eden区分配。

大对象直接进入老年代.

长期存活的对象将直接进入老年代.

当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor Gc通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生Gc的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度

10、Java引用的四种状态

强引用:

  用的最广。我们平时写代码时,new一个Object存放在堆内存,然后用一个引用指向它,这就是强引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

 

软引用:

  如果一个对象只具有软引用,则内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。(备注:如果内存不足,随时有可能被回收。)

只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

 

弱引用:

  弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。

  每次执行GC的时候,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

 

虚引用:

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。

 

附加:如果一个对象的应用类型有多个,如何判断它的可达性?

答:单弱多强

  • 单条引用链的可达性以最弱的一个引用来决定
  • 多条引用链的可达性以最强的一个引用来决定
  1. 如何减少GC出现的次数
  1. 对象不用时最好显示置为null
  2. 减少system.gc
  3. 尽量少使用静态变量
  4. 尽量使用StringBuffer,而不用string来累加字符串
  5. 分散对象创建和删除时间
  6. 减少finalize函数
  7. 尽量使用基本类型,少使用封装类型
  1. 创建对象的方法
  • new方法创建对象
  • Clone()方法:重复创建对象时使用

原型模式主要用于对象的复制,实现一个cloneable接口或者重写一个clone()方法。

浅拷贝:对值类型的成员变量进行值复制,对引用类型的对象只引用而不复制

深拷贝:对值类型的成员变量进行值复制,对引用类型的对象也进行复制

 

  1. 内存泄露与溢出
  1. 内存泄露:一个对象以及不再使用但是任然占用空间,堆中申请的空间没有被释放

解决方法:避免循环中创建内存。尽早释放无用对象。尽量少用静态变量。用stringBuffer替代String

  1. 内存溢出:空间不足

原因:请求数据量过大。集合中对象被引用,引用完未清理。启动内存参数设置过小。代码中产生死循环

解决:设置JVM启动参数。查看错误日志。检查代码。Jconsole等内存检查工具

 

13、垃圾收集器


新生代:serial  parnew

老生代: serialOld  parOld

 

解决方法:

(1)Serial收集器:(串行收集器)

 

这个收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop-The-World:将用户正常工作的线程全部暂停掉),直到它收集结束。收集器的运行过程如下图所示:

 

上图中:

 

新生代采用复制算法,Stop-The-World

老年代采用标记-整理算法,Stop-The-World

当它进行GC工作的时候,虽然会造成Stop-The-World,但它存在有存在的原因:正是因为它的简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,没有线程交互的开销,专心做GC,自然可以获得最高的单线程手机效率。所以Serial收集器对于运行在client模式下是一个很好的选择(它依然是虚拟机运行在client模式下的默认新生代收集器)。

(2)ParNew收集器:Serial收集器的多线程版本(使用多条线程进行GC)

 

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

 

  它是运行在server模式下的首选新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS收集器是一个被认为具有划时代意义的并发收集器,因此如果有一个垃圾收集器能和它一起搭配使用让其更加完美,那这个收集器必然也是一个不可或缺的部分了。收集器的运行过程如下图所示:

 

上图中:

 

新生代采用复制算法,Stop-The-World

老年代采用标记-整理算法,Stop-The-World

 

 

(3)ParNew Scanvenge收集器

 

  类似ParNew,但更加关注吞吐量。目标是:达到一个可控制吞吐量的收集器。

 

停顿时间和吞吐量不可能同时调优。我们一方买希望停顿时间少,另外一方面希望吞吐量高,其实这是矛盾的。因为:在GC的时候,垃圾回收的工作总量是不变的,如果将停顿时间减少,那频率就会提高;既然频率提高了,说明就会频繁的进行GC,那吞吐量就会减少,性能就会降低。

 

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

 

 

 

(4)G1收集器:

 

  是当今收集器发展的最前言成果之一,知道jdk1.7,sun公司才认为它达到了足够成熟的商用程度。

 

优点:

  它最大的优点是结合了空间整合,不会产生大量的碎片,也降低了进行gc的频率。

  二是可以让使用者明确指定指定停顿时间。(可以指定一个最小时间,超过这个时间,就不会进行回收了)

 

它有了这么高效率的原因之一就是:对垃圾回收进行了划分优先级的操作,这种有优先级的区域回收方式保证了它的高效率。

 

如果你的应用追求停顿,那G1现在已经可以作为一个可尝试的选择;如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。

 

注:以上所有的收集器当中,当执行GC时,都会stop the world,但是下面的CMS收集器却不会这样。

 

 

 

(5)CMS收集器:(老年代收集器)

 

CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。

 

CMS收集器运行过程:(着重实现了标记的过程)

 

①初始标记

  根可以直接关联到的对象

  速度快

②并发标记(和用户线程一起)

  主要标记过程,标记全部对象

③重新标记

由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正

④并发清除(和用户线程一起)

基于标记结果,直接清理对象

整个过程如下图所示:

上图中,初始标记和重新标记时,需要stop the world。整个过程中耗时最长的是并发标记和并发清除,这两个过程都可以和用户线程一起工作。

 

优点:并发收集,低停顿

 

缺点:

(1)导致用户的执行速度降低。

(2)无法处理浮动垃圾。因为它采用的是标记-清除算法。有可能有些垃圾在标记之后,需要等到下一次GC才会被回收。如果CMS运行期间无法满足程序需要,那么就会临时启用Serial Old收集器来重新进行老年代的手机。

(3)由于采用的是标记-清除算法,那么就会产生大量的碎片。往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次full GC

 

疑问:既然标记-清除算法会造成内存空间的碎片化,CMS收集器为什么使用标记清除算法而不是使用标记整理算法:

 

答案:

 

CMS收集器更加关注停顿,它在做GC的时候是和用户线程一起工作的(并发执行),如果使用标记整理算法的话,那么在清理的时候就会去移动可用对象的内存空间,那么应用程序的线程就很有可能找不到应用对象在哪里。

14jvm内存模型

①堆:所有线程共享的一块内存区域,对象实例几乎都在这分配内存

②虚拟机栈:线程私有的,与线程生命周期相同,用于存储局部变量表,操作栈方法返回值。局部变量表放着基本数据类型,还有对象的引用。

③本地方法栈:跟虚拟机栈很像,不过它是为虚拟机使用到的Native方法服务。

④方法区(java8取消该方法):各个线程共享的区域,储存虚拟机加载的类信息,常量,静态变量,编译后的代码。

⑤程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java虚拟机的多线程就是通过线程轮流切换并分配处理器时间来实现的,为了线程切换后能恢复到正确的位置,每条线程都需要一个独立的程序计数器,互不影响,该区域为“线程私有”。

15、JAVA 中堆和栈的区别,说下java 的内存机制

a.基本数据类型比如变量和对象的引用都是在栈分配的

 

b.堆内存用来存放由new创建的对象和数组

 

c.类变量(static修饰的变量),程序在一加载的时候就在堆中为类变量分配内存,堆中的内存地址存放在栈中

 

d.实例变量:当你使用java关键字new的时候,系统在堆中开辟并不一定是连续的空间分配给变量,是根据零散的堆内存地址,通过哈希算法换算为一长串数字以表征这个变量在堆中的”物理位置”,实例变量的生命周期–当实例变量的引用丢失后,将被GC(垃圾回收器)列入可回收“名单”中,但并不是马上就释放堆中内存

 

e.局部变量: 由声明在某方法,或某代码段里(比如for循环),执行到它的时候在栈中开辟内存,当局部变量一但脱离作用域,内存立即释放

三,JVM类问题

1、java类加载过程?

来源:

 

解决方法:

java类加载需要经历一下7个过程:

(1)加载

加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:

1. 通过一个类的全限定名获取该类的二进制流

2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。

3. 在内存中生成该类的Class对象,作为该类的数据访问入口

 

(2)验证

验证的目的是为了确保Class文件的字节流中的信息不危害到虚拟机.在该阶段主要完成以下四钟验证:

1. 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

2. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

 

(3)准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。

 

(4)解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

 

(5)初始化

初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码

 

2、类加载器双亲委派模型机制?

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

 

Java类加载器的工作原理及其组织结构:

Java类加载基于三个机制:委托性,可见性和单一性

①委托机制是指双亲委派模型:当一个雷加载和初始化的时候,类仅仅在需要的时候被加载。加载一个类的请求时由application委托给extension,然后再委托给bootstrap类加载。bootstrap类中如果没有这个类,这个请求又回到extension,如果extension也没有,会回到

application类中,如果application还是没有找到,会抛出异常。

双亲委派的优点:提高软件系统的安全性。因为用户自定义的类加载器不可能加载本应该由父类加载器加载的可靠类。

Java类加载器有三个:

启动类加载器 Bootstrap ClassLoader:负责加载系统类(指的是内置类,像string)

扩展类加载器 Extension ClassLoader:负责加载扩展类(继承类和实现类)

应用程序类加载器 Application ClassLoader:负责加载应用类(程序自定义的类)

②可见性:是子类加载器可以看到父类加载器加载的类,而父类加载器看不到子类加载器加载的类。

③单一性:指仅加载类一次,确保子类加载器不会加载已经被父类加载过的类。

 

双亲委派用到的方法:

FindLoadedClass()

loadClass()

findBootstrapClassOrNull()

findClass()

defineClass():把二进制数据转换成字节码

ResolveClass()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值