Java面试题-JVM

Java面试题

JVM

1.JVM中的内存是怎么划分的

1.方法区(method area)
  是一个线程之间共享区域,主要存放静态变量、常量、类信息和即时编译代码,也可以称为永久代,
  垃圾回收效果一般。
2.(heap)
  垃圾回收的主要场所,也是线程之间共享区域,主要存放创建的对象实例。
3.虚拟机栈(Java Virtal Machine Stack)
  主要存放局部变量、基本类型数据和堆内存中某个对象的引用。每个方法在执行的同时都会在虚拟机栈中创建
  一个栈帧用于存储本地变量表、操作数栈、本地链接和方法出口等。栈帧会随着方法进入和退出执行着出栈和
  入栈的操作。
4.本地方法栈(native method stack)
  提供本地方法服务。
5.程序计数器(program Counter Register)
  字节码文件的行号指示器,唯一一个不会出现OOM的地方。

2.说一下对象创建过程中的内存分配

对象的内存分配有两种方式分别是:
1.指针碰撞
  假如堆内存是绝对工整的,一边是被使用过的内存,另一边是未被使用过的内存,在中间会维护一个指针,
  当有对象创建的时候,该指针就会往空闲内存的那一边移动和对象一样大的内存距离。
2.空闲列表
  假如堆内存不是工整的,使用过和未使用过的内存交织在一起,虚拟机就必须维护一张表来存储堆内存的
  使用情况,当有对象创建的时候就会在表中找出足够的内存区域来存储对象,并更新表格。
拓展:内存分配是怎么保证线程安全的。
     答案:1.通过CAS+失败重试的操作来保证更新指针的原子性
          2.给每个线程分配一小段内存,称为本地变量存储,来保证安全。

3.对象被访问的时候是怎么被找到的

1.句柄访问
  在堆内存中会开辟一个句柄池来存放每个对象的句柄地址,在虚拟机栈中的引用变量存放的就是句柄地址,
  句柄地址不直接,访问速率慢。
2.直接指针访问
  虚拟机栈中的引用变量存放的是对象在堆内存中的直接地址,该方式访问直接,效率快;也是现在JVM访问
  对象的首选方式。

4.内存分配与回收过程

JVM的内存可以分为堆内存和非堆内存。堆内存分为年轻代和老年代。
年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。
1.JVM堆内存的分配:
  JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64。JVM最大分配的堆内存由-Xmx指定,
  默认是物理内存的1/4。
  默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 
  -  Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小。
  通过参数-Xmn2G 可以设置年轻代大小为2G。通过-XX:SurvivorRatio可以设置年轻代中Eden区
  与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为81。
  注意年轻代中有两块Survivor区域。
2.JVM非堆内存的分配:
  JVM使用-XX:PermSize 设置非堆内存初始值,默认是物理内存的1/64。
  由-XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/43.堆内存上对象的分配与回收:
  我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)
  则可以直接进入老年 代。虚拟机提供一个
  -XX:PretenureSizeThreshold参数,令大于这个参数值的对象直接在老年代中分配,
  避免在Eden区和两个Survivor区发生大量的内存拷贝。

  另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),
  对象年龄就大一岁,默认15岁晋升到老年代,通过
  -XX:MaxTenuringThreshold设置晋升年龄。


  堆内存上的对象回收也叫做垃圾回收,那么垃圾回收什么时候开始呢?

  垃圾回收主要是完成清理对象,整理内存的工作。上面说到GC经常发生
  的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个
  Survivor区。垃圾回收分为年轻代区域发生的Minor GC和
  老年代区域发生的Full GC,分别介绍如下。

  Minor GC(年轻代GC):
  对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,
  因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。

  Full GC(老年代GC):
  Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,
  发生Full GC一般都会有一次Minor GC。


  接下来,我们来看关于内存分配与回收的两个重要概念吧。


  动态对象年龄判定:

  如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,
  那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到
  - XX:MaxTenuringThreshold。


  空间分配担保:

  发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是
  否大于老年代的剩余空间大小。如果大于,则进行一次Full GC(老年代GC),
  如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,
  那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。

5.JVM如何判定一个对象是否应该被回收

