JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。

前言

  1. 这篇博客主要讲了 class 文件的初始化的流程和两个案例DCL之单例模式、引出的volicate的作用和原理、进而引出指令重排序线程(内存)一致性的概念、解决重排序的代码层面volicate 和JVM层次的规范以及CPU层次的内存屏障的三个层次、缓存行的概念、还有指令重排(乱序)的证明过程。

一、class文件初始化过程

1、概述

在这里插入图片描述
上一篇博文主要讲的类初始化的类加载过程,也就是loading。
这里就说一下其他部分,通过案例进行讲解。

  1. loading ,class文件加载到内存
  2. linking,包括三部分
    • verification
    • preparation
    • resolution
  3. initializing,初始化部分

2、初始化过程-案例1

a、代码T001_ClassLoadingProcedure 类加载过程

package com.mashibing.jvm.c2_classloader;

public class T001_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}

class T {
    public static int count = 2; //0
    public static T t = new T(); // null

    //private int m = 8;

    private T() {
        count ++;
        //System.out.println("--" + count);
    }
}

在这里插入图片描述

b、解析

  1. loading过程:代码执行到 main 方法打印语句时,先加载T.class 类到内存中。
  2. verification: 检测过程
  3. preparation :静态变量 赋默认值过程,此时 count = 0, t = null
  4. resolution :解析过程
  5. initializing:初始化过程,此时 count = 2,t = new T(),执行无参构造函数,然后count自加为3 。所以输出3。

3、初始化过程-案例2

a、代码

还是上面案例1 的代码,改成两个静态变量的顺序,如下:
在这里插入图片描述
在这里插入图片描述
打印的结果为2,这是为啥呢。

b、解析

调换顺序,打印的数据就变成了2,还是初始化的过程,解说如下

  1. loading,类加载过程

  2. verification ,校验过程

  3. preparation ,静态变量赋默认值,此时 t=null,count = 0

  4. resolution ,解析过程

  5. initializing,赋值过程,此时 t=new T() 执行无参构造函数,此时 count 自加后为1,然后执行第二行,count = 2,则覆盖了第一次的count 的自加操作,所以为2

  6. 也说明类初始化过程中的重要性 以及 代码顺序的重要性。

  7. 当然,一般不会这么赋初值的,一般会用静态代码块或者在构造函数里进行 赋初值。都是为了面试而准备的,当然也是说明类初始化的顺序

二、DCL(双重检查) 之 单例模式

1、Double Check Lock

2、volicate 关键字作用及原理

被volatile修饰的变量在编译成字节码文件时会多个lock指令,该指令在执行过程中会生成相应的内存屏障,以此来解决可见性跟重排序的问题。

a、volicate的作用

  1. 线程(内存)可见性
    基于缓存一致性协议,当用voliate关键字修饰的变量改动时,cpu会通知其他线程,缓存已被修改,需要更新缓存。这样每个线程都能获取到最新的变量值。 当一条线程修改了voliate变量的值,新值对于其他线程来说是可以立即得知的
  2. 静止重排序:基于内存屏障的防止指令重排
    voliate修饰的变量,保证变量赋值操作的顺序与程序代码中的执行顺序一致。可以防止cpu指令重排序。底层的实现方式是基于4种内存屏障:读读、读写、写读、读读屏障。

b、预备知识

  • 指令重排序(下面第五节也有细讲)
    • 为什么指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
    • 指令重排序遵守的准则:编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
    • 什么办法来禁止指令重排序呢:添加内存屏障
  • 内存屏障
    • 内存屏障分类:内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
    • 内存屏障作用
      (1)阻止屏障两侧的指令重排序,即屏障下面的代码不能和屏障上面的代码交换顺序(静止重排序)
      (2)在有内存屏障的地方,线程修改完共享变量以后会马上把该变量从本地内存写回到主内存,并且让其他线程本地内存中该变量副本失效(使用MESI协议)(线程可见性)
      对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
      对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,其他线程程可见

b、volicate的原理

原理

  • volatile关键字修饰的变量会存在一个“lock:”的前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线或高速缓存加锁(一般只是对 缓存行 枷锁),可以理解为CPU指令级的一种锁。
  • 在具体的执行上,它先对总线或缓存加锁,然后执行后面的指令,在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。最后释放锁后会把高速缓存中的脏数据(修改过的数据)全部刷新回主内存,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。
  • 在java内存层面可以理解为:当需要使用(use)这个变量时,必须从主存中read–>load这个变量(即要使用这个变量时,必须从主存中读取这个变量,这就保证了该变量是最新的);当线程工作内存中这个变量被赋值时(assign),那么立刻store–>write这个变量(即当该值计算完成,立刻把这个变量写会主存,并且使得该值在其他内存的工作变量中无效)

