JAVA基础JVM讲解

JVM体系结构

在这里插入图片描述

在这里插入图片描述 

主要包括两个子系统和两个组件:

Class loader(类装载器) 子系统(用来装载.class文件);

Execution engine(执行引擎) 子系统(执行字节码,或者执行本地方法);

Runtime data area (运行时数据区域)组件(方法区、堆、java栈、PC寄存器、本地方法栈);

Native interface(本地接口)组件。

  • Class loader子系统:根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。

  • Execution engine子系统:执行classes中的指令。方法的字节码是由Java虚拟机的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。执行引擎执行字节码时,首先取得一个操作码,如果操作码有操作数,取得它的操作数。它执行操作码和跟随的操作数规定的动作,然后再取得下一个操作码。这个执行字节码的过程在线程完成前将一直持续。任何JVM实现的核心是Execution engine, 换句话说:Sun 的JDK 和IBM的JDK好坏主要取决于他们各自实现的Execution engine的好坏。

  • Native interface组件 :与native libraries交互,是其它编程语言交互的接口。Java里声明为native的方法多数在jdk/src/<platform>/native里可以找到。其中可以是share,也就是平台中立的代码;也可以是某个具体平台。这个native目录里的结构跟Java源码结构一样是按包名来组织的。不过需要提醒的是,这些native方法不是“JVM”的,是“类库”的,不在JVM里面。

  • Runtime data area 组件:虚拟机定义了若干种程序运行时使用到的运行时数据区,有一些是随虚拟机的启动而创建,随虚拟机的退出而销毁,如堆、方法区。第二种则是与线程一一对应,随线程的开始和结束而创建和销毁,如栈,寄存器。

JVM内存模块

Java内存模型是Java语言在多线程并发情况下对于共享变量读写(实际是共享变量对应的内存操作)的规范,主要是为了解决多线程可见性、原子性的问题,解决共享变量的多线程操作冲突问题

JMM(java内存模型)Java Memory Model,本身是一个抽象的概念,不是真实存在的,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程读/写共享变量的副本。

JMM内存模型三大特性
1、原子性
使用 synchronized 互斥锁来保证操作的原子性
2、可见性:
volatile,会强制将该变量自己和当时其他变量的状态都刷出缓存。
synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
3、有序性
源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 ->最终执行的命令。
重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
处理器在进行重排时必须考虑数据的依赖性,多线程环境线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

JMM定义了线程和主内存之间的抽象关系:

线程之间的共享变量存储在主内存(Main Memory)中
每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。

堆内分配:

JVM运行时数据区域

1 方法区 MethodArea
方法区(MethodArea)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。

运行时常量池

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

Java虚拟机对于Class文件每一部分(自然也包括常量池)的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池,《Java虚拟机规范》并没有做任何细节的要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域,不过一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中[1]。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

2 Java堆Heap
对于Java应用程序来说,Java堆(JavaHeap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”,而这里笔者写的“几乎”是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。

Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“FromSurvivor空间”“ToSurvivor空间”等名词,这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分。如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(ThreadLocalAllocationBuffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

3 虚拟机栈
每个线程运行所需要的内存空间,成为虚拟机栈;
线程私有,生命周期和线程一致;
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存;
每个线程只能有一个活动栈帧,对应当前执行的方法。
描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。

OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

4 程序计数器
程序计数器(ProgramCounterRegister)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

保存当前执行指令的地址,一旦程序执行,程序计数器将更新到下一条指令。

为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。

5  本地方法栈
本地方法栈(NativeMethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务

《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规

定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

6  直接内存
直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。

在JDK1.4中新加入了NIO(NewInput/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

4 执行引擎
尽管并不是所有的Java虚拟机都采用解释器与编译器并存的运行架构,但目前主流的商用Java虚拟机,譬如HotSpot、OpenJ9等,内部都同时包含解释器与编译器。

HotSpot虚拟机中内置了两个(或三个)即时编译器,其中有两个编译器存在已久,分别被称为“客户端编译器”(ClientCompiler)和“服务端编译器”(ServerCompiler),或者简称为C1编译器和C2编译器(部分资料和JDK源码中C2也叫Opto编译器),第三个是在JDK10时才出现的、长期目标是代替C2的Graal编译器。

垃圾回收(GC)

无法手动垃圾回收,只能手动提醒,等待JVM自动回收
GC的作用区在堆(Heap)和方法区中
JVM进行GC时,并不是统一对这三区域(新生区,幸存区,老年区)统一回收,回收都是新生代
轻GC(普通GC)只针对于新生区,偶尔作用幸存区(在新生区满的情况下)
重GC(全局GC)全局释放内存
14、常见垃圾回收算法
1.引用计数算法
原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的问题。

2.复制算法
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中,同时回收未使用的对象。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理。
优点:不会出现碎片化问题
缺点:需要两倍内存空间,浪费

3.标记-清除算法
此算法执行分两阶段。第一阶段从引用根节点开始标记所用存活的对象,第二阶段遍历整个堆,把未标记的对象清除。
优点:不会浪费内存空间
缺点:此算法需要暂停整个应用,同时,会产生内存碎片

4.标记-压缩算法
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有存活的对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。
此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

分代回收策略

1.绝大多数刚刚被创建的对象会存放在Eden区
2.当Eden区第一次满的时候,会触发MinorGC(轻GC)。首先将Eden区的垃圾对象回收清除,并将存活的对象复制到S0,此时S1是空的。
3.下一次Eden区满时,再执行一次垃圾回收,此次会将Eden和S0区中所有垃圾对象清除,并将存活对象复制到S1,此时S0变为空。
4.如此反复在S0和S1之间切换几次(默认15次)之后,还存活的对象将他们转移到老年代中。
5.当老年代满了时会触发FullGC(全GC)
MinorGC
使用的算法是复制算法
年轻代堆空间紧张时会被触发
相对于全收集而言,收集间隔较短
FullGC
使用的算法一般是标记压缩算法
当老年代堆空间满了,会触发全收集操作
可以使用 System.gc()方法来显式的启动全收集
全收集非常耗时

这里写图片描述

猜你喜欢

转载自blog.csdn.net/hongyucai/article/details/130970444