JVM原理最全、清晰、通俗讲解(包含类加载机制讲解)

1.Java自动管理堆(Heap)和栈,程序员不能直接的设置堆和栈。

2.操作系统的堆和栈

堆(操作系统):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表。

栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量值等。操作方式与数据结构中的栈相类似。

3.为什么JVM的内存是分布在操作系统的堆中?

因为操作系统的栈是操作系统管理的,它随时会被回收,所以如果JVM放在栈中,那Java的一个null对象就很难确定会被谁回收了,那GC的存在就一点意义都莫有了,而要对栈做到自动释放也是JVM需要考虑的,所以放在堆中就最合适不过了。

4.操作系统与JVM

上图表明:JVM虚拟机位于操作系统的堆中。

程序员写好的类加载到虚拟机执行的过程是:当一个ClassLoader启动的时候,ClassLoader的生存地点在JVM中的堆,然后它去主机硬盘上将 A.class 装载到 JVM 的方法区,方法区中的这个字节文件会被虚拟机拿来 new A字节码() ,然后在堆内存生存了一个A字节码的对象,然后 A字节码这个内存文件有两个引用(一个指向A的class对象,一个指向加载自己的ClassLoader)

5.JAVA 虚拟机的生命周期

生命周期的起点是当一个Java应用 main 函数启动时虚拟机也同时被启动,而只有当在虚拟机实例中的所有非守护线程都结束时,java 虚拟机实例才结束生命。

6.JAVA 虚拟机与 mian 方法的关系

main函数就是一个Java应用的入口,main函数执行时,java 虚拟机就启动了。启动了几个 main 函数就启动了几个 java 应用,同时也启动了几个 java 虚拟机。

7.java 虚拟机的线程

java虚拟机有两种线程,一种叫做守护线程,一种叫做非守护线程(也叫普通线程),main 函数就是个非守护线程,虚拟机的 gc 就是一个守护线程。java 虚拟机中,只要有任何非守护线程还没有结束,java 虚拟机的实例都不会退出,所以即使 main 函数这个非守护线程退出,但由于在 main 函数中启动的匿名线程也非守护线程,它还没有退出,所以 jvm 也没有办法退出。

8.jvm 虚拟机的 gc(垃圾回收机制)就是一个典型的守护线程。

9.实例,理解“当所有的非守护线程全部结束,jvm生命周期才结束”

public class MainAndThread {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }
                System.out.println("睡了5s后打印,这是出main之外的非守护线程,这个推出后这个引用结束,jvm声明周期结束。任务管理的java/javaw.exe进程结束");
            }
        }).start();
        System.out.println("main 线程直接打印,main 线程结束,电脑任务管理器的java/javaw.exe进程并没有结束。");
    }
}

10. 变量回收

GC 垃圾回收机制不是创建的变量变为空就被立刻回收,而是超出了变量的 作用域 后被自动回收。

11.程序在 jvm 的流程

首先,当一个程序启动之前,它的class会被类装载器装入方法区执行引擎读取方法区的字节码自适应解析,边解析就边运行(其中一种方式),然后程序计数器指向 main 函数所在位置,虚拟机开始为 main 函数在 java 栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑 main 函数,main 函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操作系统会为本地方法分配本地方法栈,用来存储一些临时变量,然后运行本地方法,调用系统 API 等等。

12.根据Java虚拟机规范的规定,如果方法区的内存空间不能满足内存分配时,将抛出 OutOfMemoryError异常

13.jvm 结构图

方便理解可把上图分为“功能区”和“数据区”(可以参考下图)。

功能区:垃圾回收系统、类加载器、执行引擎

数据区:也就是这鞥个运行时数据区。

13.1 jvm内部执行运行流程图

14.jvm 结构图各模块的生命周期总结

对13中的jvm结构图,做一下统计,启动一个jvm虚拟机程序就是启动了一个进程。启动的同时就在操作系统的堆内存中开辟一块jvm内存区,对于图13中各个小模块的生命周期:

虚拟机栈、本地方法栈、程序计数器这三个模块是线程私有的,有多少线程就有多少个这三个模块,生命周期跟所属线程的生命周期一直。以程序计数器为例,因为多线程是通过线程轮流切换和分配执行时间来实现,所以当线程回到正确执行的位置,每个线程都有独立的程序计数器,各个线程之间的计数器互不影响,独立存储。

其余是跟jvm虚拟机的生命周期一致。

