Java面试之JVM总结

内存泄漏

  • 定义

    • 内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。
  • 原因

    • 长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被GC回收。
  • 分类

    • 静态集合类:引起内存泄漏
    • 各种连接:比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close() 方法将其连接关闭,否则是不会自动被GC 回收的。
    • 单例模式:单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被 JVM 正常回收,导致内存泄漏,考虑下面的例子
    • 监听器:通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener() 等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
  • 解决方法

    • 在确认一个对象无用后,将其所有引用显式的置为null
    • Optimizeit Profiler、JProbe Profiler:在开发中不能完全避免内存泄漏,关键要在发现有内存泄漏的时候能用好的测试工具迅速定位问题的所在
    • 注意像 HashMap 、ArrayList 的集合对象的使用

类加载

  • 双亲委派机制

    • 定义:当一个class文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException

      • 向上委派:查找缓存,是否加载了该类,有则返回,没有继续向上
      • 向下查找:查找类加载路径,有则加载返回,没有则继续向下查找
    • 优点:

      • 避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
      • 确保安全,java核心api中定义类型不会被随意替换,防止了危险代码植入
  • 类加载机制

    • 加载:ClassLoader通过一个类的完全限定名查找此类字节码文件,将其加载到内存。将静态数据结构转化成方法区中运行时的数据结构;在堆中生成一个代表这个类的 java.lang.Class对象作为数据访问的入口

    • 验证:确保class文件的字节流中包含信息符合虚拟机规范,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证

    • 准备:为类变量(static修饰的字段变量)在方法区中分配内存,并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。

    • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)

    • 初始化:类加载最后阶段,执行类构造器方法的()的过程,而且要保证执行前父类的()方法执行完毕。这个方法由编译器收集,顺序执行所有类变量(static修饰的成员变量)显式初始化和静态代码块中语句。此时准备阶段时的那个 static int i 由默认初始化的0变成了显式初始化的5. 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。

  • 类加载器

    • 引导类加载器(BootstrapClassLoader):负责加载 J A V A H O M E JAVA_HOME JAVAHOME/jre/lib下面的核心类库
    • 扩展类加载器(ExtClassLoader):负责加载 J A V A H O M E JAVA_HOME JAVAHOME/jre/lib/ext下面的扩展类库
    • 系统类加载器(AppClassLoader):自定义类加载器的父类,负责加载应用程序classpath目录下的所有jar和class等

运行时数据区

    • 会出现OutOfMemoryError,不会出现StackOverflowError

    • 线程共享

    • 内存划分

      • 新生代

        • 伊甸园区(Eden)
        • From Survivor区
        • To Survivor区
      • 老年代

      • 元空间

  • 程序计数器

    • 记录当前线程正在执行的字节码的行号
    • 不会出现OutOfMemoryError和StackOverFlowError
    • 线程私有
  • 方法区

    • 别名:Non-Heap(非堆)

    • 会出现OutOfMemoryError,不会出现StackOverflowError

    • 存储内容

      • 类型信息

        • 完整有效名称(全名=包名.类名)
        • 直接父类的完整有效名称
        • 类型的修饰符(public,abstract ,final 的某个子集)
        • 类型直接接口的一个有序列表
      • 域(Field)信息

        • 域的相关信息包括:域名称、域类型、域修饰符
        • 域的声明顺序
      • 方法信息

        • 方法名称
        • 方法的返回类型
        • 方法参数的数量和类型(按顺序)
        • 方法的修饰符
      • 类(静态)变量

      • 运行时常量池

      • JIT代码缓存

    • 线程共享

  • 虚拟机栈

    • 存储内容(栈帧)

      • 局部变量表

        • 变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
      • 操作数栈

      • 动态链接

      • 方法返回地址

    • 会出现OutOfMemoryError和StackOverflowError

    • 线程私有

  • 本地方法栈

    • 管理本地(native)方法的调用
    • 会出现OutOfMemoryError和StackOverflowError
    • 线程私有

