Java面试题及答案(四)

Java面试题及答案(四) 

目录

Java面试题及答案(四) 

目录  

 

             31.垃圾回收算法的实现原理。  

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

             33.调优工具都有哪些

             34.JVM调优

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

    36.简单说说你了解的类加载器。

    37.讲讲 JAVA 的反射机制

    38.如何判断一个对象是否应该被回收

    39.g1 和 cms 区别,吞吐量优先和响应优先的垃圾收集器选择。

    40.引用的分类

 


 31.垃圾回收算法的实现原理。   

1. 对象生死判定

    如何判断Java中一个对象应该“存活”还是“死去”,这是垃圾回收器要做的第一件事。

1)引用计数法

    Java堆中每个具体对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

  • 优点

        引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。

  • 缺点

        难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。

2)可达性分析法

    可达性分析算法也叫根搜索算法,通过一系列的称为GC Roots的对象作为起点,然后向下搜索。搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,即该对象不可达,也就说明此对象是不可用的。

如下图所示:Object5、Object6、Object7虽然互有关联虽然互有关联,但他们到GC Roots是不可达的,因此也会被判定为可回收的对象。

GC根对象

    在Java中,可作为GC Roots的对象包括以下4种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中 JNI(Native方法)引用的变量
  • 方法区中类静态属性引用的变量
  • 方法区中常量引用的变量

    JVM中用到的所有现代GC算法在回收前都会先找出所有仍存活的对象。可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图。下图展示的JVM中的内存布局可以用来很好地阐释这一概念:

2. 对象引用分类

1)强引用(Strong Reference)

    在代码中普遍存在,类似于 User user = new User()这类引用,只要强引用还在,垃圾收集器永远不会回收被引用的对象。

2)软引用(Soft Reference)

    有用但并非必需的对象,可用SoftReference类来实现软引用。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

3)弱引用(Weak Reference)

    非必须的对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,JDK提供了WeakReference类来实现弱引用。无论当前内存是否足够,用软引用相关联的对象都会被回收掉。

4)虚引用(Phantom Reference)

    虚引用也称为幽灵引用或幻影引用,是最弱的一种引用关系,JDK提供了PhantomReference类来实现虚引用。作为一个对象设置虚引用的唯一目的是:能在这个对象在垃圾回收器回收时收到一个系统通知。

3 finalize() 二次标记

    一个对象是否在垃圾回收器在GC时回收,至少要经历两次标记过程。

第一次标记过程,通过可达性分析算法分析对象是否与GC Roots可达。经过一次标记,并且被筛选为不可达的对象会进行第二次标记。

第二次标记过程,判断不可达对象是否有必要执行finalize方法。执行条件是当前对象的finalize方法被重写,并且还未被系统调用过。如果允许执行那么这个对象将会被放到一个叫F-Query的队列中,等待被执行。

4 垃圾回收算法

1)标记-清除算法

    标记-清楚算法对根集合进行扫描,对存活的对象进行标记。标记完成后,再对整个空间内未被标记的对象扫描,进行回收。

  • 优点

        实现简单,不需要对对象进行移动。

  • 缺点

        标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。

2)复制算法

    将内存区域划分成相同的两个内存块。每次仅使用一半的空间,JVM生成的新对象放在一半空间中。当一半空间用完时进行GC,把可到达对象复制到另一半空间,然后把使用过的内存空间一次清理掉。这种收集算法解决了标记清除算法存在的效率问题。

  • 优点

        按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

  • 缺点

        可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

3)标记-整理算法

    标记-整理算法采用标记-清除法一样的方式进行对象的标记,但后续不直接对可回收对象进行清理,而是将所有的存活对象往一端空闲空间移动,然后清理掉端边界以外的内存空间。

  • 优点

        解决了标记-清理算法存在的内存碎片问题。

  • 缺点

        仍需要进行局部对象移动,一定程度上降低了效率。