15. 13图中,程序计数器模块是jvm内存区域唯一不会报OutOfMemoryError情况的区域

16. 结合13图,我们总结出 jvm 内存包含两个子系统和两个组件

两个子系统:ClassLoader子系统和ExecutionEngine(执行引擎)子系统。

两个组件:RuntimeDataArea(运行时数据区域)组件和NativeInterface(本地库接口)组件。

从图中可以看出运行时数据区包括5部分:方法区、堆、虚拟机栈、本地方法栈、程序计数器

17.什么是本地库接口和本地方法库

(1) 本地方法库接口:即操作系统所使用的编程语言的方法集,是归属于操作系统的。

(2) 本地方法库保存在动态链接库中,即 .dll(Windows系统)文件中,格式是各个平台专有的

18. 双亲委派机制

JVM 在类加载时默认采用的是双亲委派机制。通俗的讲就是某个特定的类加载器在接到加载类的请求时,首先将任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才会去自己加载。

如下图:

例如:当jvm要加载 Test.class 的时候,

(1) 首先会到自定义加载器中查找(其实就是看运行时数据区的方法区有没有加载),看看是否已经加载过,如果已经加载过,则返回字节码。

(2) 如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过 Test.class 。

(3) 如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。

(4) 如果没有加载过,则继续询问上一次加载 器(BootstrapClassLoader)是否已加载过。

(5) 如果 BootstrapClassLoader 依然没有加载过,则到自己指定类加载路径下(“sun.boot.class.path”)查看是否有 Test.class 字节码,有则返回,没有通知下一层加载器(ExtClassLoader)到自己指定的类加载路径下(java.ext.dirs)查看。

(6) 依次类推,最后自定义类加载器指定的路径还没有找到 Test.class 字节码,则抛出 ClassNotFoundException 。

代码如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查,是否已经加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //父加载器不为空,调用父类加载器的loadClass,依次递归
                        c = parent.loadClass(name, false);
                    } else {
                        //父类加载器为空,则调用BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //父加载器没有找到,则调用 findClass
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //调用resolveClass()
                resolveClass(c);
            }
            return c;
        }
    }

为什么要使用这种方式呢?

这里要注意几点:

(a) 类加载器代码本身也是 java 类,因此类加载器本身也是要被加载的,因此显然必须有第一个类加载器不是Java类这就是 BootstrapClassLoader,它是使用 C++ 写的。

(b) 虽说 BootstrapClassLoader、ExtClassLoader、AppClassLoader三个是父子类加载器关系,但是并没有使用继承,而是使用了组合关系。

(c) 优点,具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,可以比较笼统的说像 JDK 自带的几个 jar 包肯定是位于最顶级的,再就是我们引用的包,最后是我们自己写的,保证了 java 程序的稳定性。

19. JDK、JRE、JVM 的关系

JDK 是 java 语言的软件开发工具包。在 JDK 的安装目录下有一个 jre 目录,里面有两个文件夹 bin 和 lib,在这里可以认为 bin 里的就是 jvm,lib 中则是 jvm 工作所需要的的类库,而 jvm 和 lib 合起来就成为 jre

JDK、JRE、JVM的关系图:

20. JVM 运行简易过程

上图左半部分其实不是在 JVM 中,程序员在 Eclipse 上写的是 .java 文件,经过编译转换成 .class 文件(比如 maven 工程需要maven install,打成 jar 包,jar 包里面都是 .class 文件);这些步骤都是在 Eclipse 上进行的。然后类加载器一直到解释器都是属于 JVM 的。

21.解释 13中 JVM 结构图各模块的内容

21.1 程序计数器(Program Counter Register)

程序计数器也称PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码;指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

(1) 区别于计算机硬件的PC寄存器,两者略有不同。计算机用pc寄存器来存放“伪指令”或地址;而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。

(2) 当虚拟机正在执行的方法是一个本地(native)方法的时候, jvm 的 pc寄存器存储的值是 undefined。

(3) 程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。

(4) 此内存区域是唯一一个在 java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

21.2 Java虚拟机栈(Java Virtual Machine Stack)

(1) 线程私有的,它的生命周期与线程相同,每个线程都有一个。

(2) 每个线程创建的同时会创建一个JVM栈, JVM 栈中每个栈帧存放当前线程中局部基本类型的变量(a.java中定义的八种基本类型:booleancharbyteshortintlongfloatdouble;b.reference(32位以内的数据类型,具体根据JVM位数(64还是32位)有关);c.部分的返回结果)。非基本类型的对象在JVM栈上仅存放一个指向堆上的地址;