java内存屏障

在这里插入图片描述
volatile语义中的内存屏障

volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:

  • 在每个volatile写操作前插入StoreStore屏障(这个屏障前后的2个Store指令不能交换顺序),在写操作后插入StoreLoad屏障(这个屏障前后的2个Store Load指令不能交换顺序);

  • 在每个volatile读操作前插入LoadLoad屏障(这个屏障前后的2个Load指令不能交换顺序),在读操作后插入LoadStore屏障(这个屏障前后的2个Load Store指令不能交换顺序);

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。
在Java中对于volatile修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障禁止处理器重排序。

举例
两条线程Thread-A与Threab-B同时操作主存中的一个volatile变量i时。Thread-A写了变量i,那么:
Thread-A发出LOCK#指令
(1)发出的LOCK#指令锁总线(或锁缓存行)(因为它会锁住总线,导致其他CPU不能访问总线,不能访问总线就意味着不能访问系统内存),然后释放锁,最后刷新回主内(瞬间完成的,写回时候其他缓存行失效),同时让Thread-B高速缓存中的缓存行内容失效。
(2)Thread-A向主存回写最新修改的i
Thread-B读取变量i,那么:
Thread-B发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值(重新从主存读)
由此可以看出,volatile关键字的读和普通变量的读取相比基本没差别,差别主要还是在变量的写操作上。

举例
在这里插入图片描述
使用场景
满足以下两点,那么volatile修饰的共享变量,不用加锁也能保证线程安全:

  • 运算结果不依赖变量的当前值(即变量计算的结果和当前的值没有关系,比如一个boolean变量的改变,但是i++这种运算就存在依赖关系,以为新值是在旧值的基础上加1),或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其他的状态变量共同参与不变性约束(即该变量不和其他变量关联)

三、硬件层数据一致性

0、JMM Java内存模型

1、硬件层的并发优化基础知识

  • 存储器的层次结构 (深入理解计算机系统 原书第三版 P421)
    在这里插入图片描述
    在这里插入图片描述
  • 计算机模型:CPU-内存 模型:从下面这张图中可以看出,CPU 到内存直接还有多缓存(L1_cache、L2_cache、L3_cache),速度也是逐级递减(L1_cache基本能和cpu持平,其他的均明显低于cpu,L2_cache的速度大约比cpu慢20-30倍),L1_cache 和 L2_cache 是与CPU的内核在一块儿的,L3_cache 是共享的。
    在这里插入图片描述
  • 总线锁会锁住总线,使得其他CPU甚至不能访问内存中其他的地址,因而效率较低
    在这里插入图片描述

b、Intel 的缓存一致性协议:MESI

协议很多,Intel 的Cache(缓存)一致性协议:MESI。
可参考此网址:https://www.cnblogs.com/z00377750/p/9180644.html

  1. MESI:modified、Exclusive、Shared、Invalid
  2. 现代CPU的数据一致性实现 = 缓存锁(MESI …) + 总线锁

在这里插入图片描述

四、缓存行(面试可能会被问道)-伪共享

1、定义

  • 缓存行:当我们要把内存里面的某一些数据放到CPU自己的缓存时,不会只把这一个数据放进去,比如一个int型数据 12 ,只有 4 个字节,读缓存时不会只把这4个字节的数据读入到缓存,而且为了提高效率,把4个字节后面的一块儿内容全部读进去,读一个内容把一块儿内容全都读进去,这一块儿内容是一个基本的缓存单位,就是 缓存行,读取缓存以cache line为基本单位,目前64bytes。

  • 伪共享问题: 位于同一 缓存行 的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题

  • 使用 缓存行 的对齐能够提高效率 。

在这里插入图片描述

2、案例

a、T01_CacheLinePadding

  1. 案例中, 数组开辟的两个数值的地址,应该在同一个缓存行。
  2. 通过两个线程去频繁的改变他们数值。
  3. 两个变量在一个缓存行,两个线程应该在两个CPU内核,那么会涉及到数据共享问题(上面说过Intel 的CPU 会用 MESI 缓存协议一致性)
  4. 因为涉及到缓存一致性,所以会导致时间会比较长
package com.mashibing.juc.c_001_02_FalseSharing;

public class T01_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class T {
        public long x = 0L; //8bytes
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

b、T02_CacheLinePadding

