JAVA后端开发面试基础知识(一)——JVM

1. JVM的主要组成部分及其作用

  • Class loader(类装载)

    根据给定的全限定名类名(如: java.lang.Object)来装载class文件到 Runtime data area中的method area。

  • Execution engine(执行引擎)

    执行classes中的指令。

  • Native Interface(本地接口)

    与native libraries交互,是其它编程语言交互的接口。

  • Runtime data area(运行时数据区域)

    这就是我们常说的JVM的内存。

首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader) 再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

2. JVM运行时数据区/内存模型

  • 程序计数器(Program Counter Register)【私有】

    当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;

  • Java 虚拟机栈(Java Virtual Machine Stacks)【私有】

    每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  • 本地方法栈(Native Method Stack)【私有】

    与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;

  • Java 堆(Java Heap)【共享】

    Java 虚拟机中内存大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;

  • 方法区(Methed Area)【共享】

    用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

    • 运行时常量池

      运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池(Constant Poll Table)用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。

      其中字符串常量池属于运行时常量池的一部分,不过在HotSpot虚拟机中,JDK1.7将字符串常量池移到了java堆中。

  • 直接内存

    直接内存不是JVM运行时的数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中引 入了NIO(New Input/Output)类,引入了一种基于通道(Chanel)与缓冲区(Buffer)的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java中的DirectByteBuffer对象作为对这块内存 的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和Native对中来回复制数据。

    • 直接内存(堆外内存)与堆内存比较

      1. 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
      2. 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
    • 使用场景

      • 有很大的数据需要存储,它的生命周期很长
      • 适合频繁的IO操作,例如网络并发场景
    • 为什么要主动调用System.gc

      通过触发一次gc操作来回收堆外内存

      堆外内存的回收其实依赖于我们的gc机制,首先我们要知道在java层面和我们在堆外分配的这块内存关联的只有与之关联的DirectByteBuffer对象了,它记录了这块内存的基地址以及大小,那么既然和gc也有关,那就是gc能通过操作DirectByteBuffer对象来间接操作对应的堆外内存了。DirectByteBuffer对象在创建的时候关联了一个PhantomReference, 说到PhantomReference它其实主要是用来跟踪对象何时被回收的,它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块

  • 版本变化

    • Jdk1.6及之前:有永久代, 常量池在方法区

    • Jdk1.7:有永久代,但已经逐步"去永久代",常量池在堆

    • Jdk1.8及之后:无永久代,常量池在元空间

3. 堆栈的区别

  • 物理地址

    堆的物理地址分配对对象是不连续的。因此性能慢些。

    栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

  • 内存分配

    堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。

    一般堆大小远远大于栈。 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

  • 存放的内容

    堆存放的是对象的实例和数组。因此该区更关注的是数据的存储

    栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

    静态变量放在方法区,静态的对象还是放在堆

  • 程序的可见度

    堆对于整个应用程序都是共享、可见的。

    栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

  • 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;

    堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

4. 对象创建的五种方法

  • new关键字
  • Class的newInstance方法
  • Constructor类的newInstance方法
  • clone方法
  • 反序列化

5. new的过程

  • 虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。

  • 类加载通过后,接下来分配内存。

    • 若Java堆中内存是绝对规整的,使用"指针碰撞"方式分配内存;

      即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。

    • 如果不是规整的,就从空闲列表中分配,叫做"空闲列表"方式。

    • 划分内存时还需要考虑一个问题-并发,也有两种方式:

      • CAS同步处理,
      • 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。
  • 然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码...),后执行方法。

  • 对象的访问定位

    • 句柄

      优势 :引用中存储的是稳定 的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄 中的实例数据指针 ,而引用本身不需要修改。

    • 直接指针

      优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。

6. Java 中都有哪些引用类型

  1. 强引用(StrongReference)

    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的 对象来解决内存不足的问题。

  2. 软引用(SoftReference)

    如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

  3. 弱引用(WeakReference)

    弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收, Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  4. 虚引用(PhantomReference)

    虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。

    虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用 。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

7. 如何判断对象是否可以被回收

  • 引用计数器法

    为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1, 当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用 的问题;

  • 可达性分析算法

    从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。 当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