(3) 每一个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机从入栈到出栈的过程

(4) 栈运行原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个有关方法和运行期数的数据集。当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈...,依次执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧。

(5) Java虚拟机栈的最小单位可以理解为一个个栈帧,一个方法对应一个栈帧,一个栈帧可以执行很多指令,如下图:

(6) 对上图中的动态链接解释下,比如当出现 main 需要调用 method1() 方法的时候,操作指令就会触动这个动态链接,就会找到方法区中对应的 method1(),然后把 method1() 方法压入到虚拟机栈中, 执行 method1() 栈帧的指令;此外如果指令表示的代码是个常量,这也是个动态链接,也会到方法区中的运行时常量池,找到类加载时就专门存放变量的运行时常量池的数据。

21.3 本地方法栈(Native Method Stack)

先解释什么是本地方法?

jvm 中的本地方法是指方法的修饰符是带有 native 的,方方法体不是用 java 代码写的这一类方法,这类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。

作用同 java 虚拟机栈,区别是:虚拟机栈为虚拟机执行 java 方法服务,而本地方法栈是为虚拟机使用到的 Native 方法服务。

本地方法栈是线程私有的,它的生命周期与线程相同,每个线程都有一个。

21.4 Java 堆(java heap)

(1) 堆是 java 虚拟机所管理的内存中最大的一块

(2) 不同于上面3个,堆是 jvm 所有线程共享的。

(3) 堆在虚拟机启动的时候创建

(4) 堆的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存

(5) 堆是垃圾收集器管理的主要区域

(6) 很多时候 java 堆也被称为 GC 堆。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java对还可以细分为:新生代和老年代;新生代又可以分为:Eden区、From Survivor 区、To Survivor 区。

(7) java 堆是计算机物理存储上不连续的、逻辑上时连续的,也是大小可以调节(通过 -Xms 和 -Xmx 控制)

(8) 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

21.5 方法区(Method Area)

(1) 方法区在虚拟机启动的时候创建

(2) 方法区被所有 Jvm 线程共享

(3) 除了和堆一样不需要连续的内存空间和可以固定大小或者可扩展外,还可以选择不实现垃圾收集。

(4) 用于存放已被虚拟机加载的类信息、常量、静态变量、以及编译后的方法实现的二进制形式的机器指令集等数据。

(5) 被装载的 class 信息存储在 Method Area 的内存中。当虚拟机装载某个类型时,它是由类装载器定位相应的 class 文件,然后读入这个 class 内容并把它传输到虚拟机中。

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

方法区补充:指令集是个非常重要的概念,因为程序员写的代码其实在 jvm 虚拟机中是被转成了一条条指令集执行的,看下图:

首先看上面各部位位于13jvm结构图中的那些未知:左侧的 foo() 代码是指令集存储在方法区;程序计数器就不用说了;’局部变量位于虚拟机栈中;右侧最下方的求值栈(也就是操作数栈),我们从动图中可以明显看到栈顶这个关键词,因此也是位于java虚拟机栈中。

另外,图中,指令是 java 经过 javac 编译后得到的 jvm 指令, pc寄存器 指向下一条该执行的指令地址,局部变量区存储函数运行中产生的局部变量,栈存储计算的中间结果和最后结果。

上图执行的源码是:

public class Demo {
    public static void foo() {
       int a = 1;
       int b = 2;
       int c = (a + b) * 5;
    }
}

下面简单解释下执行过程,注意:偏移量的数字只是简单代表第几个指令。首先常数1入栈,栈顶元素就是1,然后栈顶元素移入局部变量区存储,常数2入栈,栈顶元素变为2,然后栈顶元素移入局部变量区存储;接着1,2再次入栈,弹出栈顶两个元素相加后的结果入栈,将5入栈,栈顶两个元素弹出并相乘后入栈,然后栈顶变为15,最后移入局部变量区。执行 return 命令,如果当前线程对应的栈中没有了栈帧,这个 java 栈帧也将会被 jvm 撤销。

21.6 类加载器子系统(class loader subsystem)

(1) 根据给定的全限定名类名(如 java.lang.Object)来装载 class 文件的内容到运行时数据区(runtime data area)中的方法区(method area)。java 程序员可以 extends java.lang.ClassLoader 类写自己的 ClassLoader。

