Java知识复习(五)JVM虚拟机

1、虚拟机的自动内存管理和C/C++的区别

C/C++开发程序时需要为每一个new操作去写对应的delete/free操作,不容易出现内存泄漏和溢出问题。而Java程序将内存控制权交给了Java虚拟机

2、JVM的运行机制

1、Java程序的具体运行过程如下:

  • 编译:Java程序首先需要通过Java编译器将源代码(.java文件)编译成字节码(.class文件)。
  • 装载:Java虚拟机(JVM)会将字节码文件加载到内存中,并将其转化为机器码。
  • 链接:在链接阶段,Java虚拟机会对字节码进行验证、准备和解析等操作。其中,验证操作主要是检查字节码文件是否符合Java语言规范和安全性要求;准备操作主要是为类的静态变量分配内存,并赋予默认值;解析操作主要是将常量池中的符号引用转化为直接引用。
  • 初始化:在初始化阶段,Java虚拟机会对类的静态变量进行初始化,执行静态代码块中的代码等操作。
  • 执行:在执行阶段,Java虚拟机会执行程序的主方法,并按照程序的逻辑进行操作,包括调用方法、创建对象、赋值等操作。在程序执行完毕后,Java虚拟机会自动回收内存并退出程序。

2、虚拟机结构

Java虚拟机包括类加载器子系统、运行时数据区、执行引擎、本地接口库等
在这里插入图片描述

  • 类加载器(ClassLoader):负责将Java类的字节码文件加载到JVM中,通常分为三类:引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。
  • 运行时数据区(Runtime Data Area):包括方法区、堆、虚拟机栈、本地方法栈和程序计数器等几个主要部分。其中,方法区用于存储类的信息和静态变量,堆用于存储对象实例和数组,虚拟机栈和本地方法栈用于存储方法的局部变量和参数,程序计数器用于记录当前线程执行的位置。
  • 执行引擎(Execution Engine):负责将字节码文件转换为机器码,并执行程序的指令。通常包括解释器和即时编译器两种执行方式。
  • 本地方法接口(Native Interface):Java程序可以调用本地方法,也可以通过本地方法接口调用C、C++等语言编写的程序。
  • 安全性管理器(Security Manager):控制Java程序对本地资源的访问权限,保证程序的安全性。
  • 垃圾收集器(Garbage Collector):自动回收不再使用的内存空间,释放系统资源,保证程序的稳定性和性能。

3、JVM的内存区域

1、内存区域划分: 线程私有区域、线程共享区域和直接内存
在这里插入图片描述

  • 线程私有区域的生命周期与线程相同
  • 线程共享区域的生命周期与虚拟机相同
  • 直接内存也叫堆外内存,它不是JVM运行时数据区的一部分,但在并发编程中被频繁调用

2、程序计数器: 线程私有,无内存溢出问题

  • 每个运行中的线程都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的值为空
  • 程序计数器属于“线程私有”的内存区域,它是唯一没有Out Of Memory(内存溢出)的区域

3、虚拟机栈: 线程私有,描述Java方法的执行过程

  • 虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧中存储了局部变量表、操作数栈、动态链接、方法出口等信息。
  • 栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈和出栈
    在这里插入图片描述

4、本地方法区: 线程私有

  • 本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务

5、堆: 也叫作运行时内存,线程共享

  • 在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域
  • 由于现代JVM采用分代收集算法,因此Java堆从GC(Garbage Collection,垃圾回收)的角度还可以细分为:新生代、老年代和永久代

6、方法区: 线程共享

  • 方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据
  • 永久代的内存回收主要针对常量池的回收和类的卸载,因此可回收的对象很少
  • 在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在方法区的运行时常量池中
    在这里插入图片描述

4、JVM的运行时内存

1、JVM的运行时内存也叫作JVM堆:可分类为如下图所示在这里插入图片描述
2、新生代: Eden区、ServivorTo区和ServivorFrom区

  • JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收
  • Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代
  • ServivorTo区:保留上一次MinorGC时的幸存者
  • ServivorFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者
  • 新生代的GC过程叫作MinorGC,采用复制算法实现

3、老年代

  • 老年代主要存放有长生命周期的对象和大对象。老年代的GC过程叫作MajorGC,采用标记清除算法
  • 在老年代,对象比较稳定,MajorGC不会被频繁触发。在进行MajorGC前,JVM会进行一次MinorGC,在MinorGC过后仍然出现老年代空间不足或无法找到足够大的连续空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM的内存空间