8. 垃圾回收算法

  1. 标记-清除算法:标记无用对象,然后进行清除回收。

    • 优点:实现简单,不需要对象进行移动。

    • 缺点:效率不高,无法清除垃圾碎片。

  2. 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

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

    • 缺点:内存使用率不高,只有原来的一半。

  3. 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

    • 优点:解决了标记-清理算法存在的内存碎片问题。
    • 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
  4. 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

  • 并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。

9. 垃圾回收器

  • Serial收集器(复制算法)

    新生代单线程收集器,标记和清理都是单线程

    • 优点:简单高效
  • ParNew收集器 (复制算法)

    新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU 环境下有着比Serial更好的表现;

  • Parallel Scavenge收集器 (复制算法)

    • 新生代并行收集器,追求高吞吐量,高效利用 CPU。

    吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

  • Serial Old收集器 (标记-整理算法)

    老年代单线程收集器,Serial收集器的老年代版本;

  • Parallel Old收集器 (标记-整理算法)

    老年代并行收集器,吞吐量优先

    Parallel Scavenge收集器的老年代版本

  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法)

    • 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

    • CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合

    • 在启动 JVM 的参数加上"-XX:+UseConcMarkSweepGC"来指定使用 CMS 垃圾回收器。

    • CMS 使用的是标记-清除的算法实现的, 所以在gc的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。

    • 整个过程分为6个步骤,其中初始标记、并发标记这两个步骤仍然需要"Stop The World"

      1. 初始标记(CMS-initial-mark)

        为了收集应用程序的对象引用需要暂停应用程序线程,该阶段完成后,应用程序线程再次启动。

      2. 并发标记(CMS-concurrent-mark)

        从第一阶段收集到的对象引用开始,遍历所有其他的对象引用,此阶段会打印2条日志:CMS-concurrent-mark-start,CMS-concurrent-mark。

      3. 并发预清理(CMS-concurrent-preclean)

        改变当运行第二阶段时,由应用程序线程产生的对象引用,以更新第二阶段的结果。 此阶段会打印2条日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean。

      4. CMS-concurrent-abortable-preclean

        加入此阶段的目的是使cms gc更加可控一些,作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间,通过两个参数来来控制是否进行下一阶段:

        1. -XX:CMSScheduleRemarkEdenSizeThreshold(默认2M):即当eden使用小于此值时;
        2. -XX:CMSScheduleRemarkEdenPenetratio(默认50%):在concurrent preclean阶段之后,如果Eden占用率高于CMSScheduleRemarkEdenSizeThreshold,开启'concurrent abortable preclean',并且持续的precleanig直到Eden占比超过CMSScheduleRemarkEdenPenetratio,之后,开启remark阶段
      5. 重标记CMS-concurrent-remark

        由于上面三阶段是并发的,对象引用可能会发生进一步改变。因此,应用程序线程会再一次被暂停以更新这些变化,并且在进行实际的清理之前确保一个正确的对象引用视图。这一阶段十分重要,因为必须避免收集到仍被引用的对象。

      6. 并发清理(CMS-concurrent-sweep)

        所有不再被应用的对象将从堆里清除掉。

      7. 并发重置(CMS-concurrent-reset)

        收集器做一些收尾的工作,以便下一次GC周期能有一个干净的状态。

    • 参数控制

      • -XX:+UseConcMarkSweepGC:

        使用CMS收集器,-XX:UseParNewGC会自动开启。因此,如果年轻代的并行GC不想开启,可以通过设置-XX:-UseParNewGC来关掉

      • -XX:+CMSClassUnloadingEnabled

        相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。

      • -XX:+CMSConcurrentMTEnabled

        当该标志被启用时,并发的CMS阶段将以多线程执行(因此,多个GC线程会与所有的应用程序线程并行工作)。该标志已经默认开启,如果顺序执行更好,这取决于所使用的硬件,多线程执行可以通过-XX:-CMSConcurremntMTEnabled禁用(注意是-号)。

      • -XX:+UseCMSCompactAtFullCollection

        Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长

      • -XX:+CMSFullGCsBeforeCompaction

        设置进行几次Full GC后,进行一次碎片整理

      • -XX:ParallelCMSThreads

        设定CMS的线程数量(一般情况约等于可用CPU数量)

      • -XX:CMSMaxAbortablePrecleanTime

        当abortable-preclean阶段执行达到这个时间时才会结束

      • -XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly来决定什么时间开始垃圾收集;如果设置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有当old代占用确实达到了-XX:CMSInitiatingOccupancyFraction参数所设定的比例时才会触发cms gc;如果没有设置,系统会根据统计数据自行决定什么时候触发cms gc;因此有时会遇到设置了80%比例才cms gc,但是50%时就已经触发了,就是因为这个参数没有设置的原因。

  • G1(Garbage First)收集器 (标记-整理算法)

    Java堆并行收集器,G1收集器是 JDK1.7提供的一个新收集器,G1收集器基于"标记-整理"算法实现,也就是说不会产生内存碎片。

    • G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

    • G1在压缩空间方面有优势

      1. G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
      2. Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
      3. G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
      4. G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
      5. G1会在Young GC中使用、而CMS只能在O区使用
    • 以下场景下G1更适合:

      1. Full GC 次数太频繁或者消耗时间太长
      2. 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
      3. 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象
      4. 对象分配的频率或代数提升(promotion)显著变化
      5. 太长的垃圾回收或内存整理时间(超过0.5~1秒)
    • 参数

      • -XX:+UseG1GC

        使用G1 GC

      • -XX:MaxGCPauseMillis=n

        设置一个暂停时间期望目标,这是一个软目标,JVM会近可能的保证这个目标

      • -XX:InitiatingHeapOccupancyPercent=n

        内存占用达到整个堆百分之多少的时候开启一个GC周期,G1 GC会根据整个栈的占用,而不是某个代的占用情况去触发一个并发GC周期,0表示一直在 GC,默认值是45

      • -XX:NewRatio=n

        年轻代和老年代大小的比例,默认是2

      • -XX:SurvivorRatio=n

        eden和survivor区域空间大小的比例,默认是8

      • -XX:MaxTenuringThreshold=n

        晋升的阈值,默认是15(译者注:一个存活对象经历多少次GC周期之后晋升到老年代)

      • -XX:ParallelGCThreads=n

        GC在并行处理阶段试验多少个线程,默认值和平台有关。

      • -XX:ConcGCThreads=n

        并发收集的时候使用多少个线程,默认值和平台有关。

      • -XX:G1ReservePercent=n

        预留多少内存,防止晋升失败的情况,默认值是10

      • -XX:G1HeapRegionSize=n

        G1 GC的堆内存会分割成均匀大小的区域,这个值设置每个划区域的大小,这个值的默认值是根据堆的大小决定的。最小值是1Mb,最大值是32Mb