(2) 对(1)中的加载过程是:当一个 ClassLoader 启动时,ClassLoader 的生存地点在 jvm 中的堆,然后它去主机硬盘上去挂载 A.class 到 jvm 的方法区,方法区中的这个A字节码内存块被虚拟机拿来 new A字节码,在堆内存生存一个A的class对象,然后A字节码内存块有两个引用,一个指向 A的class对象,一个指向加载自己的 ClassLoader。如下图:

21.7 执行引擎子系统(Execution Engine子系统)

(1)负责执行来自 类加载器子系统 中加载进来的指令集(存在于方法区),通俗地讲就是类加载器子系统把代码逻辑(什么时候该if,什么时候该相加、相减)以指令的形式加载到了方法区,执行引擎就负责执行这些指令。用网上最流行的一张图如下:

(2) 程序在 JVM 主要执行的过程就是执行引擎与运行时数据区不断交互的过程,可以理解为上面“方法区中的动图”

(3) 但是执行引擎拿到的方法区中的指令还是人能够看懂的,这里执行引擎的工作就是把指令转成 JVM 执行的语言(也可以理解成操作系统的语言),最后操作系统语言在转成计算机机器码。

(4) 字节码解释器:一条一条地读取,解释并执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行。即时编译器(Just-In-Time):即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码笔一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

这里的字节码解释器对应20中的解释器。 简单理解 JIT 就是当代码中某些方法复用次数比较高的,并超过一个特定的值就成为了“热点代码”。这个热点代码就会被 JIT 编译成本地代码加快访问速度。

22. 本地(Native)方法

(1) 本地方法就是带有 native 标识符修饰的方法

(2) native 标识符修饰的方法并不提供方法体,但因为其实现体是由非java代码在外部实现的,因此不能与 abstract 连用。

(3) 存在的意义:不方便用 java 代码写的代码,使用更为专业的语言写更合适;甚至有些 JVM 的实现就是用 C 编写的

(4) 更多的本地方法最后是与 jdk 的执行引擎的解释器语言一致

(5) Windows、Linux、Unix、Dos操作系统的核心代码大部分都是使用 c 和 c++ 编写,底层接口用汇编编写。

(6) 为什么 native 方法修饰的方法 pc程序计时器 为 undefined。读懂什么的知识点就很容易自己理解了。在一开始类加载时, native修饰的方法就被保存在了本地方法栈中,当需要调用native方法时,调用的是一个指向本地方法栈中某个方法的地址,然后执行方法直接与操作系统交互,返回运行结果。整个过程并没有经过执行引擎的解释器把字节码解释成操作系统语言,pc程序计数器也就没有器作用。

23. GC垃圾回收机制

、了解堆内存:类加载器读取了类文件后,需要把类、方法、常量放到堆内存中,以方便执行器执行,堆内存分为三部分,如下图:

23.1 新生区

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden Space)和存活区(Survivor Space),所有的类都是在伊甸区被 new 出来的。存活区有两个:0区(Survivor 0 Space)和1区(Survivor 1 Space)。当伊甸园的空间用完时,程序有需要创建对象,JVM垃圾回收器将对伊甸区进行垃圾回收(Minor GC),将伊甸区中的剩余对象移动到存活0区。若存活0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(Full GC),进行养老区的内存清理。若养老区执行 Full GC 之后发现依然无法保存对象,就会产生 OOM 异常 “OutOfMemoryError”。

如果出现 java.lang.OutOfMemoryError:Java heap space 异常,说明Java虚拟机的堆内存不足。原因有二:

  • Java 虚拟机的堆内存设置不够,可以通过-Xms、-Xmx来调整
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器手机(存在被引用)

23.2 养老区

养老区用于保存从新生区筛选出来的Java对象,一般池对象都在这个区域活跃。

23.3 永久区

永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

如果出现java.lang.OutOfMemoryError:PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。原因有二:

  • 程序启动需要大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
  • 大量动态反射生成的类不断被加载,最终导致Perm区被占满

说明:

  • JDK 1.6之前:常量池分配在永久代。
  • JDK 1.7:有,但已经逐步“去永久代”。
  • JDK 1.8及置换:无(java.lang.OutOfMemoryError:PermGen space 不会出现在 JDK1.8中)。

24. 21运行时数据区各个模块协作工作的总结较好的图

首先要执行的代码是

先执行main方法

当要调用其他方法时

