学学Java基础——JVM

什么是JVM,为什么要学习JVM

JVM是Java虚拟机(Java Virtual Machine)的缩写,它是一种能够运行Java字节码的抽象计算机,它提供了一个与平台无关的执行环境,使得Java程序可以在不同的操作系统上运行。JVM运行上操作系统层之上,应用程序层之下,JAVA正是因为有了JVM,才具有跨平台能力,JAVA源文件需要通过JVM转译后运行

学习JVM可以提高Java程序的性能和稳定性:通过了解JVM的工作原理和机制,可以对Java程序进行合理的优化和调整,提高程序的响应速度和吞吐量,降低系统资源消耗,减少垃圾回收频率,提高代码质量等。还可以提高Java程序的可移植性和兼容性:通过了解JVM的平台无关性和字节码规范,可以使Java程序在不同的操作系统和硬件设备上运行,保证程序的一致性和兼容性。还能提高Java程序的安全性和可靠性:通过了解JVM的安全机制和异常处理机制,可以使Java程序在运行时避免一些潜在的风险和错误,保证程序的安全性和可靠性。

JVM有什么内容

JVM基本结构

注:JVM调优基本是堆和方法区调优

类加载器

Java中有四种类型的类加载器

1,启动类加载器(Bootstrap ClassLoader):用C++实现的,是虚拟机自身的一部分,主要负责加载核心的类库,如<JAVA_HOME>\lib下的rt.jar等。

2,扩展类加载器(Extension ClassLoader):用Java实现的,继承自ClassLoader,负责加载<JAVA_HOME>\lib\ext下的扩展类库。

3,应用程序类加载器(Application ClassLoader):也用Java实现的,继承自ClassLoader,负责加载用户类路径(ClassPath)上所指定的类库,也是默认的类加载器。

4,自定义类加载器(Custom ClassLoader):用户可以根据需要自定义的类加载器,只要继承自ClassLoader或其子类即可。

这四种类型的类加载器之间存在父子关系,除了启动类加载器外,其他类加载器都有一个父类加载器。这些父子关系通常不是以继承(inheritance)的方式实现,而是以组合(composition)的方式委托给父类加载器完成。

双亲委派机制

是Java中类加载过程采用的机制,其主要作用是避免类的重复加载和防止Java核心API被随意替换。

原理

当一个类加载器收到类加载请求时,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

双亲委派机制的工作流程

应用程序类加载器收到一个类加载请求时,它首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器扩展类加载器去完成。

扩展类加载器收到一个类加载请求时,它首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器启动类加载器去完成。

如果启动类加载器可以完成类加载任务,就成功返回;如果启动类加载器无法完成此加载任务,会抛出ClassNotFoundException异常,并通知子类扩展类加载器尝试自己去完成。

如果扩展类加载器也无法完成此加载任务,也会抛出ClassNotFoundException异常,并通知子类应用程序类加载器尝试自己去完成。

如果应用程序类加载器也无法完成此加载任务,就会抛出ClassNotFoundException异常,并通知用户没有找到该类。

Java程序对类加载器的操作

获取类加载器的实例

1,通过Class类的getClassLoader()方法,获取当前类或指定类的类加载器。

// 获取当前类的类加载器
ClassLoader classLoader1 = this.getClass().getClassLoader();

// 获取String类的类加载器
ClassLoader classLoader2 = String.class.getClassLoader();

2,通过ClassLoader类的getSystemClassLoader()方法,获取系统默认的类加载器。

ClassLoader classLoader = ClassLoader.getSystemClassLoader();

3,通过Thread类的getContextClassLoader()方法,获取当前线程的上下文类加载器。

// 获取当前线程的上下文类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
加载类

Java程序可以通过类加载器的loadClass()方法,显式地加载指定名称的类。

// 获取系统默认的类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// 加载指定名称的类
Class<?> c = classLoader.loadClass("java.lang.String");
自定义类加载器

通过继承ClassLoader类或其子类,自定义自己的类加载器,实现特定的加载逻辑和功能。

通过重写findClass()方法,实现从自定义的来源(如数据库、网络等)加载类文件。

通过重写loadClass()方法,实现修改双亲委派模型(Parent Delegation Model)的行为。

栈——虚拟机栈(VM Stack)

JVM的栈是指存放每个线程运行时所需数据的内存区域,用于存储局部变量和方法调用信息。JVM的栈是线程私有的资源,每个线程都有自己独立的栈空间。栈中的数据随着方法的进入和退出而创建和销毁。栈的大小是固定的,由JVM在运行时确定。如果栈空间不足,会抛出StackOverflowError错误。