10. 分代垃圾回收器是怎么工作的

  • 分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。

  • 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

    1. 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
    2. 清空 Eden 和 From Survivor 分区;
    3. From Survivor 和 To Survivor 分区交换;
    • 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
  • 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

11. 内存分配策略

  • 对象优先在 Eden 区分配

  • 大对象直接进入老年代

    虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。

  • 长期存活对象将进入老年代

  • 动态对象年龄判定

    为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代.

    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

  • 空间分配担保

    1. 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。

    2. 如果不成立,则虚拟机会查看 HandlePromotionFailure设置值是否允许担保失败。

    3. 如果允许,那么会继续检查老年代最大可用的连续 空间是否大于历次晋升到老年代对象的平均大小

      • 如果大于,将尝试着进行一次Minor GC,尽管这次 Minor GC是有风险的

      • 如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

12. 类加载机制

  • 隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用 类装载器加载对应的类到jvm中,
  • 显式装载, 通过class.forname() 等方法,显式加载需要的类

(1)类加载器分类

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。 主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):

    用来加载java核心类库,无法被 java程序直接引用。

  2. 扩展类加载器(extensions class loader)

    它用来加载 Java 的扩展库。 Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

  3. 系统类加载器(system class loader)

    它根据 Java 应用的类路径 (CLASSPATH) 来加载 Java类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。

  4. 用户自定义类加载器

    通过继承 java.lang.ClassLoader类的方式实现,然后覆盖findClass()方法。

    ClassLoader超类的loadClass方法用于将类的加载操作委托给父类加载器去进行,只有该类尚未加载并且父类加载器也无法加载该类时,才调用findClass()方法。 如果要实现该方法,必须做到以下几点:

    1. 为来自本地文件系统或者其他来源的类加载其字节码
    2. 调用ClassLoader超类的defineClass()方法,向虚拟机提供字节码

