JVM八股(极简速成、为了秋招没办法版


不是完整版也不是非常深入,属于是极简速成为了秋招没办法的版本,背完了考到就皆大欢喜,考到不会的就拉倒自认倒霉。参考javaguide和牛客,感谢前人的八股救我狗命。

Java内存区域

JVM包含哪几个部分

  • 类加载器
    • 加载.class文件
  • 运行时数据区
    • 存放数据,分为五个区域
      • PC计数器
      • 虚拟机栈
      • 本地方法栈
      • 方法区
  • 执行引擎
    • .class文件被加载后,执行引擎负责把命令解释给操作系统
  • 本地库接口
    • 调用本地接口

Java程序是如何运行的

写好的Java源代码文件编译成.class字节码文件后,通过类加载器加载到内存中,才能被实例化,然后到Java虚拟机中解释执行,最后通过操作系统操作 CPU 执行获取结果。

Java内存区域

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)
PC计数器
  • 指向下一条需要执行的字节码指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 多线程的情况下,程序计数器用于记录当前线程执行的位置保证线程切换后能恢复到正确的执行位置,所以是线程私有的。
Java 虚拟机栈

是线程私有的,它的生命周期和线程相同。除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过栈来实现的

每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。

  • 局部变量表:方法中各种数据类型与对象引用(局部变量)。
  • 操作数栈:存放方法执行过程中产生的中间计算结果与计算过程中产生的临时变量
  • 动态链接 主要服务一个方法需要调用其他方法的场景。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接
  • 方法返回地址

可能会出现两种错误:

  • StackOverFlowError 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
本地方法栈

虚拟机使用 Native 方法

所有线程共享的一块内存区域。在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例垃圾收集器管理的主要区域

方法区

所有线程共享的一块内存区域。

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。

方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

运行时常量池

存放编译期生成的各种字面量(Literal)和 符号引用(Symbolic Reference)的 常量池表(Constant Pool Table)

  • 字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。

  • 常见的符号引用包括符号引用、字段符号引用、方法符号引用、接口方法符号。

字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建

JDK 1.7 将字符串常量池移动到堆中,主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存

Java代码的编译过程

*Java 对象的创建过程 (很重要)

  • Step1:类加载检查

检查是否能在常量池中定位到这个类的符号引用(这个类是否被加载过),并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程

  • Step2:分配内存

    • 对象所需的内存大小在类加载完成后便可确定

    • 内存分配的两种方式: “指针碰撞”“空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

      • 指针碰撞:

        • 适用场合:堆内存规整(即没有内存碎片)的情况下。
        • 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
        • 使用该分配方式的 GC 收集器:Serial, ParNew
      • 空闲列表:

        • 适用场合:堆内存不规整的情况下。
        • 原理:虚拟机会维护一个列表记录哪些内存块可用分配足够大的内存块给对象实例,最后更新列表记录
        • 使用该分配方式的 GC 收集器:CMS
    • 内存分配并发线程安全问题

      • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性

        TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时再采用上述的 CAS 进行内存分配

  • Step3:初始化零值

    • 将分配到的内存空间都初始化为零值
  • Step4:设置对象头

    • 虚拟机要对对象进行必要的设置存放在对象头中,例如这个对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息 。
  • Step5:执行 init 方法

    • 执行 new 指令之后,会接着执行 <init> 方法,把对象按照代码进行初始化。

JVM垃圾回收

建立在两个分代假说之上:

  1. 弱分代假说 (Weak Generational Hypothesis) : 绝大多数对象都是朝生夕灭
  2. 强分代假说 (Strong Generational Hypothesis) : 熬过越多次垃圾收集过程的对象就越难以消

堆空间的基本结构

Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)

堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)
  • Eden 区
  • 两个 Survivor 区 S0 和 S1
  1. 老生代(Old Generation)
  2. 元空间(Metaspace)

内存分配原则

  1. 对象优先在 Eden 区分配

    大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去,

    空间分配担保

    空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间

  2. 大对象(需要大量连续内存空间的对象,如:字符串、数组)直接进入老年代

  3. 长期存活的对象将进入老年代

    虚拟机给每个对象一个对象年龄(Age)计数器。

    对象在 Survivor 中每一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。

内存回收种类

  • 部分收集 (Partial GC):

    • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;

    • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;

    • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

  • 整堆收集 (Full GC):收集整个 Java 堆和方法区。

死亡对象判断方法

引用计数法

给对象中添加一个引用计数器:

  • 每当有一个地方引用它,计数器就加 1
  • 引用失效,计数器就减 1
  • 任何时候计数器为 0 的对象就是不可能再被使用的。

难解决对象之间循环引用的问题。

可达性分析算法

通过 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