GC

  • 如何判断对象是否是垃圾?

    • 引用计数法

      • 方法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的
      • 缺点:无法避免循环引用问题,即两个对象之间循环引用的时候,各自的计数器始终不会变成 0
    • 根搜索算法(可达性分析算法)

      • 从某一些指定的根对象(GC Roots)出发,一步步遍历找到和这个根对象具有引用关系的对象,然后再从这些对象开始继续寻找,从而形成一个个的引用链,不在这些引用链上面的对象便被标识为引用不可达对象

      • GC Roots 对象

        • 虚拟机栈的栈帧中的局部变量表引用的对象
        • 方法区中的静态变量和常量引用的对象
        • 本地方法栈中JNI(Native方法)引用的对象。
  • 垃圾回收算法

    • 标记 - 清除算法(Mark-Sweep)

      • 方法

        • 方式:根搜索算法标记不可达对象,当所有的待回收的“垃圾对象”标记完成之后,便进行统一清除。
      • 优点

        • 当存活对象比较多的时候,性能比较高,因为该算法只需要处理待回收的对象,而不需要处理存活的对象。
      • 缺点

        • 产生内存碎片
    • 标记 - 压缩算法(Mark-Compact)

      • 方法

        • 使用根搜索算法在标记完成之后,该算法并不会直接清除掉可回收对象 ,而是让所有的对象都向一端移动,然后将端边界以外的内存全部清理掉。
      • 优点

        • 使得内存上面不会再有碎片问题,并且新对象的分配只需要通过简单的指针碰撞便可完成。
      • 缺点

        • 无论是标记-清除算法还是垃圾-整理算法,都会涉及句柄的开销或是面对碎片化的内存回收。
    • 复制算法(Copying)

      • 方法

        • 复制算法将内存区域均分为了两块(记为S0和S1),而每次在创建对象的时候,只使用其中的一块区域(例如S0),当S0使用完之后,便将S0上面存活的对象全部复制到S1上面去,然后将S0全部清理掉。
      • 优点

        • ① 不会产生内存碎片;② 标记和复制可以同时进行;③ 复制时也只需要移动栈顶指针即可,按顺序分配内存,简单高效;④ 每次只需要回收一块内存区域即可,而不用回收整块内存区域,所以性能会相对高效一点。
      • 缺点

        • 可用的内存减小了一半,存在内存浪费的情况。
    • 分代收集算法(Generational Collectjion)

      • 方法

        • 根据堆内存具体的使用情况而自动选用更适合当前情况的回收算法。
      • 新生代为什么使用复制算法?老年代为什么使用标记-压缩算法?

        • 复制算法是把标记存活的对象复制到另一块内存区域中,标记-压缩算法是将存活对象都向一端移动,然后直接清理掉端边界以外的内存。
        • 针对于新生代需要清理的对象数量十分巨大,所以标记-压缩算法在将存活的对象插入到待清理对象之前,需要大量移动操作,时间复杂度很高;反观复制算法,不需要移动待回收对象的操作,直接将存活对象复制到另一块空闲内存区域中,大大减小了时间复杂度。
        • 老年代对象存活率高,复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低。
  • GC过程

    • 对象首次创建进行内存分配的时候,首先会放置在 Eden 区,当 Eden 区放满了或者当该对象太大无法放进 Eden 区的时候,此时会对年轻代(Eden区 和 S0)进行一次 GC,将幸存下来的对象放置在 S1,然后清空掉 Eden区和 S0 区;(此时年轻代采用的是复制算法)
    • 在上面第一步中对年轻代进行垃圾回收的时候,同时会对幸存的对象进行标记,统计每个幸存对象经历的 GC 次数;
    • 当 S1 区满了之后,或者年轻代的对象经历过指定次数的 GC 之后,这部分对象会被放置到老年代之中;
    • 当老年代也满了之后,便会对老年代进行一次 GC;(老年代采用的是 标记-压缩算法)
  • 垃圾回收器

    • 新生代垃圾回收器

      • Serial 垃圾回收器

        • Serial收集器是最基本的、发展历史最悠久的收集器。俗称为:串行回收器,采用复制算法进行垃圾回收
        • 使用-XX:+UseSerialGC参数可以设置新生代使用这个串行回收器
      • ParNew 垃圾回收器

        • ParNew其实就是Serial的多线程版本,除了使用多线程之外,其余参数和Serial一模一样。俗称:并行垃圾回收器,采用复制算法进行垃圾回收
        • 使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器
      • ParallelGC 回收器

        • ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和系统吞吐量相关的参数使得其使用起来更加的灵活和高效。
        • 使用-XX:+UseParallelGC参数可以设置新生代使用这个并行回收器
    • 老年代垃圾回收器

      • SerialOld 垃圾回收器

        • SerialOld是Serial回收器的老年代回收器版本,它同样是一个单线程回收器。使用的垃圾回收算法是标记 - 压缩算法
      • ParallelOldGC 回收器

        • 老年代ParallelOldGC回收器也是一种多线程的回收器,和新生代的ParallelGC回收器一样,也是一种关注吞吐量的回收器,他使用了标记压缩算法进行实现
      • CMS 回收器

        • CMS全称为:Concurrent Mark Sweep意为并发标记清除,他使用的是标记清除法。主要关注系统停顿时间

        • 初始标记和重新标记任然需要“stop the world”,但是在整个过程中最耗时的并发标记和并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的

        • 使用-XX:+UseConcMarkSweepGC进行设置老年代使用该回收器。

        • 过程

          • 初始标记(CMS initial mark):独占CPU,stop-the-world, 仅标记GCroots能直接关联的对象,速度比较快
          • 并发标记(CMS concurrent mark):可以和用户线程并发执行,通过GCRoots Tracing 标记所有可达对象
          • 重新标记(CMS remark):独占CPU,stop-the-world, 对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新逃逸对象
          • 并发清理(CMS concurrent sweep):可以和用户线程并发执行,清理在重复标记中被标记为可回收的对象。
    • G1 垃圾回收器

      • 同时回收年轻代和老年代的,他最大的特点就是把jvm堆内存拆分为了多个大小相等的Region
      • G1充分发挥多核性能,使用多CPU来缩短Stop-The-world的时间
      • G1从整体上来看是基于‘标记-整理’算法实现,从局部(相关的两块Region)上来看是基于‘复制’算法实现,这两种算法都不会产生内存空间碎片。