(2)类装载过程

  1. 加载

    根据查找路径找到相应的 class 文件然后导入;

    • 通过一个类的全限定名来获取其定义的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
  2. 验证

    检查加载的 class 文件的正确性;

  3. 准备

    给类中的静态变量分配内存空间,并将其初始化为默认值

  4. 解析

    虚拟机将常量池中的符号引用替换成直接引用的过程。

    符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;

  5. 初始化

    对静态变量和静态代码块执行初始化工作。

    为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化,包括给声明类变量指定初始值,和为类static变量指定静态代码块地址。

  • OSGi

    OSGi 是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。

13. JVM调优

(1) 工具

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;
  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
  • 命令行:
    • top查看CPU使用情况,或通过CPU使用率收集,找到CPU占用率高Java进程,假设其进程编号为pid;
    • 使用top -Hp pid(或ps -Lfp pid或者ps -mp pid -o THREAD,tid,time)查看pid进程中占用CPU较高的线程,假设其编号为tid;
    • 使用Linux命令,将tid转换为16进制数,printf '%0x\n' tid,假设得到值txid;
    • 使用jstack pid | grep txid查看线程CPU占用代码,然后根据得到的对象信息,去追踪代码,定位问题。

(2) 参数

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • -XX:NewSize:设置新生代最小空间大小。
  • -XX:MaxNewSize:设置新生代最大空间大小。
  • -XX:PermSize:设置永久代最小空间大小。
  • -XX:MaxPermSize:设置永久代最大空间大小。
  • -Xss:设置每个线程的堆栈大小。
  • --XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。

(3)调优思路

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

    • Minor GC执行时间不到50ms;约10秒一次;
    • Full GC执行时间不到1s;不低于10分钟1次;
  • 线程池(java.util.concurrent.ThreadPoolExecutor)

    解决用户响应时间长的问题

    • corePoolSize:核心线程数(最新线程数)

    • maximumPoolSize:最大线程数

      超过这个数量的任务会被拒绝,用户可以通过 RejectedExecutionHandler接口自定义处理方式

    • keepAliveTime:线程保持活动的时间

    • workQueue:工作队列,存放执行的任务

      Java线程池需要传入一个Queue参数(workQueue)用来存放执行的任务,而对Queue的不同选择,线程池有完全不同的行为:

      • SynchronousQueue:一个无容量的等待队列,一个线程的insert操作必须等待另一线程的remove操作,采用这个Queue线程池将会为每个任务分配一个新线程
      • LinkedBlockingQueue:无界队列,采用该Queue,线程池将忽略maximumPoolSize参数,仅用corePoolSize的线程处理所有的任务,未处理的任务便在 LinkedBlockingQueue中排队
      • ArrayBlockingQueue:有界队列,在有界队列和maximumPoolSize的作用下,程序将很难被调优:更大的Queue和小的maximumPoolSize将导致CPU的低负载;小的Queue和大的池,Queue就没起动应有的作用。

      封装方式:

      • 以SynchronousQueue作为参数,使maximumPoolSize发挥作用,以防止线程被无限制的分配, 同时可以通过提高maximumPoolSize来提高系统吞吐量
      • 自定义一个RejectedExecutionHandler,当线程数超过maximumPoolSize时进行处理,处理方式为隔一段时间检查线程池是否可以执行新Task,如果可以把拒绝的Task重新放入到线程池,检查的时间依赖keepAliveTime的大小。
  • 连接池(org.apache.commons.dbcp.BasicDataSource)

    在使用org.apache.commons.dbcp.BasicDataSource的时候,因为之前采用了默认配置,所以当访问量大时,通过JMX观察到很多Tomcat线程都阻塞在BasicDataSource使用的Apache ObjectPool的锁上,直接原因当时是因为BasicDataSource连接池的最大连接数设置的太小,默认的BasicDataSource 配置,仅使用8个最大连接。

    当较长的时间不访问系统,比如2天,DB上的Mysql会断掉所以的连接,导致连接池中缓存的连接不能用。

    Mysql默认支持100个链接,所以每个连接池的配置要根据集群中的机器数进行,如有2台服务器, 可每个设置为60

    • initialSize:一直打开的连接数

    • minEvictableIdleTimeMillis:该参数设置每个连接的空闲时间,超过这个时间连接将被关闭

    • timeBetweenEvictionRunsMillis:后台线程的运行周期,用来检测过期连接

    • maxActive:最大能分配的连接数

    • maxIdle:最大空闲数,当连接使用完毕后发现连接数大于maxIdle,连接将被直接关闭。只有 initialSize < x < maxIdle的连接将被定期检测是否超期。这个参数主要用来在峰值访问时提高吞吐量。

    • 如何保存连接

      BasicDataSource会关闭所有超期的连接,然后再打开initialSize数量的连接,这个特性与minEvictableIdleTimeMillis、 timeBetweenEvictionRunsMillis一起保证了所有超期的initialSize连接都会被重新连接,从而避免了Mysql长时间无动作会断掉连接的问题。

  • JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量

    • 目标:

      • GC的时间足够的小
      • GC的次数足够的少
      • 发生Full GC的周期足够的长

      前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。

    1. 针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值

    2. 年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率 NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize - XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize - XX:MaxNewSize设置为同样大小

      如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根据以下两点

      本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理

    3. 在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法**: **- XX:+UseParallelOldGC,默认为Serial收集

    4. 线程堆栈的设置

      每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太大了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

    5. 可以通过下面的参数打Heap Dump信息

      • -XX:HeapDumpPath
      • -XX:+PrintGCDetails
      • -XX:+PrintGCTimeStamps
      • -Xloggc:/usr/aaa/dump/heap_trace.txt
      • XX:+HeapDumpOnOutOfMemoryError
  • 程序算法:改进程序逻辑算法提高性能