4、永久代

  • 永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。
  • Class在类加载时被放入永久代。永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出Out Of Memory异常
  • 需要注意的是,在Java 8中永久代已经被元数据区(也叫作元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此,元空间的大小不受JVM内存的限制,只和操作系统的内存有关。
  • 而Full GC是对整个堆来说的;当永久代的空间不足时就会触发一次Full GC

5、确定垃圾

1、如何确定垃圾: Java采用引用计数法和可达性分析来确定对象是否应该被回收在这里插入图片描述
2、引用计数法

  • 在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引进计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收
  • 引用计数法容易产生循环引用问题。循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收

3、可达性分析

  • 具体做法是首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果在GC roots和一个对象之间没有可达路径,则称该对象是不可达的
  • 不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然是不可达的,则将被垃圾收集器回收。

6、垃圾回收算法

  • 4种算法:标记清除、复制、标记整理、分代收集
    在这里插入图片描述

1、标记清除算法

  • 标记清除算法是基础的垃圾回收算法,其过程分为标记和清除两个阶段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间
  • 由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用空间的问题在这里插入图片描述

2、复制算法

  • 复制算法是为了解决标记清除算法内存碎片化的问题而设计的。
  • 复制算法首先将内存划分为两块大小相等的内存区域,即区域1和区域2,新生成的对象都被存放在区域1中,在区域1内的对象存储满后会对区域1进行一次标记,并将标记后仍然存活的对象全部复制到区域2中,这时区域1将不存在任何存活的对象,直接清理整个区域1的内存即可
  • 复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量的内存浪费
    在这里插入图片描述

3、标记整理算法

  • 标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存
    在这里插入图片描述

4、分代收集算法

  • 无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法
  • 分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。因此,JVM根据不同的区域对象的特点选择了不同的算法
  • 大部分JVM在新生代都采用了复制算法,JVM将新生代进一步划分为一块较大的Eden区和两块较小的Servivor区,Servivor区又分为ServivorFrom区和ServivorTo区。JVM在运行过程中主要使用Eden区和ServivorFrom区,进行垃圾回收时会将在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,然后清理Eden区和ServivorFrom区的内存空间
  • 老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法
  • 在JVM中还有一个区域,即方法区的永久代,永久代用来存储Class类、常量、方法描述等。在永久代主要回收废弃的常量和无用的类
  • JVM内存中的对象主要被分配到新生代的Eden区和ServivorFrom区,在少数情况下会被直接分配到老年代。
  • 在新生代的Eden区和ServivorFrom区的内存空间不足时会触发一次GC,该过程被称为MinorGC。 在MinorGC后,在Eden区和ServivorFrom区中存活的对象会被复制到ServivorTo区,然后Eden区和ServivorFrom区被清理。如果此时在ServivorTo区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代。若Servivor区的对象经过一次GC后仍然存活,则其年龄加1。在默认情况下,对象在年龄达到15时,将被移到老年代

7、Java中的4中引用类型

  1. 强引用:在Java中最常见的就是强引用。在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用;有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成Java内存泄漏(Memory Link)的主要原因
  2. 软引用:软引用通过SoftReference类实现。如果一个对象只有软引用,则在系统内存空间不足时该对象将被回收
  3. 弱引用:弱引用通过WeakReference类实现,如果一个对象只有弱引用,那么无论内存是否充足,在垃圾回收过程中一定会被回收
  4. 虚引用:虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。虚引用通过PhantomReference类实现,虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态

8、分代收集算法和分区收集算法

  1. 分代收集算法:JVM根据对象存活周期的不同将内存划分为新生代、老年代和永久代,并根据各年代的特点分别采用不同的GC算法
  2. 分区收集算法:分区算法将整个堆空间划分为连续的大小不同的小区域,对每个小区域都单独进行内存使用和垃圾回收,这样做的好处是可以根据每个小区域内存的大小灵活使用和释放内存
  3. 分区收集算法可以根据系统可接受的停顿时间,每次都快速回收若干个小区域的内存,以缩短垃圾回收时系统停顿的时间,最后以多次并行累加的方式逐步完成整个内存区域的垃圾回收

9、垃圾收集器

  • JVM针对新生代和老年代分别提供了多种不同的垃圾收集器
  • 针对新生代提供的垃圾收集器有Serial、ParNew、Parallel Scavenge
  • 针对老年代提供的垃圾收集器有Serial Old、Parallel Old、CMS
  • 还有针对不同区域的G1分区收集算法

1、Serial垃圾收集器: 单线程,复制算法

  • Serial垃圾收集器基于复制算法实现,它是一个单线程收集器,在它正在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束
  • 对于单CPU运行环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器是Java虚拟机运行在Client模式下的新生代的默认垃圾收集器。

2、ParNew垃圾收集器: 多线程,复制算法

  • ParNew垃圾收集器是Serial垃圾收集器的多线程实现,同样采用了复制算法,它采用多线程模式工作,除此之外和Serial收集器几乎一样
  • ParNew垃圾收集器在垃圾收集过程中会暂停所有其他工作线程,是Java虚拟机运行在Server模式下的新生代的默认垃圾收集器

3、Parallel Scavenge垃圾收集器: 多线程,复制算法

  • Parallel Scavenge收集器是为提高新生代垃圾收集效率而设计的垃圾收集器,基于多线程复制算法实现,在系统吞吐量上有很大的优化,可以更高效地利用CPU尽快完成垃圾回收任务

4、Serial Old垃圾收集器: 单线程,标记整理算法

  • Serial Old垃圾收集器是Serial垃圾收集器的老年代实现,同Serial一样采用单线程执行,不同的是,Serial Old针对老年代长生命周期的特点基于标记整理算法实现
  • Serial Old垃圾收集器是JVM运行在Client模式下的老年代的默认垃圾收集器。

5、Parallel Old垃圾收集器: 多线程,标记整理算法

  • Parallel Old垃圾收集器采用多线程并发进行垃圾回收,它根据老年代长生命周期的特点,基于多线程的标记整理算法实现

6、CMS垃圾收集器

  • CMS(Concurrent Mark Sweep)垃圾收集器是为老年代设计的垃圾收集器
  • 其主要目的是达到最短的垃圾回收停顿时间,基于线程的标记清除算法实现,以便在多线程并发环境下以最短的垃圾收集停顿时间提高系统的稳定性

7、G1垃圾收集器

  • G1(Garbage First)垃圾收集器为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域
  • 相对于CMS收集器,G1垃圾收集器两个突出的改进:基于标记整理算法,不产生内存碎片;可以精确地控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收

10、网络编程模型

1、阻塞I/O模型

  • 阻塞I/O模型的工作流程为:在用户线程发出I/O请求之后,内核会检查数据是否就绪,此时用户线程一直阻塞等待内存数据就绪;在内存数据就绪后,内核将数据复制到用户线程中,并返回I/O执行结果到用户线程,此时用户线程将解除阻塞状态并开始处理数据
  • 典型的阻塞I/O模型的例子为data = socket.read(),如果内核数据没有就绪,Socket线程就会一直阻塞在read()中等待内核数据就绪

2、非阻塞I/O模型

  • 非阻塞I/O模型指用户线程在发起一个I/O操作后,无须阻塞便可以马上得到内核返回的一个结果
  • 如果内核返回的结果为false,则表示内核数据还没准备好,需要稍后再发起I/O操作。一旦内核中的数据准备好了,并且再次收到用户线程的请求,内核就会立刻将数据复制到用户线程中并将复制的结果通知用户线程

3、多路复用I/O模型

  • 在多路复用I/O模型中会有一个被称为Selector的线程不断轮询多个Socket的状态,只有在Socket有读写事件时,才会通知用户线程进行I/O读写操作。
  • 多路复用I/O模型是多线程并发编程用得较多的模型,Java NIO就是基于多路复用I/O模型实现的
  • 在实际应用中,在多路复用方法体内一般不建议做复杂逻辑运算,只做数据的接收和转发,将具体的业务操作转发给后面的业务线程处理

4、信号驱动I/O模型

  • 在信号驱动I/O模型中,在用户线程发起一个I/O请求操作时,系统会为该请求对应的Socket注册一个信号函数,然后用户线程可以继续执行其他业务逻辑;
  • 在内核数据就绪时,系统会发送一个信号到用户线程,用户线程在接收到该信号后,会在信号函数中调用对应的I/O读写操作完成实际的I/O请求操作

5、异步I/O模型AIO

  • 在异步I/O模型中,用户线程会发起一个asynchronous read操作到内核,内核在接收到synchronous read请求后会立刻返回一个状态,来说明请求是否成功发起,在此过程中用户线程不会发生任何阻塞
  • 接着,内核会等待数据准备完成并将数据复制到用户线程中,在数据复制完成后内核会发送一个信号到用户线程,通知用户线程asynchronous读操作已完成

6、Java NIO

  • Java NIO(New I/O)是Java SE 1.4引入的一组新的I/O API,提供了更快速、更高效、更灵活的I/O操作方式。相比于传统的I/O操作方式,Java NIO具有以下优点:
  • 缓冲区(Buffer):NIO中所有的I/O操作都是通过缓冲区来进行的,可以提高I/O操作的效率和速度。
  • 通道(Channel):NIO中的通道是一种新的I/O抽象,可以支持双向数据传输,可以在缓冲区和文件之间进行快速、高效的数据传输。
  • 非阻塞I/O操作:NIO中可以使用非阻塞I/O操作,可以实现单线程处理多个通道的数据读写操作,提高了通道的并发性能。
  • 选择器(Selector):NIO中的选择器是一种高效的多路复用技术,可以使用一个线程管理多个通道,实现高效的I/O操作。

11、JVM的类加载机制

1、JVM的类加载阶段: 加载、验证、准备、解析、初始化。

  • 加载阶段:将类的二进制数据加载到内存中,并在方法区中生成一个代表该类的Class对象。类的二进制数据可以来自于本地文件系统、网络等各种途径。
  • 验证阶段:对类的二进制数据进行合法性验证,包括文件格式验证、元数据验证、字节码验证和符号引用验证等。验证阶段的目的是确保类的二进制数据符合Java虚拟机规范,并且不会对JVM造成安全威胁。
  • 准备阶段:为类的静态变量分配内存,并初始化为默认值(0、null等)。这个阶段并不会对静态变量进行初始化,而是为它们分配内存空间。
  • 解析阶段:将类的符号引用转化为直接引用。符号引用是一组符号来描述目标,可以是类、接口、方法、字段等,而直接引用则是直接指向目标的指针、相对偏移量或者句柄等。解析阶段的目的是将符号引用转化为直接引用,以便于JVM在程序运行时能够准确地找到目标。
  • 初始化阶段:为类的静态变量赋值,并执行静态代码块中的代码。在这个阶段,JVM会根据程序中对类的使用情况来执行相应的初始化操作,例如创建类的实例、调用类的静态方法等。
  • 一句话总结:虚拟机把Class文件加载到内存,并对数据进行校验、转换解析和初始化,形成虚拟机可以直接使用的java类型,即java.lang.Class

2、类加载器: 启动类加载器、扩展类加载器和系统类加载器和自定义类加载器

  • 启动类加载器:负责加载Java_HOME/lib目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库
  • 扩展类加载器:负责加载Java_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库
  • 系统类(应用程序类)加载器:负责加载用户路径(classpath)中指定jar包和Djava.class.path所指定目录下的类和jar包
  • 自定义类加载器:通过Java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader

3、双亲委派机制

  • JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中
    在这里插入图片描述

4、OSGI

  • OSGI(Open Service Gateway Initiative)是Java动态化模块化系统的一系列规范,旨在为实现Java程序的模块化编程提供基础条件
  • 基于OSGI的程序可以实现模块级的热插拔功能,在程序升级更新时,可以只针对需要更新的程序进行停用和重新安装,极大提高了系统升级的安全性和便捷性

12、int i=1,它们的存储位置分别在哪?

  • 成员变量 int a = 1, a作为变量名,在JVM中是以代码的形式存在,存放在方法区,当有线程执行到该代码的时候,会加载该代码进行执行,而1作为参数 a 的值在运行时存放在堆内存中,a指向该内存
  • int a=1为局部变量的时候,这个时候a同样存在方法区的代码中,运行时a存在于该方法对应的栈帧的局部变量表中,而该变量表中a的值为1,所以1存在栈内存中。

13、栈和堆分别存放什么?

  • 栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量)
  • 堆内存存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象)
  • 堆和栈的联系:例如int [] arr=new int [3];首先给堆分配一个地址,然后把堆的地址赋给arr,arr就通过地址指向了数组。所以arr想操纵数组时,就通过地址,而不是直接把实体都赋给它。这种我们不称为基本数据类型,而叫引用数据类型。称为arr引用了堆内存当中的实体。数组也是一种对象。