  1. 案例中, 和上面的案例不同的是,一个数组。这里的是两个缓存行(两个缓存快),因为一个 Long 占 8个字节, 7个变量(p1-p7)就是 7*8 = 56个字节,在加一个 long 类型 变量x 正好是64个字节,缓存行的默认大小是 64个字节。
  2. 通过两个线程去频繁的改变他们数值。
  3. 两个变量在两个缓存行,两个线程应该在两个CPU内核,那么不会涉及到数据共享问题(上面说过Intel 的CPU 会用 MESI 缓存协议一致性)
  4. 因为涉及到缓存一致性,所以会导致时间会比较慢
package com.mashibing.juc.c_001_02_FalseSharing;

public class T02_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class Padding {
        private volatile long p1, p2, p3, p4, p5, p6, p7; // 7*8=56个字节
    }

    private static class T extends Padding {
        public volatile long x = 0L; //8bytes   8+56 = 64 个字节,所以 一个 T 就是一个缓存行
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

c、解析

  1. 按照正常的设想,按照视频的学习,确实代码1要比代码2 的时间要长一些,因为涉及到了缓存一致性的问题。
  2. 但是我这里测试的确实代码1要比代码2的时间要短很多。可能会涉及到电脑类型、CPU内核数等等相关才照成这个问题的。

五、指令乱序执行问题(指令重排序)(二.2.2也有讲)

1、乱序执行指令(并行)

  • CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(去内存读数据要比CPU执行指令慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系

在这里插入图片描述

2、合并写操作

3、合并写案例:WriteCombining

该案例说明:合并写的速度快。

package com.mashibing.juc.c_029_WriteCombining;

/**
 * 原封不动老外的代码:合并写案例
 */
public final class WriteCombining {

    private static final int ITERATIONS = Integer.MAX_VALUE;
    private static final int ITEMS = 1 << 24;
    private static final int MASK = ITEMS - 1;

    private static final byte[] arrayA = new byte[ITEMS];
    private static final byte[] arrayB = new byte[ITEMS];
    private static final byte[] arrayC = new byte[ITEMS];
    private static final byte[] arrayD = new byte[ITEMS];
    private static final byte[] arrayE = new byte[ITEMS];
    private static final byte[] arrayF = new byte[ITEMS];

    public static void main(final String[] args) {

        for (int i = 1; i <= 3; i++) {
            System.out.println(i + " SingleLoop duration (ns) = " + runCaseOne());
            System.out.println(i + " SplitLoop  duration (ns) = " + runCaseTwo());
        }
    }

    public static long runCaseOne() {
        long start = System.nanoTime();
        int i = ITERATIONS;

        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
            arrayD[slot] = b;
            arrayE[slot] = b;
            arrayF[slot] = b;
        }
        return System.nanoTime() - start;
    }

    public static long runCaseTwo() {
        long start = System.nanoTime();
        int i = ITERATIONS;
        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
        }
        i = ITERATIONS;
        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayD[slot] = b;
            arrayE[slot] = b;
            arrayF[slot] = b;
        }
        return System.nanoTime() - start;
    }
}

在这里插入图片描述

4、如何禁止CPU指令重排

六、指令乱序执行证明

1、指令案例:T04_Disorder

a、代码

package com.mashibing.jvm.c3_jmm;

public class T04_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for(;;) {
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                    //shortWait(100000);
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();other.start();
            one.join();other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                //System.out.println(result);
            }
        }
    }

    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

在这里插入图片描述

b、说明

  • x,y的值可能的取值:(1,0)(0,1)(1,1)
    一旦出现了(0,0)说明出现了指令乱序执行。

  • 如果顺序执行,执行顺序可能是

    1. a=1 x=b b=1 y=a,此时 x=0,y=1
    2. a=1 b=1 x=b y=a,此时 x=1,y=1
    3. b=1 y=a a=1 x=b,此时 x=1,y=0
  • 如果是乱序执行,比如说执行顺序如下

    1. x=b y=a a=1 b=1,此时 x=0,y=0
    2. x=b a=1 y=a b=1,此时 x=0,y=1
  • 如果结果出现 x=0,b=0,证明cpu乱序执行。事实证明,在结果输出中,确实发生了指令重排。

2、如何保证特定情况下不乱序(这里与五.4的内容类似)

a、硬件内存屏障 Intel X86

  • 加锁是肯定的可以的,但是在不同的CPU(很多CPU)都添加了 硬件内存屏障(CPU级别的内存屏障,和java的内存屏障无关系)

  • Intel 设置的比较简单,只有三条指令。

    1. sfence(store fence 存栅栏的意思): store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
    2. lfence(load fence 读屏障):load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
    3. mfence(二者之和):modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