1.引用计数法
  通过一个引用计数器来实现,多一个引用就+1,少一个引用就-1,
  当计数器为0时,说明对象不太可能被用到,所以就会被回收掉。
2.可达性分析法
  可达性分析法通过一系列GCroots来搜索,如果所有的GCroots
  都没有可达的路径,这个对象被称为不可达。要注意的是不可达
  不代表对象就可以被回收,回收对象至少要经过两次标记。
拓展:Java中四大引用类型分别是什么。
     答案:1.强引用:普通存在, P p = new P(),只要强引用存在,
            垃圾收集器永远不会回收掉被引用的对象。
          2.软引用:通过SoftReference类来实现软引用,
            在内存不足的时候会将这些软引用回收掉。
          3.弱引用:通过WeakReference类来实现弱引用,
            每次垃圾回收的时候肯定会回收掉弱引用。
          4.虚引用:也称为幽灵引用或者幻影引用,
            通过PhantomReference类实现。设置虚引用只是为了
            对象被回收时候收到一个系统通知。

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

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

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

eden 和 survior 是默认按81分配的。
拓展1:显而易见eden和survior都是年轻代的,那么为什么会有年轻代。
      优化GC性能,如果没有分代,那我们所有的对象都在一块,
      GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域
      进行扫描。
拓展2:eden 和 survior 为什么是按默认按81分配的。
      因为年轻代的80%的对象都是要被回收的。

8.新生代中为什么要分为 Eden 和 Survivor。

1.为什么 Survivor 分区不能是 0 个?
  如果 Survivor 是 0 的话,也就是说新生代只有一个 Eden 分区,
  每次垃圾回收之后,存活的对象都会进入老生代,这样老生代的内存空间
  很快就被占满了,从而触发最耗时的 Full GC ,显然这样的收集器的效率
  是我们完全不能接受的。
2.为什么 Survivor 分区不能是 1 个?
  如果 Survivor 分区是 1 个的话,假设我们把两个区域分为 1:1,
  那么任何时候都有一半的内存空间是闲置的,显然空间利用率太低不是
  最佳的方案。
  但如果设置内存空间的比例是 8:2 ,只是看起来似乎“很好”,
  假设新生代的内存为 100 MB( Survivor 大小为 20 MB ),
  现在有 70 MB 对象进行垃圾回收之后,剩余活跃的对象为 15 MB 进入
  Survivor 区,这个时候新生代可用的内存空间只剩了 5 MB,这样很快又要
  进行垃圾回收操作,显然这种垃圾回收器最大的问题就在于,需要频繁进行垃圾回
  收。
3.为什么 Survivor 分区是 2 个?
  如果 Survivor 分区有 2 个分区,我们就可以把 Eden、
  From Survivor、To Survivor 分区内存比例设置为 8:1:1 ,
  那么任何时候新生代内存的利用率都 90% ,这样空间利用率基本是符合
  预期的。再者就是虚拟机的大部分对象都符合“朝生夕死”的特性,所以每次新
  对象的产生都在空间占比比较大的 Eden 区,垃圾回收之后再把存活的对象方法
  存入 Survivor 区,如果是 Survivor 区存活的对象,那么“年龄”就 +1 ,当
  年龄增长到 15 (可通过 -XX:+MaxTenuringThreshold 设定)对象就升级到
  老生代。
总结
根据上面的分析可以得知,当新生代的 Survivor 分区为 
2 个的时候,不论是空间利用率还是程序运行的效率都是最优的,
所以这也是为什么 Survivor 分区是 2 个的原因了。

9.当出现了内存溢出,你怎么排错。

1.首先控制台查看错误日志
2.然后使用jdk自带的jvisualvm工具查看系统的堆栈日志
3.定位出内存溢出的空间:堆,栈还是永久代(jdk8以后不会出现永久代
  的内存溢出)。
4.如果是堆内存溢出,看是否创建了超大的对象
5.如果是栈内存溢出,看是否创建了超大的对象,或者产生了死循环。

10.JVM 内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等。

重排序:JVM虚拟机允许在不影响代码最终结果的情况下,可以乱序执行。

内存屏障:可以阻挡编译器的优化,也可以阻挡处理器的优化。