14、String创建几个对象

  • String s = “abc” 在创建这个对象时,会在常量池中看有没有abc这个字符串;如果没有 此时还会在常量池中创建一个;如果有 则不创建。而s是变量,是引用而不是新的对象。
  • String s = “a” +“b”+“c” 在字符串常量相加时,编译器编译时产生的字节码,会把如 “a” +“b”+"c"优化为“abc”。所以只会产生一个对象
  • String s = new String(“abc”); 在创建这个对象时因为使用了new关键字,所以肯定会在堆中创建一个对象。然后会在常量池中看有没有abc这个字符串;如果没有 此时还会在常量池中创建一个;如果有 则不创建。所以一共会产生两个对象。
  • String s = “abc”;String s1= s + “ab”; 在变量与常量进行相加时,‘+’可以理解:为利用加号进行拼接字符串
1.String s1 = "a" +"b"+"c";   //1个对象
2. String s2 = "a";		    //1个对象
  String s3= s2 +"bc";   	//3个对象
3.String s4 = new String("你") + s2 + "b";  	  //5个对象
-----------------下面代码与上面代码没有联系-------------------------------
1.String s6 = "a";
2.String s7= "b" +"c" + s6;  	 //3个对象
3.String s8= s6 + "d" +"f" ;	 //4个对象
4.String s9= "ab" + "cd" + s6 + "b" +"c"   //5个对象

参考

  1. 书籍:《Offer来了》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值