JVM调优

  • 正常运行系统

    • jmap:查看JVM各个区域的使用情况
    • jstack:查看线程的运行情况,比如哪些线程阻塞,是否出现了死锁
    • jstat:查看垃圾回收情况,特别是Full GC,如果Full GC比较频繁,需要进行调优
  • 已经OOM的系统

    • 利用jvisualvm分析dump文件,找到异常实例对象和异常线程,定位到具体代码,然后在进行详细的分析和调试。

JVM常用参数

  • -Xms512m :设置JVM初始内存为512m
  • -Xmx512m :设置JVM最大可用内存为512M
  • -XX:PermSize=10M :JVM初始分配的永久代的容量,必须以M为单位
  • -XX:MaxPermSize=10M :JVM允许分配的永久代的最大容量,必须以M为单位,大部分情况下这个参数默认为64M
  • -Xmn200M :设置年轻代大小为200M
  • -Xss128k :设置虚拟机栈的大小为128k
  • -Xoss128k :设置本地方法栈的大小为128k
  • -XX:NewRatio=4 :设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代).设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
  • -XX:SurvivorRatio=4 :设置年轻代中Eden区与Survivor区的大小比值.设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
  • -XX:-PrintGC Details 每次GC时打印详细信息
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赴前尘

喜欢我的文章?请我喝杯咖啡吧!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值