面试之Java虚拟机专题

说下对象的创建方法?对象的内存布局?对象的访问定位?

四种不同的方法创建对象

1、用new语句创建对象,这是最常用的创建对象的方式;

2、调用对象的clone方法。

  MyObject obj =new MyObject();

  MyObject objs= obj.clone();

使用clone方法克隆一个对象的步骤:

  1)被克隆的类要实现Cloneable接口;

  2)被克隆的类要重写clone方法;

 1 class Obj implements Cloneable{
 2     private Date birth = new Date();
 3     public Date getBirth(){
 4        return birth;
 5     }
 6     public void setBirth(){
 7         this.birth=birth;
 8     }
 9     public void changeDate(){
10         this.birth.setMonth(4);
11     }
12     public Object clone(){
13         Obj o = null;//o指向了复制后的新对象
14         try{
15             o=(Obj)super.clone();//实现浅复制
16         }catch(CloneNotSupportedException e){
17             e.printStackTrace();
18         }
19     //实现深复制
20     o.birth = (Date)this.getBirth().clone();
21     return o;
22     }
23 }
24 public class TestRef{
25     public static void main(String[] args){
26         Obj a = new Obj();
27         Obj b = (Obj)a.clone();
28         b.changeDate();
29         System.out.println("a="+a.getBirth());
30         System.out.println("b="+b.getBirth());
31     }
32 }
33 //程序运行结果:
34 //a=Sun Jul 13 23:58:56 CST 2013
35 //b=Mon May 13 23:58:56 CST 2013

  那么在编程时,如何选择使用哪种复制方式呢?首先,检查类有无非基本类型(即对象)的数据成员。若没有,则返回super.clone()即可;若有,确保类中包含的所有非基本类型的成员变量都实现了深复制。

    Object o = super.clone();//先执行浅复制

    对每一个对象attr执行以下语句:

    o.attr = this.getAttr().clone();

    最后返回。

  需要注意的是,clone方法的保护机制在Oject中clone()是被声明为protected的,以User类为例,通过声明为protected,就可以保证只有User类里面才能“克隆”User对象。

引申:浅复制和深复制有什么区别?

  浅复制:被复制的对象的所有变量都含有与原来对象相同的值,而所有其他对象的引用仍然指向原来的对象。换而言之,浅复制仅仅复制所考虑的对象,而不复制他引用的对象。

  深复制:被复制对象的所有变量都含有与原来对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制的新对象,而不再是原有的那些被引用的对象。换而言之,深复制把复制的对象所引用的对象都复制了一遍。

扩展:

原型模式主要用于对象的复制,实现一个接口(实现Cloneable接口),重写一个方法(重写Object类中的clone方法),即完成了原型模式。

原型模式中的拷贝分为“浅拷贝”和“深拷贝”:

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

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

  (Object类中clone方法只会拷贝对象中的基本数据类型的值,对于数据中、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。)

原型模式的优点:

  1)如果创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程。

  2)使用原型模式创建对象比直接new一个对象在性能上要好得多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。

原型模式的适用场景:

  因为以上优点,所以在需要重复地创建相似对象时可以考虑使用原型模式。比如需要在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多。

3、运行反射手段,使用Class.forName()

  MyObject object =(MyObject)Class.forName("subin.rnd.MyObject").newInstance();

4、运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。

什么是内存泄漏和内存溢出?

内存泄漏:指一个不再被程序使用的对象或变量还在内存中占有存储空间。

 两种情况:

  1)在堆中申请的空间没有被释放;

  2)对象已不再被使用但还仍然在内存中保留着;

内存泄露的典型例子是一个没有重写hashCode和equals方法的Key类在HashMap中保存的情况,最后会生成很多重复的对象。所有的内存泄漏最后都会抛出OutOfMemoryError异常。

造成内存泄漏的原因:

  1)静态集合类

  2)各种连接,例如数据库连接等

  3)监听器

  4)变量不合理的作用域

内存泄露的解决方案:

  1)避免在循环中创建对象;

  2)尽早释放无用对象的引用;

  3)尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收;

  4)使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域;

在实际场景中,你怎么查找内存泄漏?

  可以使用 Jconsole。

内存溢出:指程序运行过程中无法申请到足够的内存而导致的内存的一种错误。

内存溢出的几种情况(OOM异常):

OutOfMemoryError异常:

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

  1、虚拟机栈和本地方法栈溢出

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

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

  2、堆溢出

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

  解决方案:

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

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

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

  3、方法区溢出

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

  4、运行时常量池溢出

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

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

  导致内存溢出的原因:

    1)内存中加载的数据量过大,如一次从数据库取出过多数据;

    2)集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

    3)代码中存在死循环或循环产生过多重复的对象实体;

    4)启动参数内存值设定的大小。

  内存溢出的解决方法:

    第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。一般要将-Xms和-Xmx选项设置为相同,以避免在每次GC后调整堆的大小;建议堆的最大值设置为可用的内存的最大值的80%)。

    第二步,检查错误日志,查看“OutOfMemory”错误前是否有其他异常或错误。

    第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

    第四步,使用内存查看工具动态查看内存使用情况(Jconsole)。