哪些对象可以作为 GC Roots 呢?

  • 虚拟机(栈帧中的局部变量表)中引用的对象
  • 本地方法(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
  • JNI(Java Native Interface)引用的对象

垃圾收集算法

标记-清除算法

分为**“标记(Mark)”“清除(Sweep)”**阶段:

  • 标记:标记出所有需要回收的对象
  • 清除:标记完成后统一回收掉所有标记的对象

两个明显的问题:

  1. 效率问题:标记和清除两个过程效率不高
  2. 空间问题:标记清除后会产生大量不连续的内存碎片
复制算法

为了解决标记-清除算法的效率内存碎片问题

它可以将内存分为大小相同的两块每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉

效率:对内存区间的一半进行回收

内存碎片:复制整理。

问题:

  1. 可用内存变小:可用内存缩小为原来的一半。
  2. 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
标记-整理算法

根据老年代的特点提出的一种标记算法。适合老年代这种垃圾回收频率不是很高的场景。

  • 标记:标记出所有需要回收的对象
  • 整理:让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法*

当前虚拟机的垃圾收集都采用分代收集算法。

根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择**”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”**算法进行垃圾收集。

一次完整的GC流程

  1. 在新生代中分配新创建的对象将新生代按照8:1:1分成Eden 区,以及两个 Survivor 区。创建的对象将Eden 区全部挤满,触发Minor GC。

  2. 触发Minor GC 检查机制

    检查新生代中对象比老年代中剩余空间大还是小? 因为Minor GC 之后 Survivor 区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。

    • 老年代剩余空间 > 新生代中的对象大小,直接Minor GC
    • 老年代剩余空间 < 新生代中的对象大小,是否启用了“老年代空间分配担保规则”,即老年代中剩余空间大小是否大于历次 Minor GC 之后剩余对象的大小,那就允许进行 Minor GC。因为从概率上来说,以前的放的下,这次的也应该放的下。
      • 老年代中剩余空间大小 > 历次Minor GC之后剩余对象的大小,进行 Minor GC
      • 老年代中剩余空间大小 < 历次Minor GC之后剩余对象的大小,进行Full GC,把老年代空出来再检查
  3. 进行Minor GC

    • Minor GC之后的对象足够放到 Survivor 区,GC 结束
    • Minor GC之后的对象不够放到 Survivor 区,老年代能放下,GC结束
    • Minor GC之后的对象不够放到 Survivor 区,老年代也放不下,触发 Full GC。
  4. 进行full GC

  • Full GC 之后,老年代任然放不下剩余对象,抛出OOM异常

  • 未开启老年代分配担保机制,且一次 Full GC 后,老年代任然放不下剩余对象,也只能 OOM

  • 开启老年代分配担保机制,但是担保不通过,一次 Full GC 后,老年代任然放不下剩余对象,也是
    能 OOM。

内存溢出 OOM

程序运行过程中申请的内存大于系统能够提供的内存,out of memory。

哪些区域会OOM?除了PC计数器都有可能

原因与解决方案

原因常见的有以下几种

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小。

内存溢出的解决方案:

  1. 修改JVM启动参数,直接增加内存
  2. 检查错误日志,查看“OutofMemory”错误前是否有其它异常或错误
  3. 对代码进行走查和分析,找出可能发生内存溢出的位置。
  4. 使用内存查看工具动态查看内存使用情况。

垃圾收集器

  1. Serial 收集器

    只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

    新生代采用标记-复制算法,老年代采用标记-整理算法。

  2. ParNew 收集器

    使用多线程进行垃圾收集。

    新生代采用标记-复制算法,老年代采用标记-整理算法。

  3. Parallel Scavenge 收集器

    关注点是吞吐量(高效率的利用 CPU)。

    新生代采用标记-复制算法,老年代采用标记-整理算法。

  4. Serial Old 收集器

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

  5. Parallel Old 收集器

    Parallel Scavenge 收集器的老年代版本

  6. CMS 收集器

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。并发收集器让垃圾收集线程与用户线程同时工作

    CMS 收集器是一种 “标记-清除”算法实现的。

    整个过程分为四个步骤:

    • 初始标记: 暂停所有的其他线程,标记一下GC Roots 能直接关联到的对象,速度很快 ;
    • 并发标记:GC Roots的直接关联对象开始遍历所有对象的过程。耗时较长但是不需要停顿用户线程
    • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
    • 并发清除: 开启用户线程,清理删除掉标记阶段判断的已经死亡的对象。

    优点:并发收集、低停顿

    缺点:

    • CPU 资源敏感
    • 无法处理浮动垃圾
    • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
  7. G1 收集器

    G1 (Garbage-First) 是一款面向服务器的垃圾收集器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。使用 Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器高收集效率

    G1 把连续的Java堆划分为多个大小相等的独立区域 (Region),每一个Region都可以根据需要扮演新生代的Eden空间、Survivor空间,或者老年代空间

    维护了一类特殊的Humongous区域,专门用来存储大对象。大小超过了一个 Region容量一半的对象即可判定为大对象。

    筛选回收

    G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的区域(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

Java引用类型

1.强引用(StrongReference)

必不可少垃圾回收器绝不会回收它。当内存空间不足,抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)

可有可无。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存

3.弱引用(WeakReference)

可有可无。具有弱引用的对象拥有更短暂的生命周期。GC线程一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

4.虚引用(PhantomReference)

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

类加载过程详解

类的生命周期
  • 加载(Loading)
  • 连接(Linking)
    • 验证(Verification)
    • 准备(Preparation)
    • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)。

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

加载(Loading)