4)分代收集算法

    当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代和永久代,如图所示:

新生代(Young Generation)

    绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程,我们称之为Mirror GC。

    新生代中存在一个Eden区和两个Survivor区。新对象会首先分配在Eden中(如果新对象过大,会首先分配在老年代中)。在GC中,Eden对象会被移到Survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代。

    可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如

-XX:NewRatio=8 表示 老年代/新生代=8:1;老年代占堆大小的7/8,新生代占堆大小的1/8(默认即是1/8)。举个配置的例子:

-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8

老年代(Old Generation)

    对象没有变得不可达,并且从新生代中存活了下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代的要少得多。对象从老年代中消失的过程,可以称之为Major GC/Full GC。

永久代

    像一些类的层级信息,方法数据和方法信息(如字节码、栈和变量大小),运行时常量池(JDK7之后移出永久代),已确定的符号引用和虚方法表等等。他们几乎都是静态的并且很少被卸载和回收,在JDK8之前的HotSpot虚拟机中,类的这些“永久的”数据存放在一个叫做永久代的区域。

    永久代一段连续的内存空间,我们在JVM启动之前,可以通过设置-XX:MaxPermSize的值来控制永久代的大小。但是JDK8之后取消了永久代,这些元数据被移到了一个与堆不相连的成为元空间(Metaspace)的本地内存区域。

在这里插入图片描述

详情内容可参考

动态展示详情可参考

详细内容可参考:https://www.cnblogs.com/lwkdbk/p/13191890.html

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

对于jvm虚拟机内存中,除了程序计数器意外,其他虚拟机栈、本地方法栈,方法区、堆都存在溢出的可能。

1.堆溢出

java堆是用来存储对象的,只要保证不断的生成对象,而且让他们的GC root不断掉,而不会引起垃圾回收,这样堆就会由于对象的不断创建而不断变大而溢出。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。先通过内存映像分析,判断是内存溢出还是泄漏。

如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。

如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

-Xmx用来设置你的应用程序(不是JVM)能够使用的最大内存数,如果你的程序要花很大内存的话,那就需要修改缺省的设置,比如配置tomcat的时候,如果流量啊程序啊都很大的话就需要加大这个值了,BUT不要大得超过你的机器的内存。

另一个-Xms用来设置程序初始化的时候内存栈的大小,增加这个值的话你的程序的启动性能会得到提高。不过同样有前面的限制,以及受到-Xmx的限制。

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

通过-xss可以设置栈的大小

当线程所需要的栈深度大于虚拟机所设置的允许的栈大小时候,会出现StackOverflowError异常。

当虚拟机想要扩充栈的数量而缺少内存空间时候,就会出现outofmemoryError异常。

3.运行时常量池溢出

方法区与java堆一样,是各个线程共享的内存区域,它用于储存已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

方法区中其实还有一块运行常量池,当class文件中的常量池在类加载之后就被放入运行常量池,运行常量池还可以通过String.intern将常量放入进去,因此其具有动态性,一旦方法区中空间不足时候会抛出OutofMemoryerror异常。

4.方法区溢出

通过不停的类加载去产生大量的类,造成方法区中存放太多的类信息等而溢出、

设置-XX:PermSize持久代初始值和-XX:MaxPermSize持久代最大值参数

5.元数据溢出

在jdk1.8中方法区溢出变成了元数据溢出

6.直接内存溢出

DirectMemory 容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与 Java 堆最大值(-Xmx指定)一样,下面程序利用 DirectByteBuffe 模拟直接内存溢出的情况。

  • Hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象。
  • 使用 Netty 的堆外的 ByteBuf 对象,在使用完后,并未归还,导致使用的一点一点在泄露

排错

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

33.调优工具都有哪些

 常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

  • jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
  • jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
  • MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
  • GChisto,一款专业分析gc日志的工具

34.JVM调优

1.监控GC的状态

使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。