25. Java代码编译(Java Compiler)过程,也就是由 .java 文件 到 .class 文件的过程(附上网上很流行的图)

注:源代码就是 .java 文件,JVM 字节码就是 .class 文件

27.是先加载字节码文件还是先执行main方法?

先加载字节码文件到方法区,然后再找到 main 执行程序。

28.java被编译成了class文件,JVM怎么从硬盘上找到这个文件并装载到JVM?

是通过本地 java 接口(JNI),找到 class 文件后并装载进 JVM,然后找到main方法,最后执行。

29.我们平时所说的八大基本类型在栈中的存放位置

运行时数据区 -> 虚拟机栈 -> 虚拟机栈的一个栈帧 -> 栈帧中的局部变量表;局部变量表存放的数据除了八大基本类型外,还可以存放一个reference(局部变量表的容量的最小单位 变量槽(slot)的大小);所以可以是存放字符串类型的,但是要以 String a = "aa"; 的形式出现,如果是 new Object() 那就只能出现在堆中了,栈里面存的是栈执行堆的地址。

30.堆内存大小 -Xms -Xmx 设置相同,因为 -Xmx 越大 tomcat就有更多的内存可以使用,这就意味着 JVM 调用 垃圾回收机制的频率就会减少(垃圾回收机制是jvm内存不够时自动调用的),可以避免每次垃圾回收完后jvm重新分配内存。

31.GC 具体什么时候执行,这个是由系统来进行决定的,是无法预测的。

32.方法区也被称为内存中的永久代

看下面例子:

Student s = new Student("小马", 18);

s是指针,存放在栈中
new Student("小马", 18) 是对象,存放在堆中
Student类信息存放在方法区

总结:
对象的实例保存在堆上,对象的元数据(instantClass)保存在方法区,对象的引用保存在栈上。

类加载是会先看方法区有没有意见加载过这个类,因此方法区中的类是唯一的。方法区中的类都是运行时的,都是正在使用的,是不能被GC的,所有可以理解成永久代。

33.java 内存模型

最后一个知识点,理解下多线程的一点知识。我们应该知道了在运行时数据区中虚拟机栈、pc寄存器、本地方法栈是每个线程都有的,很明显这些都是独立的不会发生线程不安全的问题,但是平时讨论的线程不安全、要枷锁等等情况是怎么回事呢?

其实,发生线程不安全问题的原因在于cpu,看下图,简单理解cpu:

在 CPU 内部有一组 CPU 寄存器,也就是  CPU 的存储器。 CPU 操作寄存器的速度比操作计算机主存快的多。在主存和 CPU 寄存器之间还存在一个  CPU缓存 , CPU 操作  CPU缓存 的速度快与主存但慢于  CPU寄存器。某些  CPU 可能有多个缓存层(一级缓存和二级缓存)。计算机的主存也称为RAM,所有的 CPU 都能访问主存,而且主存比上面提到的缓存和寄存器大很多。

当一个  CPU 需要访问主存时,会先读取一部分主存数据到  CPU缓存,然后在读取  CPU缓存 到寄存器。当  CPU 需要写数据到主存时,同样会先 flush 寄存器到  CPU缓存,然后再在某些节点吧缓存数据 flush 到主存。

Java 内存模型和硬件架构之间的桥接:

正如上面讲到的,Java内存模型和硬件内存架构并不一致。硬件内存架构中并没有区分栈和堆,从硬件上看,不管是栈还是堆,大部分数据都会存到主存中,当然一部分栈和堆的数据也可能会存到 CPU寄存器 中,如下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系:

当对象和变量存储到计算机的各个内存区域时,必然会面对一些问题,其中最主要的两个问题是:

1. 共享对象对各个线程的可见性;2.共享对象的竞争关系

问题1:共享对象的可见性

当多个线程同时操作同一个共享对象时,如果没有合理的使用 volatile 和 synchronized 关键字,一个线程对共享对象的更新可能导致其他线程不可见。

想象一下我们的共享对象存储在主存,一个CPU中的线程读取主存数据到CPU缓存,然后对共享对象做了更改,但CPU缓存中更改后的对象还没有flush到主存,此时线程对共享对象的更改对其他的CPU中的线程是不可见的。最终就是每个线程都会拷贝共享对象,而且拷贝对象位于不同的CPU缓存中。

下图展示了上面描述的过程。左边CPU中运行的线程从主存拷贝共享对象obj到它的CPU缓存,把对象obj的 count 变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中;

 要解决共享对象可见性这个问题,我们可以使用java volatile关键字,volatile关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU内存屏幕指令实现的。