栈内部

栈帧(Stack Frame)

栈帧是JVM的栈中的基本单位,它表示一个方法调用时所需的数据和信息。每当一个方法被调用时,就会创建一个新的栈帧并压入JVM的栈,每当一个方法执行完毕时,就会弹出当前的栈帧并销毁。栈帧包括以下几个部分:

        局部变量表(Local Variable Table):局部变量表用于存放方法的参数和局部变量,它是一个数组结构,每个元素可以存放一个基本类型值(如int、long等)或一个对象引用。局部变量表的大小在编译时确定,不会在运行时改变。

        操作数栈(Operand Stack):操作数栈用于存放方法执行过程中的中间结果,它是一个后进先出(LIFO)的栈结构,每个元素可以存放一个基本类型值或一个对象引用。操作数栈的最大深度在编译时确定,不会在运行时改变。

        动态链接(Dynamic Linking):动态链接用于支持运行时多态,它是一个指向方法区中当前类的运行时常量池(Runtime Constant Pool)的引用。运行时常量池中存放了类、字段、方法等符号引用,通过动态链接可以将这些符号引用转换为直接引用。

        方法出口(Method Exit):方法出口用于记录方法返回后的执行位置,它包括返回值、返回地址等信息。返回值表示方法执行后返回给调用者的结果,返回地址表示方法执行后继续执行的字节码指令位置。

本地方法栈(Native Method Stack)

JVM本地方法栈(Native Method Stack)是存放每个线程运行时所需数据的内存区域,它用于存放本地方法(Native Method)调用时产生的栈帧(Stack Frame)。

本地方法是指使用其他语言(如C或C++)编写的方法,它们不受JVM管理,而是通过本地方法接口(JNI)与JVM进行交互。本地方法栈与虚拟机栈(VM Stack)类似,也遵循后进先出(LIFO)的原则,也可能抛出StackOverflowError或OutOfMemoryError异常。但是本地方法栈在Java虚拟机规范中没有明确规定其具体实现方式和参数设置方式,因此不同的JVM实现可能有不同的处理方式。

本地方法调用框架(Native Method Invocation Frame)

本地方法栈内部也是栈帧,而本地方法调用框架是一种特殊的栈帧,它用于存放本地方法调用时所需的数据。本地方法调用框架包括以下几个部分:

JNI环境指针:JNI环境指针是一个指向JNI环境结构体(JNI Environment Structure)的指针,该结构体提供了一系列函数接口,供Java代码和本地代码进行交互。

参数区域:参数区域用于存放传递给本地方法的参数,包括JNI环境指针和其他Java参数。

返回地址:返回地址用于记录本地方法返回后的执行位置,它指向调用者的方法出口中的返回地址。

程序计数器(Program Counter Register)

JVM程序计数器(Program Counter Register)是存放每个线程当前执行的字节码指令地址的内存区域,它是线程私有的资源,每个线程都有自己独立的程序计数器。

JVM程序计数器的作用

是为了保证线程切换后能够恢复到正确的执行位置。因为CPU在多线程环境下,会不断地在各个线程之间切换,每次切换回来后,就需要知道接着从哪里开始继续执行。JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。

JVM程序计数器的主要功能有

记录字节码指令流的执行位置:JVM程序计数器用于记录每个线程当前正在执行的字节码指令的地址,以便于下一条指令的获取和执行。如果当前线程正在执行的是Java方法,那么程序计数器记录的是虚拟机字节码指令的地址;如果当前线程正在执行的是本地方法(Native Method),那么程序计数器记录的是Undefined(未定义)。

支持线程切换和恢复:JVM程序计数器用于支持多线程的切换和恢复,当一个线程被挂起时,它的程序计数器会保存当前的执行位置,当该线程被恢复时,它可以根据程序计数器继续执行下一条指令。

支持异常处理和跳转:JVM程序计数器用于支持异常处理和跳转,当一个方法在执行过程中发生异常时,它可以根据异常处理器表(Exception Handler Table)中的信息,将程序计数器设置为异常处理代码的入口地址,从而实现异常处理逻辑;当一个方法需要进行条件或无条件跳转时,它可以根据跳转目标地址,将程序计数器设置为目标指令的地址,从而实现跳转逻辑。

注:JVM程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的内存区域,它可以看作是虚拟机实现的细节,不同的JVM实现可能有不同的处理方式。