(4)原则

  1. 多数的Java应用不需要在服务器上进行GC优化;
  2. 多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
  3. 在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);
  4. 减少创建对象的数量;
  5. 减少使用全局变量和大对象;
  6. GC优化是到最后不得已才采用的手段;
  7. 在实际使用中,分析GC情况优化代码比优化GC参数要多得多;

(5)层级 409

  • 架构调优

  • 代码调优

    算法、数据结构

  • JVM调优

    垃圾回收器、内存分配

  • 数据库调优

    数据表分配、sql优化

  • 操作系统调优

(6) 性能定义

  • 吞吐量

    重要指标之一,是指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。

  • 延迟

    其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。

  • 内存占用

    垃圾收集器流畅运行所需要的内存数量。

这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。

14. Minor GC、Major GC、Full GC

  • Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;

  • Major GC是指发生在老年代的 GC,OldGen区内存不足,触发Major GC,出现了 Major GC 通常会伴随至少一次 Minor GC。 Major GC 的速度通常会比 Minor GC 慢 10 倍以上。

  • Full GC

    • 清理整个堆空间---包括年轻代和永久代

    • 触发的场景

      1. System.gc

      2. promotion failed (年代晋升失败,比如eden区的存活对象晋升到S区放不下,又尝试直接晋升到Old 区又放不下,那么Promotion Failed,会触发FullGC)

      3. CMS的Concurrent-Mode-Failure

        CMS回收过程中主要分为四步:

        1. CMS initial mark
        2. CMS Concurrent mark
        3. CMS remark
        4. CMS Concurrent sweep。

        在2中gc线程与用户线程同时执行,那么用户线程依旧可能同时产生垃圾, 如果这个垃圾较多无法放入预留的空间就会产生CMS-Mode-Failure, 切换为SerialOld单线程做mark-sweep-compact。

      4. 新生代晋升的平均大小大于老年代的剩余空间 (为了避免新生代晋升到老年代失败)

        • 当使用 G1、CMS 时,FullGC发生的时候是Serial+SerialOld。
        • 当使用ParalOld时,FullGC发生的时候是 ParallNew+ParallOld.

15. 类文件结构

16. User user = new User() 做了什么操作,申请了哪些内存?

  1. new User(); 创建一个User对象,内存分配在堆上
  2. User user; 创建一个引用,内存分配在栈上
  3. = 将User对象地址赋值给引用