如何减少GC出现的次数?(Java内存管理)

1)对象不用时最好显式置为NULL

  一般而言,为NULL的对象都会被作为垃圾处理,所以将不同的对象显式地设为NULL,有利于GC收集器判定垃圾,从而提高了GC的效率。

2)尽量少用System.gc()

  此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

3)尽量少用静态变量

  静态变量属于全局变量,不会被GC回收,他们会一直占用内存。

4)尽量使用StringBuffer,而不用String来累加字符串。

  由于String是固定长度的字符串,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如str5=str1+str2+str3+str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须产生新String对象,但这些过渡对象对系统来说没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

5)分散对象创建或删除的时间

  集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,已回收内存或整合内存碎片,从而增加主GC的频率。

  集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

6)尽量少用finalize函数。因为它会加大GC的工作量,因此尽量少用finalize方式回收资源。

7)如果需要使用经常用到的照片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory。

8)能用基本类型如int,long,就不用Integer,Long对象

  基本类型变量占用的内存资源比相应包装类对象占用的少得多,如果没有必要,最好使用基本变量。

9)增大-Xmx的值。

数组多大放在JVM老年代?永久带对象如何GC?如果想不被GC怎么办?如果想在GC中生存一次怎么办?

  虚拟机提供了一个-XX:PretenureSizeThreshold参数(通常是3MB),令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

  垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(FullGC)。如果仔细查看垃圾回收器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免FullGC是非常重要的。

  让对象实现finalize()方法,一次对象的自我拯救。

JVM常见的启动参数有哪些?

  -Xms:设置堆的最小值。

  -Xmx:设置堆的最大值。

  -Xmn:设置新生代的大小。

  -Xss:设置每个线程的栈大小。

  -XX:NewSize:设置新生代的初始值

  -XX:MaxNewSize:设置新生代的最大值

  -XX:PermSize:设置永久代的初始值

  -XX:MaxPermSize:设置永久代的最大值

  -XX:SurvivorRatio:年轻代中Eden区与Survivor区的大小比值

  -XX:PretenureSizeThreshold:零大于这个设置值的对象直接在老年代分配。

说下几种常用的内存调试工具:jps、jump、jhat、jstack、jconsole、jstat

  Java内存泄露的问题调查方法:jmap,jstack的使用等等。

  jps:查看虚拟机进程的状况,如进程ID。

  jmap:用于生成堆转储快照文件(某一时刻的)。

  jhat:对于生成的堆转储快照文件进行分析。

  jstack:用来生成线程快照(某一时刻的)。生成线程快照的主要目的是定位线程长时停顿的原因(如死锁,死循环,等待I/O等),通过查看各个线程的调用堆栈,就可以知道没有响应的线程在后台做了什么或者等待什么资源。

  jstat:虚拟机统计信息监视工具。如显示垃圾收集的情况,内存使用得情况。

  Jconosole:主要是内存监控和线程监控。内存监控:可以显示内存的使用情况。线程监控:遇到线程停顿时,可以使用这个功能。

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

  Java类加载器的作用就是在运行时加载类。

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

  1、委托机制是指双亲委派模型。当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。假设你有一个应用需要的类叫作abc.class,首先加载这个类的请求由Application类加载器委托给它的父类加载器Extension类加载器,然后再委托给Bootstrap类加载器。Bootstrap类加载器会先看看rt.jar中有没有这个类,因为并没有这个类,所以这个请求又回到Extension类加载器,它会查看jre/lib/ext目录下有没有这个类,如果这个类被Extension类加载器找到了,那么它将被加载,而Application类加载器不会加载这个类;而如果这个类没有被Extension类加载器找到,那么再由Application类加载器从classpath中寻找,如果没找到,就会抛出异常。

  双亲委派模型机制的优点就是能够提高软件系统的安全性。因为在此机制下,用户自定义的类加载器不可能加载本应该由父类加载器加载的可靠类,从而防止不可靠的恶意代码代替由父类加载器加载的可靠代码。如java.lang.Object类总是由根类加载器加载的,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。

  2、可见性原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。

  3、单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。

Java的类加载器有三个,对应Java的三种类:

  Bootstrap Loader  //负责加载系统类(指的是内置类,像String)

  ExtClassLoader  //负责加载扩展类(就是继承类和实现类)

  AppClassLoader  //负责加载应用类(程序员自定义的类)

    Java提供了显式加载类的API:Class.forName(classname)。

 

posted @ 2020-04-09 21:44  MrHH  阅读(294)  评论(0编辑  收藏  举报