方法区(Method Area)/元空间

JVM方法区是一种用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等的内存区域,它是各个线程共享的。JVM方法区在JVM启动时创建,其大小可以固定,也可以扩展。JVM方法区的内存不必是连续的。

JVM方法区的作用:是为了支持类的加载、解析、验证、初始化等过程,以及存储运行时常量池和类元数据等信息。JVM方法区也是垃圾回收的对象之一,如果方法区中没有足够的空间来存储新的类或常量,JVM会抛出OutOfMemoryError错误。

注:不同的虚拟机实现对方法区的规范和管理有所差异。在HotSpot虚拟机中,方法区有两种实现方式:永久代和元空间。永久代是在JDK 7及以前使用的一种基于堆的实现方式,它有一个固定的最大容量,如果超过这个容量,就会触发Full GC来回收无用的类或常量。元空间是在JDK 8及以后使用的一种基于本地内存的实现方式,它没有固定的最大容量,但受限于操作系统的可用内存。

方法区存什么

JVM方法区存放以下内容:

类型信息:包括类的全限定名、父类、接口、修饰符、字段、方法等元数据。

常量池:包括字面常量(如字符串、整数、浮点数等)和符号引用(如对其他类型、字段、方法的引用)。

静态变量:即类变量,它们被类的所有实例共享,也可以在没有实例对象时访问。

代码缓存:即即时编译器编译后的本地机器指令。
 

堆(Heap)

JVM堆是用来存储对象和数组的运行时数据区域,它是被所有线程共享的。JVM堆在JVM启动时创建,其大小可以固定,也可以扩大和缩小。JVM堆的内存不需要是连续的。

年轻代(Young Generation)

年轻代是存放新创建的对象和数组的区域,它分为三个子区域:Eden空间、From Survivor空间和To Survivor空间。

Eden空间是对象和数组的主要分配区域,当Eden空间满了,就会触发一次Minor GC(轻量级内存回收器),把存活的对象和数组复制到From Survivor空间或To Survivor空间。

From Survivor空间和To Survivor空间是用来保存经过一次或多次Minor GC后仍然存活的对象和数组的区域,它们之间会互换角色,即每次Minor GC后,From Survivor空间变成To Survivor空间,To Survivor空间变成From Survivor空间。

年轻代的大小可以通过参数-Xmn来设置,一般建议设置为整个堆内存的1/3到1/4。

堆的新生代会有两个幸存者区的原因:

是为了避免内存碎片化和提高复制效率。新生代中的对象一般存活时间较短,所以采用复制算法来回收内存。复制算法的基本思想是将内存分为两块或多块,每次只使用其中一块,当这一块内存用完时,就将存活的对象复制到另一块上面,并清空原来的内存。如果只有一个幸存者区,那么每次复制时,Eden区和幸存者区中的存活对象都要被复制到幸存者区中,这样会导致两个问题:

        一是内存碎片化,因为Eden区和幸存者区中的对象可能不是连续的,复制到幸存者区后也会不连续,这样就会造成空间的浪费和分配的困难。

        二是复制效率低,因为每次都要复制Eden区和幸存者区中的所有对象,而实际上很多对象可能在下一次垃圾回收时就会被清除掉。

因此,为了解决这些问题,新生代设置了两个幸存者区,分别称为from区和to区。(步骤1)刚刚新建的对象在Eden区,当Eden区满了时,触发一次Minor GC,Eden区中的存活对象就会被移动到from区,Eden区被清空;(步骤二)等Eden区再满了时,就再触发一次Minor GC,Eden区和from区中的存活对象就会被复制到to区,并且给每个对象增加一个年龄计数器。这样做有以下好处:

        一是避免了内存碎片化,因为每次复制时都是把整个Eden区和from区中的对象连续地复制到to区中,保证了空闲空间是连续的。

        二是提高了复制效率,因为每次只复制一半的内存空间,并且根据对象的年龄判断是否要复制到老年代中,减少了不必要的复制。

每次复制后,from区和to区会交换角色,保证每次都有一个空闲的幸存者区。如果一个对象经过多次(默认是15次)Minor GC仍然存活在幸存者区中,那么它就会被移动到老年代中。这样就保证了只有经过多次筛选仍然存活的对象才会进入老年代

老年代(Old Generation)

JVM的老年代中存放的对象是年龄大于15的对象,也就是经过多次垃圾回收仍然存活的对象。当老年代满了,就会触发一次Major GCFull GC,对整个堆内存进行回收。老年代的大小可以通过参数-Xmx来设置,一般建议设置为整个堆内存的2/3到3/4。