举一个例子: 系统崩溃前的一些现象:

  • 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
  • FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
  • 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放

之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。

2.生成堆的dump文件

通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

3.分析dump文件

打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件:

  • Visual VM
  • IBM HeapAnalyzer
  • JDK 自带的Hprof工具
  • Mat(Eclipse专门的静态内存分析工具)推荐使用

备注:文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。

4.分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。

注:如果满足下面的指标,则一般不需要进行GC:

  • Minor GC执行时间不到50ms;
  • Minor GC执行不频繁,约10秒一次;
  • Full GC执行时间不到1s;
  • Full GC执行频率不算频繁,不低于10分钟1次;

5.调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

6.不断的分析和调整

通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。

 

 


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

重排序:为了提高性能,编译器和处理器会对执行进行重拍

内存屏障:为了保障执行顺序和可见性的一条cpu指令  
happen-before:操作间执行的顺序关系。有些操作先发生。 
主内存:共享变量存储的区域即是主内存 
工作内存:每个线程copy的本地内存,存储了该线程以读/写共享变量的副本

详情内容可参考:http://ifeve.com/java-memory-model-1/ 
http://www.jianshu.com/p/d3fda02d4cae 
http://blog.csdn.net/kenzyq/article/details/50918457


    36.简单说说你了解的类加载器。

1.类的加载过程  

JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤,如下图所示:

1) 装载:查找并加载类的二进制数据;

2)链接:

验证:确保被加载类的正确性;

准备:为类的静态变量分配内存,并将其初始化为默认值;

解析:把类中的符号引用转换为直接引用;

3)初始化:为类的静态变量赋予正确的初始值;

          那为什么我要有验证这一步骤呢?首先如果由编译器生成的class文件,它肯定是符合JVM字节码格式的,但是万一有高手自己写一个class文件,让JVM加载并运行,用于恶意用途,就不妙了,因此这个class文件要先过验证这一关,不符合的话不会让它继续执行的,也是为了安全考虑吧。

        准备阶段和初始化阶段看似有点牟盾,其实是不牟盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

2. 类的初始化

    类什么时候才被初始化:

1)创建类的实例,也就是new一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4)反射(Class.forName("com.lyj.load"))

5)初始化一个类的子类(会首先初始化子类的父类)

6)JVM启动时标明的启动类,即文件名和类名相同的那个类

         只有这6中情况才会导致类的类的初始化。

     类的初始化步骤:

        1)如果这个类还没有被加载和链接,那先进行加载和链接

        2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)

         3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。

3.类的加载

       类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。看下面2图

         类的加载的最终产品是位于堆区中的Class对象
        Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口

 

加载类的方式有以下几种:

 1)从本地系统直接加载

2)通过网络下载.class文件

3)从zip,jar等归档文件中加载.class文件

4)从专有数据库中提取.class文件

5)将Java源文件动态编译为.class文件(服务器)

4.加载器

来自http://blog.csdn.net/cutesource/article/details/5904501

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

1)Bootstrap ClassLoader

负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader

负责记载classpath中指定的jar包及目录中class

4)Custom ClassLoader


    37.讲讲 JAVA 的反射机制

Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键
http://blog.csdn.net/gjanyanlig/article/details/6818655/


    38.如何判断一个对象是否应该被回收

 判断对象是否存活一般有两种方式:

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。


    39.g1 和 cms 区别,吞吐量优先和响应优先的垃圾收集器选择。

区别一: 使用范围不一样

CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用

区别二: STW的时间

CMS收集器以最小的停顿时间为目标的收集器。

G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)

区别三: 垃圾碎片

CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片

G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

区别四: 垃圾回收的过程不一样

    40.引用的分类

  • 强引用:GC时不会被回收
  • 软引用:描述有用但不是必须的对象,在发生内存溢出异常之前被回收
  • 弱引用:描述有用但不是必须的对象,在下一次GC时被回收
  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用用来在GC时返回一个通知

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值