通过类加载器 完成。

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
验证(Verification)

确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求。

  1. 文件格式验证(Class 文件格式检查)
  2. 元数据验证(字节码语义检查)
  3. 字节码验证(程序语义检查)
  4. 符号引用验证(类的正确性检查)
准备(Preparation)

为类变量分配内存并设置类变量初始值。

  • 类变量( 即静态变量,被 static 关键字修饰的变量),而不包括实例变量。
  • 初始值"通常情况"下是数据类型默认的零值(如 0、null、false 等)
解析(Resolution)

虚拟机将常量池内的符号引用替换为直接引用的过程

初始化(Initialization)

执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)

类卸载

类的 Class 对象被 GC

卸载类需要满足 3 个要求:

  1. 不存在该类的实例对象
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

类加载器详解

类加载器的主要作用就是加载 Java 类的字节码.class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。

类加载器加载规则

不会一次性加载所有的类,而是根据需要去动态加载。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。

三个 ClassLoader
  1. BootstrapClassLoader(启动类加载器)最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jarresources.jarcharsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
  2. ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  3. AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

双亲委派模型

当我们想要加载一个类的时候,具体是哪个类加载器加载?

每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。

只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类) 时,子加载器才会尝试自己去完成加载

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

双亲委派模型的执行流程
  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载,那么它会抛出一个 ClassNotFoundException 异常。
双亲委派模型的好处

避免类的重复加载

保证了 Java 的核心 API 不被篡改。

举例自己写一个object类。比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object

打破双亲委派模型方法

自定义加载器的话,需要继承 ClassLoader

如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。

如果想打破双亲委派模型则需要**重写 loadClass() 方法。**类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。

重写 loadClass()方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。

JVM参数

  1. 显式指定堆内存

    -Xms<heap size>[unit]  //最小
    -Xmx<heap size>[unit]  //最大
    //heap size 表示要初始化内存的具体大小。
    //unit 表示要初始化内存的单位。单位为 “ g” (GB)、“ m”(MB)、“ k”(KB)。
    
  2. 显式新生代内存

    // 通过-XX:NewSize和-XX:MaxNewSize指定
    -XX:NewSize=<young size>[unit]
    -XX:MaxNewSize=<young size>[unit]
    -XX:NewSize=256m  //新生代分配 最小 256m 的内存
    -XX:MaxNewSize=1024m  // 新生代分配 最大 1024m 的内存
    
    // 通过-Xmn<young size>[unit]指定
    -Xmn256m  //新生代分配 256m 的内存
    

​ 将新对象预留在新生代,因为Full GC 的成本远高于 Minor GC。

  1. 显式指定元空间的大小

    没有指定 Metaspace 的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代并不会出现这种情况)。

  2. 垃圾回收

    -XX:+UseSerialGC  // 串行垃圾收集器
    -XX:+UseParallelGC  // 并行垃圾收集器
    -XX:+UseParNewGC  //CMS 垃圾收集器
    -XX:+UseG1GC  // G1 垃圾收集器
    
    
  3. 处理 OOM

-XX:+HeapDumpOnOutOfMemoryError  // JVM 在遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中。
-XX:HeapDumpPath=./java_pid<pid>.hprof  // 写入文件的路径
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"  
// 用于发出紧急命令,以便在内存不足的情况下执行; 应该在 cmd args 空间中使用适当的命令。例如,如果我们想在内存不足时重启服务器,我们可以设置参数: -XX:OnOutOfMemoryError="shutdown -r" 
-XX:+UseGCOverheadLimit  // 限制在抛出 OutOfMemory 错误之前在 GC 中花费的 VM 时间的比例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
《深入理解JVM第四》是一本关于Java虚拟机(JVM)原理和实现的经典著作。它由周志明所著,共计300页。这本书的主要目的是教会读者如何深入理解并掌握JVM的工作原理和内部机制。 本书首先介绍了JVM的基本概念和结构。它详细解释了JVM如何加载、验证、解析和初始化Java类。此外,书中还涉及了运行时数据区域的结构和功能,包括堆、栈、方法区等。 接下来,本书讨论了JVM的垃圾回收机制。它介绍了不同类型的垃圾回收算法和相关的性能调优技术。读者可以通过阅读这一部分,了解如何优化程序的内存使用和垃圾回收效率。 此外,本书还涵盖了JVM的即时编译器和优化技术。它详细介绍了JIT编译器的工作原理,并解释了常用的优化技术,如内联、逃逸分析和锁消除等。这对于那些希望通过编写高效的Java代码来提高程序性能的开发人员来说非常有用。 最后,本书还提供了一些高级主题,如类加载器、字节码增强和调试技术。通过阅读这些章节,读者可以加深对JVM内部机制的理解,并学习如何调优和调试JVM相关的问题。 总体而言,《深入理解JVM第四》是一本全面而深入的JVM学习资料。它适合那些希望更深入了解JVM内部工作原理的Java开发人员。无论是学生、工程师还是研究人员,都可以从这本书中获得宝贵的知识和技巧。读者可以通过仔细阅读和实践书中的示例代码,提升自己的Java编程能力和理解JVM的水平。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值