永久代(Permanent Generation)

永久代是存放类元信息、常量池、方法区等数据的区域,它不属于Java堆内存的一部分,但也会受到垃圾回收器的管理。永久代在Java 8中被移除了,取而代之的是元空间(Meta Space),它位于本地内存中,不受JVM参数的限制。永久代或元空间的大小可以通过参数-XX:MaxPermSize或-XX:MaxMetaspaceSize来设置。

GC

JVM的GC是指Java虚拟机的垃圾回收机制,它主要是对堆内存和方法区的回收。GC 的原理是通过追踪和标记对象的引用状态,来判断哪些对象是存活的,哪些对象是死亡的,然后回收死亡对象的内存。它的目的是为了释放不再使用的对象占用的内存空间,从而提高程序的性能和稳定性。

GC的算法

GC的算法是指垃圾回收器在回收内存中不再使用的对象时所遵循的规则。常见的GC算法有以下四种:

        引用计数算法:给每个对象设置一个引用计数器,当有地方引用这个对象时,计数器加一,当引用失效时,计数器减一,当计数器为零时,表示该对象可以被回收。这种算法的优点是简单高效,缺点是无法处理循环引用的情况,而且需要维护计数器的开销。

        标记清除算法:分为标记和清除两个阶段,先从根对象开始遍历所有可达对象,并标记它们,然后清除所有未被标记的对象。这种算法的优点是不需要额外的空间,缺点是会产生内存碎片,而且需要暂停整个程序。

       标记整理算法:在标记清除算法的基础上,增加了一个整理的阶段,将所有存活的对象移动到一端,使得空闲空间连续。这种算法的优点是避免了内存碎片,缺点是需要移动对象的成本,而且也需要暂停整个程序。

        复制算法:将内存分为两块或多块,每次只使用其中一块,当这一块内存用完时,就将存活的对象复制到另一块上面,并清空原来的内存。这种算法的优点是不会产生内存碎片,而且复制的速度快,缺点是浪费了一半或更多的内存空间。

       一般来说,不同的GC算法适用于不同的场景。例如,在新生代中,由于对象的存活率较低,可以使用复制算法来提高效率而在老年代中,由于对象的存活率较高,可以使用标记清除或标记整理算法来节省空间。因此,现代的垃圾回收器通常采用分代收集算法,根据对象的生命周期将内存划分为几块,并使用最合适的算法进行回收。

JMM

JMM是Java内存模型(Java Memory Model)的简称,它是一种抽象的概念,用来描述多线程之间Java程序中各个变量的访问方式和规则。

主内存和工作内存:JMM将内存划分为主内存(Main Memory)和工作内存(Working Memory)。主内存是所有线程共享的,用于存储Java对象的实例数据。工作内存是每个线程私有的,用于存储主内存中的部分副本,以及线程执行时的临时变量。

JMM主要围绕着三个特性来建立:原子性,可见性和有序性

原子性是指一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。JMM只能保证基本类型的读取和赋值操作是原子性的,如果要保证一个代码块的原子性,可以使用synchronized或者Lock等同步机制。

可见性是指当一个线程修改了一个共享变量的值,其他线程能够立即看到修改后的值。JMM提供了volatile关键字来保证可见性,当一个变量被volatile修饰后,它会立即刷新到主内存中,其他线程读取这个变量时,会直接从主内存中读取最新的值。除了volatile之外,final和synchronized也能实现可见性。

有序性是指程序执行的顺序按照代码的先后顺序执行。由于编译器和处理器为了优化程序性能而对指令序列进行重排序,可能会导致多线程环境下程序执行结果不一致。JMM提供了volatile和synchronized关键字来保证一定的有序性,volatile可以防止指令重排,synchronized可以保证同一时刻只有一个线程执行同步代码块。

JMM还定义了8种内存交互操作来完成数据的读写操作

lock(锁定),unlock(解锁),read(读取),load(加载),use(使用),assign(赋值),store(存储),write(写入)。这些操作必须满足一些同步规则,比如不允许read、load、store、write操作单独出现,不允许线程丢弃最近的assign操作,不允许线程将没有assign的数据从工作内存同步到主内存等等。

lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。

read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。

load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。

use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。

store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。

write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM对8种内存交互操作制定的规则

不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。

不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。

不允许线程将没有assign的数据从工作内存同步到主内存。

一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。

一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。

如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。

如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。

一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值