happens-before原则:
1、一个线程的A操作总是在B之前,那多线程的A操作肯定是在B之前。
2、Monitor再加锁的情况下,持有锁的肯定先执行。
3volatile修饰的情况下,写先于读发生。
4、线程启动在一切之前 strat。
5、线程死亡在一切之后 end。
6、线程操作在一切线程中断之前。
7、一个对象构造函数的结束都在该对象的finalizer的开始之前。
8、传递性,如果A肯定在B之前,B肯定在C之前,那A肯定是在C之前。

主内存:所有线程共享的内存空间。

工作内存:每个线程特有的内存空间。

11.简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。

如果站在JVM的角度来看,只存在两种类加载器:
1.启动类加载器(Bootstrap ClassLoader):由C++语言实现
 (针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath
  参数指定的路径中的类库加载到内存中。
2.其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
  1.扩展类加载器(Extension ClassLoader):负责加载
  <JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的
  路径中的所有类库。
  2.应用程序类加载器(Application ClassLoader):负责加载
  用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。
  一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
  
双亲委派的好处:
JVM区分对象不光是从类名上,不同的类加载,加载的同一类名也不是同一个
对象。所以双亲委派机制就避免了加载同一个类而不能识别的情况,所有的子类
都要交给父类加载器进行加载,这就保证了自定义对象不安全的情况。

打破双亲委派的方法:
打破双亲委派机制则要继承ClassLoader类,并且重写loadClass和
findClass方法

12.你们线上应用的 JVM 参数有哪些。

-server
Xms6000M
-Xmx6000M
-Xmn500M
-XX:PermSize=500M
-XX:MaxPermSize=500M
-XX:SurvivorRatio=65536
-XX:MaxTenuringThreshold=0
-Xnoclassgc
-XX:+DisableExplicitGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSClassUnloadingEnabled
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=90
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log

13.怎么打出线程栈信息。

第一步:在终端运行Java程序。
第二步:通过命令 pidof java 找到已经启动的java进程的ID,
选择需要查看的java程序的进程ID。
第三步:使用命令 kill -3 (java进行的pid) 打印出java程序的
线程堆栈信息。
第四步:通常情况下运行的项目可能会比较大,那么这个时候打印的堆栈信息
可能会有几千到几万行,为了方便查看,我们往往需要将输出内容进行重定向。
使用linux下的重定向命令方式即可:例如: demo.sh > run.log 2>&1 
将输出信息重定向到 run.log中。
注:在操作系统中,0 1 2分别对应着不同的含义, 如下:
0 : 标准输入,即:C中的stdin , java中的System.in
1 : 标准输出,即:C中的stdout ,java中的System.out
2 : 错误输出,即:C中的stderr , java中的System.err
PS: 在JDK1.5以上,我们可以通过在Java程序中调用
Thread.getStackTrace()方法来进行堆栈的自动打印,
使得线程堆栈的打印时机可编程控制。

14.请解释如下 jvm 参数的含义:

-server -Xms512m -Xmx512m -Xss1024K 
-XX:PermSize=256m -XX:MaxPermSize=512m
-XX:MaxTenuringThreshold=20 
XX:CMSInitiatingOccupancyFraction=80 
-XX:+UseCMSInitiatingOccupancyOnly

Server模式启动
最小堆内存512m
最大512m
每个线程栈空间1m
永久代256
最大永久代256
最大转为老年代检查次数20
Cms回收开启时机:内存占用80%
命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期

15.JVM虚拟机有内存泄露怎么解决。

JVM内存泄漏主要的原因就是不用的对象依然存在强引用。解决办法就是
找到相关代码然后进行优化,无用的对象不要存在强引用类型。

16.System.gc() Runtime.gc()会做什么事情? 能保证 GC 执行吗?

java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()
的简写,两者的行为没有任何不同。
调用 gc 方法暗示着 Java 虚拟机做了一些努力来回收未用对象,
以便能够快速地重用这些对象当前占用的内存。当控制权从方法调用中返回时,
虚拟机已经尽最大努力从所有丢弃的对象中回收了空间。

17.垃圾回收器的基本原理是什么?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、
大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)
中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是
"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些
内存空间。
拓展:JVM的永久代中会发生垃圾回收吗
     答案:永久代会垃圾回收,但是没有自己的垃圾收集器,
     是和老年代一起回收的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值