intel lock汇编指令(java的),原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序

b、JVM级别如何规范(JSR133)

  • LoadLoad屏障:
    对于这样的语句Load1; LoadLoad; Load2,
    在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • StoreStore屏障:
    对于这样的语句Store1; StoreStore; Store2,
    在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

  • LoadStore屏障:
    对于这样的语句Load1; LoadStore; Store2,
    在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

  • StoreLoad屏障:
    对于这样的语句Store1; StoreLoad; Load2,
    在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

  • 上面的JVM级别的四个指令屏障 都是依赖于 硬件去实现的,硬件的实现不是只有内存屏障能够实现JVM级别的内存屏障,即JVM的内存屏障或者交JVM级别的有序性,它的硬件级别的实现并不一定依赖于硬件级别的内存屏障,还依赖于硬件级别的 lock 指令

  • 硬件界别的和JVM级别不是一回事儿。

3、volatile的实现细节

volatile 的实现过程,分不同的层面,包含以下三个层面。

  1. .java 编译成 字节码(byte code) .class 这是字节码层面
  2. 字节码 在JVM层级实现,这是 JVM层面
  3. JVM 交付到 硬件 去执行,这是 OS硬件层面

a、字节码层面

  1. ACC_VOLATILE
  2. 只需要一个 volatile 关键字即可
  3. 访问标志(access flag):就是修饰符,比如 public、private、protect等。
    在这里插入图片描述

b、JVM层面

到了JVM屏障,volatile内存区的读写 都加屏障

StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier

c、OS和硬件层面

https://blog.csdn.net/qq_26222859/article/details/52235930
hsdis 工具 - HotSpot Dis Assembler
在 windows 使用 lock 指令实现 | MESI 实现

4、synchronized实现细节

同 volatile 一样,也是三个层级。

  1. 字节码层面
  2. JVM层面
  3. OS 硬件层面

a. 字节码层面

从下面截图中可以看出,使用的是 ACC_SYNCHRONIZED 修饰符。
monitorenter monitorexit

i、小案例
package com.mashibing.jvm.c3_jmm;

public class TestSync {
    synchronized void m() {

    }

    void n() {
        synchronized (this) {

        }
    }

    public static void main(String[] args) {

    }
}

从编译后的字节码看到如下图
在这里插入图片描述

ii、解析

synchronized 块 中可以看出,有三个指令, monitorenter,monitorexit,monitorexit。后两个是一个指令。

为什么会三个,后两个是一样的呢,逻辑如下图,第三个指令是如果出现了异常进行捕捉,才用到第三个 monitorexit 指令。
在这里插入图片描述

b. JVM层面

是C 和 C++ 写的,则 C 和 C++ 调用了操作系统提供的同步机制。

c. OS和硬件层面

CPU Intel X86 : 使用 lock cmpxchg / xxx 等指令。
https://blog.csdn.net/21aspnet/article/details/88571740

相关文章
|
1月前
|
存储 缓存 数据库
解决缓存与数据库的数据一致性问题的终极指南
解决缓存与数据库的数据一致性问题的终极指南
150 63
|
2月前
|
消息中间件 canal 缓存
项目实战:一步步实现高效缓存与数据库的数据一致性方案
Hello,大家好!我是热爱分享技术的小米。今天探讨在个人项目中如何保证数据一致性,尤其是在缓存与数据库同步时面临的挑战。文中介绍了常见的CacheAside模式,以及结合消息队列和请求串行化的方法,确保数据一致性。通过不同方案的分析,希望能给大家带来启发。如果你对这些技术感兴趣,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
147 6
项目实战:一步步实现高效缓存与数据库的数据一致性方案
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
41 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
2月前
|
缓存 NoSQL Java
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
缓存与分布式锁、Redisson分布式锁、缓存数据一致性【必须满足最终一致性】
118 14
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
|
2月前
|
消息中间件 缓存 NoSQL
15)如何保证缓存和数据库之间的数据一致性
15)如何保证缓存和数据库之间的数据一致性
59 1
|
3月前
|
缓存 监控 架构师
缓存数据一致性 - 架构师峰会演讲实录
缓存数据一致性 - 架构师峰会演讲实录
|
3月前
|
缓存 NoSQL Redis
【Azure Redis 缓存】Azure Cache for Redis 服务的导出RDB文件无法在自建的Redis服务中导入
【Azure Redis 缓存】Azure Cache for Redis 服务的导出RDB文件无法在自建的Redis服务中导入
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
6天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
4天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
7 1