常见面试题-JVM(一)

  1. 什么时候会有内存泄漏,怎么排查?

答:

首先内存泄漏是堆中的一些对象不会再被使用了,但是无法被垃圾收集器回收,如果不进行处理,最终会导致抛出 java.lang.OutOfMemoryError 异常。

内存泄露:

  • 不需要使用的对象被其他对象不正确的引用,导致无法回收。
  • 对象生命周期过长

内存泄漏的8中情况:

  1. 大量使用静态集合类(HashMap、LinkedList等),静态变量的生命周期和JVM程序一致,在程序结束之前,静态变量不会被释放,导致内存泄漏。(属于生命周期过长)

  2. 单例模式的静态特性,也会导致生命周期过长,如果单例对象持有外部对象的引用,会导致外部对象不会被回收。

  3. 内部类持有外部类:每个非静态内部类都会持有外部类的隐式引用,假如a为非静态内部类,ba的外部类,如果b包含了大量对象的引用,非常占用内存空间,那么如果我们创建了非静态内部类a,此时即使b对象不再被使用了,也无法回收,占用内存空间,导致内存泄漏。

    解决办法:如果内部类不需要访问外部类的成员信息,可以考虑转换为静态内部类。

  4. 各种连接(数据库连接、网络连接和IO连接)未及时关闭,导致大量对象无法回收,造成内存泄漏。

  5. 变量不合理的作用域:一个变量的定义的作用范围大于其适用范围,很有可能造成内存泄露。

  6. 改变哈希值:

    当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。

    否则,对象修改后的哈希值与最初存入HashSet集合时的哈希值就不同了,这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致HashSet集合中无法单独删除当前对象,造成内存泄漏

    为什么改变哈希值之后找不到对象?因为根据存入时的哈希值去寻找放入的位置,而改变哈希值之后,再去查找就按照新的哈希值所对应的位置去查找,肯定找不到。

    这也是 String 为什么被设置成了不可变类型,我们可以放心的把 String 存入 HashSet,或者把String当作 HashMap 的 key 值。

    public class ChangeHashCodeTest {
        public static void main(String[] args) {
            HashSet<Point> set = new HashSet<>();
            Point cc = new Point();
            cc.setX(10); // hashCode = 10
            set.add(cc);
            cc.setX(20); // hashCode = 20
            System.out.println("set remove = " + set.remove(cc));
            set.add(cc);
            System.out.println("set.size = " + set.size());
            /**
             * 输出:
             * set remove = false
             * set.size = 2
             */
        }
    }
    class Point {
        int x;
        public int getX() {
            return x;
        }
        public void setX(int x) {
            this.x = x;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Point point = (Point) o;
            return x == point.x;
        }
        @Override 
        public int hashCode() {
            return x;
        }
    }
    
  7. 缓存泄露:

    一旦把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢,就是因为代码会加载一个表的数据到缓存中,测试环境只有几百条数据,而生产环境有几百万的数据。

    对于这个问题,可以使用WeakHashMap代表缓存,此Map的特点是:当除了自身有key的引用外,此key没有其他引用,那么此map会自动丢弃此值。

  8. ThreadLocal

    ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。

    ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。

    如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

    如何解决此问题?

    第一,使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;

    第二,不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。

    第三,最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。

    try {
        threadLocal.set(System.nanoTime());
    } finally {
        threadLocal.remove();
    }
    

排查内存泄漏:

可以查看泄露对象GC Roots的引用链,找到泄露对象在哪里被引用导致无法被回收

  1. JVM常见配置

堆设置

-Xms3550m   初始堆大小 
-Xmx3550m   最大堆大小 
-XX:NewSize=1024     设置年轻代大小 
-XX:NewRatio=4       设置年轻代和年老代的比值.如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 
-XX:SurvivorRatio=8  设置年轻代中Eden区与一个Survivor区的比值,默认为8
-XX:MaxPermSize=256m 设置持久代大小

收集器设置

-XX:+UseSerialGC:设置串行收集器 
-XX:+UseParallelGC:设置并行收集器 
-XX:+UseParalledlOldGC:设置并行年老代收集器 
-XX:+UseConcMarkSweepGC:设置并发收集器

垃圾回收统计信息

-XX:+PrintGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-Xloggc:filename

并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数.并行收集线程数. 
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比.公式为1/(1+n)

并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式.适用于单CPU情况. 
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数.并行收集线程数.
  1. JVM的堆配置过大的副作用有哪些?

答:JVM的堆内存配置过大,可能要面临的问题有:

  • 回收大块堆内存而导致的长时间的时间停顿。
  • 如果因为程序设计失误,将大对象从磁盘读取到内存中,可能会导致大对象在分配时直接进入老年代,没有在 Minor GC 中被清理掉。这样会导致频繁的发生 Full GC,给用户的体验是程序每个几分钟就停顿十几秒,非常卡顿。

扩展:JVM的堆配置过小的副作用有哪些?

  • Minor GC 过于频繁
  1. 如果出现堆内存溢出 java.lang.OutOfMemoryError Java heap space,该如何解决?

答:解决思路如下:

  • 首先需要拿到堆转储快照进行分析,查看导致 OOM 的对象是否有必要存在,并且分析清除是因为哪些对象导致了 OOM
  • 如果是内存泄漏导致 OOM,可以查看泄露对象GC Roots的引用链,找到泄露对象在哪里被引用导致无法被回收
  • 如果不是内存泄漏,那么说明内存中的对象都是存活的,导致 OOM,这时应该检查虚拟机的堆内存设置是否有向上调整的空间。并且检查是否存在对象生命周期过长存储结构不合理的情况,减少程序运行中的内存消耗。

扩展说明:因为存储结构不合理导致堆内存溢出(来自于《深入理解Java虚拟机第3版》)

举例:使用 HashMap 存储大量的数据,会导致浪费大量的空间,因为 HashMap 的空间效率使用太低。

对于一个 HashMap来说,有效数据只有 Key、Value 的两个 long 型数据,占 16字节,long 数据被包装为 java.lang.Long 对象后,就分别具有 8 字节的 Mark Word、8字节的 Klass 指针、8字节的 long 型数值。两个 Long 对象组成 Map.Entry 之后,又多了 16 字节的对象头、8字节的 next 字段、4字节的 int 型的 hash 字段、4字节的空白填充(为了对齐)还有 HashMap 中对这个 Entry 的 8 字节的引用,这样实际占用的内存为:(Long(24byte) * 2) + Entry(32byte) + HashMap Ref(8byte)=88byte,空间效率仅仅为 16byte / 88byte = 18%。

  1. 写代码时候有没有什么方式尽量减少Full GC的概率?

答:

  1. 避免一次性加载大量数据加载到内存,比如excel导入导出,jdbc数据库查询
  2. 避免大对象的代码处理业务链流程过长,比如aop中获取到了对象参数,大对象捕获到了,导致对象生命周期变长了,没及时释放。
  3. 禁止使用system.gc方法
  4. 避免在使用threalocal后,未主动调用remove方法,尽量避免大对象的使用,以及频繁的创建和销毁。更要避免全局锁的竞争等。

你可能感兴趣的:(面试题,jvm)