问题2:竞争现象

如果多个线程共享一个对象,如果他们同时修改这个共享对象,这就产生了竞争现象。

线程A和线程B共享一个对象obj。假设线程A从主存读取obj.count变量到自己的CPU缓存,同时线程B也读取了 obj.count 变量到它的CPU缓存,并且这两个线程都对obj.count 做了加1操作。此时, obj.count 加1操作被执行了两次,不过都在不同的CPU缓存中。

如果这两个加1操作是串行的,那么 obj.count 变量会在原始值上加2,最后 obj.count 的值会是3,。然而下图中的两个 加1 是并行的,不管是线程A还是线程B先 flush 计算结果到主存,最终主存中的 obj.count 只会增加1次变成2,尽管一共有两次加1操作。

要解决上面的问题我们可以使用 java synchronized 代码块。 synchronized 代码块可以保证同一时刻只能有一个线程进入代码竞争区, synchronized 代码块也能保证代码块中的所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会flush主存,不管这些变量是不是volatile类型的。

volatile 和 synchronized区别

volatile 本质是在高速 jvm 当前变量存在寄存器中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。

volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以保证变量修改可见性和原子性。

volatile 不会造成线程阻塞;synchronized 可能会造成线程阻塞。

volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

34.方法区与堆的区别

方法区存放了类的信息,有类的静态变量、final类型变量、field自动信息、方法信息,处理逻辑的指令集,我们仔细想想一个类里面也就这些东西,而堆中存放的是对象和数组,咋一看好像方法区跟堆的作用是一样的。其实呢:1.这里就关系到我们平时所说的对象是类的实例,是不是有点恍然大悟了?这里的对应关系就是“方法区-类”“堆-对象”,以“人”为例,堆里面存放的是你这个“实实在在的人,有血有肉”,而方法区中存放的是描述你的文字信息,如“你的名字、身高、体重,还有你的行为,如吃饭、走路等”。 2.再者我们从另一个角度理解,就是从前我们得知方法区中的类是惟一的,同步的。但是我们在代码中往往同一个类会new 几次,也就是由多个实例,既然有多个实例,那么在堆中就会分配多个实例空间内存。

35.方法区的内容是一次把一个工程的所有类的信息都加载进去还是边加载边执行呢?

其实单从性能方面也能猜测到是直接在当前使用的类,也就是边加载边执行。例如我们使用tomcat启动一个spring工程,通常启动过程中会加载数据库信息,配置文件中的拦截器信息,service的注解信息,一些验证信息等,其中的类信息就会率先加载到方法区。但如果我们想让程序启动快一点就设置懒加载,把一些验证去掉,如一些类信息的加载等真正使用的时候再去加载,这样说明了方法区的内容可以先加载进去,也可以在使用到的时候加载。

36.方法区、栈、堆之间的过程

类加载器加载的类信息放到方法区--》执行程序后,方法区的方法压入栈的栈顶--》栈指向压入栈顶的方法--》遇到 new对象 的情况就在堆中开辟这个类的实例空间。(这里栈是由此对象在堆中的地址的)

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM的类加载是由类加载器及其子类实现的。类加载器是Java运行时系统的重要组成部分,负责在运行时查找和加载类文件中的类。在JVM中,类加载器按照一定的层次结构进行组织,每个类加载器负责加载特定位置的类。其中,启动类加载器(Bootstrap ClassLoader)是负责加载存放在<JAVA_HOME>/lib目录中的核心类库,如rt.jar、resources.jar等,同时也可以加载通过-Xbootclasspath参数指定的路径中的类库。启动类加载器是用C语言编写的,随着JVM启动而加载。当JVM需要使用某个类时,它会通过类加载器查找并加载这个类。加载过程会经历连接阶段,包括验证、准备和解析。在验证阶段,JVM会确保加载的类信息符合JVM规范。在准备阶段,JVM会为类变量分配内存并设置初始值,在方法区中分配这些内存。在解析阶段,JVM会根据符号引用替换为直接引用,以便后续的使用。通过类加载器的协同工作,JVM能够在运行时动态加载类,从而实现Java的灵活性和跨平台性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [JVM 的类加载原理](https://blog.csdn.net/ChineseSoftware/article/details/119212339)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [JVM类加载器](https://blog.csdn.net/rockvine/article/details/124825354)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值