17. 内存泄漏及解决方法

  • 系统崩溃前的一些现象:

    • 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延 长到4、5s
    • FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
    • 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放
    • 之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。
  • 解决方法

    1. 生成堆的dump文件

    2. 分析dump文件

      Eclipse专门的静态内存分析工具:Mat。

    3. 分析内存泄漏

      通过Mat我们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。

    4. 回归问题

      • 为什么崩溃前垃圾回收的时间越来越长?

        根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据

      • 为什么Full GC的次数越来越多?

        因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收

      • 为什么年老代占用的内存越来越大?

        因为年轻代的内存无法被回收,越来越多地被Copy到年老代

  • 怎样阻止内存泄露

    1. 使用List、Map等集合时,在使用完成后赋值为null
    2. 使用大对象时,在用完后赋值为null
    3. 目前已知的jdk1.6的substring()方法会导致内存泄露
    4. 避免一些死循环等重复创建或对集合添加元素,撑爆内存
    5. 简洁数据结构、少用静态集合等
    6. 及时的关闭打开的文件,socket句柄等
    7. 多关注事件监听(listeners)和回调(callbacks),比如注册了一个listener,当它不再被使用的时候,忘了注销该listener,可能就会产生内存泄露

18. HotSpot逃逸分析

19. Java中的常量池

  • 静态常量池

    即.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量, 还包含类、方法的信息,占用class文件绝大部分空间。

  • 运行时常量池

    则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

20. -XX:+UseCompressedOops 的作用

​ 当你将你的应用从 32 位的 JVM 迁移到 64 位的 JVM 时,由于对象的指针从 32 位增加到了 64 位,因此堆内存会突然增加,差不多要翻倍。这也会对 CPU 缓存(容量比内存小很多)的数据产生不利的影响。 因为,迁移到 64 位的 JVM 主要动机在于可以指定最大堆大小,通过压缩 OOP 可以节省一定的内存。 通过 -XX:+UseCompressedOops 选项,JVM 会使用 32 位的 OOP,而不是 64 位的 OOP。

21. 内存溢出和内存泄漏的区别

  • 内存溢出 out of memory

    是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory

  • 内存泄露 memory leak

    是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

  • 实现思路

    • 栈内存溢出(StackOverflowError)

      • 程序所要求的栈深度过大导致,可以写一个死递归程序触发。
    • 堆内存溢出(OutOfMemoryError:java heap space)

      • 泄露则看对象如何被 GC Root 引用。

      • 溢出则通过 调大 -Xms,-Xmx参数。

    • 持久带内存溢出(OutOfMemoryError: PermGen space)

      • 用String.intern()触发常量池溢出
      • Class对象未被释放,Class对象占用信息过多,有过多的Class对象。可以导致持久带内存溢出

22. threadlocal

23. AOP面向切面编程

  • AOP 专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题,在 Java EE 应用中,常常通过AOP来处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等,在不改变已有代码的情况下,静态/动态的插入代码。
  • 将AOP放到这里的主要原因是因为AOP改变的class文件,达到嵌入方法的目的, 使用 AspectJ进行由.java到.class文件编译。而使用CGLIB载入使用javac编译的.class文件后,使用动态代理的方式,将要执行的方法嵌入到原有class方法中,完成在内存中对class对象的 式模态动式模态静动态方式在.class载入时需要做额外的处理,导致性能受到一定影响,但其优势是无须使用额外的构造,这也就是所谓 的内在原理。同时静态方式在载入前已经修好完.class文件,而术技理代态动 编译器。总体的技术的切入点在于在修改机器执行码,达到增加执行方法的目的。

24. StringTable

相关推荐
不会编程的懒洋洋2 分钟前
Spring Cloud Eureka 服务注册与发现
java·笔记·后端·学习·spring·spring cloud·eureka
赖龙7 分钟前
java程序打包及执行 jar命令及运行jar文件
java·pycharm·jar
U12Euphoria9 分钟前
java的runnable jar采用exe和.bat两种方式解决jre环境的问题
java·pycharm·jar
java小吕布28 分钟前
Java Lambda表达式详解:函数式编程的简洁之道
java·开发语言
程序员劝退师_35 分钟前
优惠券秒杀的背后原理
java·数据库
java小吕布1 小时前
Java集合框架之Collection集合遍历
java
一二小选手1 小时前
【Java Web】分页查询
java·开发语言
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ1 小时前
idea 弹窗 delete remote branch origin/develop-deploy
java·elasticsearch·intellij-idea
Code成立1 小时前
《Java核心技术 卷I》用户图形界面鼠标事件
java·开发语言·计算机外设
Xiao Fei Xiangζั͡ޓއއ1 小时前
一觉睡醒,全世界计算机水平下降100倍,而我却精通C语言——scanf函数
c语言·开发语言·笔记·程序人生·面试·蓝桥杯·学习方法