Java程序员必备的JVM学习

2022-10-01 08:45:18

前言

JVM的学习毋庸置疑是非常重要的,在面试中也是非常常见 本次的内容比较多,如果您能耐心看完,我想一定能对你有帮助

JVM概述

为什么要学习jvm

JVM是中高级程序员必备技能,可以对项目进行管理,对性能进行调优

虚拟机

所谓虚拟机,就是一台虚拟机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。

大名鼎鼎的VMware就属于虚拟机,它是完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。程序虚拟机典型的代表就是java虚拟机了,它专门为执行某个单个计算机程序而设计。在java虚拟机中执行的指令我们称为java字节码指令。

java虚拟机是一种执行java字节码文件的虚拟计算机,它拥有独立的 运行机制。

java技术的核心就是java虚拟机,因为所有的java程序都运行在java虚拟机内部。

JVM作用

JVM作用

Java 虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,每一条 java 指令,java 虚拟机中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪儿。

特点

一次编译到处运行

自动内存管理

自动垃圾回收功能

现在的JVM不仅可以执行java字节码文件,还可以执行其他语言编译后的字节码文件,是一个跨语言平台。

跨语言平台

JVM位置

JVM位置

JVM 是运行在操作系统之上的,它与硬件没有直接的交互

关系图

JVM整体组成可分为以下四个部分

1、类加载器(ClassLoader)

2、运行时数据区(Runtime Data Area)

3、执行引擎(Execution Engine)

4、本地库接口(Native Interface)

JVM四部分

详细图

详细图

各组成部分用途

程序在执行之前先要把java代码转换成字节码(class文件),JVM首先需要把字节码通过一定的方式类加载器(ClassLoader)把文件加载到内存中的运行时数据区(Runtime Data Area),而字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行。因此需要特定的命令解析器**执行引擎(Execution Engine)将字节码翻译成底层操作系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口本地库接口(Native Interface)来实现整个程序的功能,这就是这4个组成部分的职责与功能。

而我们通常所说的JVM组成指的是运行时数据区(Runtime Data Area),因为通常需要程序员吊事分析的区域就是”运行时数据区“,或者更具体的来说就是”运行时数据区“里面的Heap(堆)模块。

java代码的执行流程

java代码的执行流程1

java代码执行流程2

java编译器编译过程中,任何一个结点执行失败就会造成编译失败。虽然各个平台的java虚拟机内部实现细节不尽相同,但是它们执行的字节码内容确实一样的。

JVM主要任务就是负责将字节码装载到其内部,解释/编译为对应平台上的机器指令执行。JVM使用类加载器(ClassLoader)装载class文件。

类加载完成后,会进行字节码校验,字节码校验通过之后JVM解释器会把字节码翻译成机器码交由操作系统执行。

但不是所有的代码都是解释执行,JVM对此作了优化,比如HotSpot虚拟机,它本身提供了JIT(Just In Time)编译器。

JVM架构模型

java编译器输入的指令流基本上是一种基于栈的指令集架构,另一种指令集架构是基于寄存器的指令集架构

这两种架构之间的区别:

基于栈式架构的特点

设计和实现更简单,适用于资源受限的系统。

使用零地址指令方式分配,其执行过程依赖于操作栈,指令集更小,编译器容易实现。

基于寄存器式架构特点:

指令完全依赖于硬件,可移植性差。

性能优秀,执行更高效。

完成一项操作使用的指令更少。

jvm架构

使用 javap -v class 文件可以将 class 文件反编译为指令集。

所以由于跨平台的设计,Java 指令集都是根据栈来设计的,不同 CPU 架构不同,

所以不能设计为基于寄存器的.

优点是跨平台,指令集小,编译器容易实现.

缺点是性能下降,实现同样功能需要更多的指令

JVM类加载

类加载子系统

类加载子系统

类加载器子系统负责从文件系统或者网络中加载 class 文件。 classLoader 只

负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。

加载的类信息存放于一块称为方法区(元空间)的内存空间。

类加载的角色

类加载的角色

  1. class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板 在执行的时候是要加载 JVM 当中来,根据这个模板实例化出 n 个一模一样的实例.
  2. class file 加载到 JVM 中,被称为 DNA 元数据模板,放在方法区中.
  3. 在.class–>JVM–>最终称为元数据模板,此过程就要有一个运输工具(类加 载器 Class Loader),扮演一个快递员的角色.

类加载过程

类加载过程

加载

根据类的地址,从硬盘上读取类的信息,

将信息读入到方法区,生成Class类的对象

链接

验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致;

对文件格式的验证:class 文件在文件开头有特定的文件标识(字节

码文件都以 CA FE BA BE 标识开头);主,次版本号是否在当前 java 虚拟机接收

范围内。

对元数据的验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java 语言规范的要求,例如这个类是否有父类;是否继承浏览不允许被继承的类 (final 修饰的类)…

准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值

不包含用 final 修饰的 static 常量,在编译时进行初始化

例如: public static int value = 123;

value 在准备阶段后的初始值是 0,而不是 123。

解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是 Class 文

件的逻辑符号,直接引用指向的方法区中某一个地址)。

初始化

类什么时候初始化?

1 )创建类的实例,也就是 new一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4)反射(Class.forName(“”))

5)初始化一个类的子类(会首先初始化子类的父类)

类的初始化顺序

对 static 修饰的变量或语句块进行赋值.

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

顺序是:父类 static –> 子类 static –> 父类构造方法- -> 子类构造方法

publicclassClassInit{static{
        num=20;}staticint num=10;publicstaticvoidmain(String[] args){//num从准备到初始化值变化过程  num=0 --> num=20 --> num=10System.out.println(num);//10}}
publicclassClassInit{staticint num=10;static{
        num=20;}publicstaticvoidmain(String[] args){//num从准备到初始化值变化过程  num=0 --> num=10 --> num=20System.out.println(num);//20}}

类加载器分类

站在 JVM 的角度看,类加载器可以分为两种:

  1. 引导类加载器(启动类加载器 Bootstrap ClassLoader).(java语言写的)
  2. 其他所有类加载器,这些类加载器由 java 语言实现,独立存在于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader。(其他语言写的)

站在 java 开发人员的角度来看,类加载器就应当划分得更细致一些。自 JDK1.2 以来 java 一直保持者三层类加载器。

类加载器分类

启动类加载器(引导类加载器)

这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库

负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器.

出于安全考虑,引用类加载器只加载存放在<JAVA_HOME>\lib 目录,或者被-Xbootclasspath 参数锁指定的路径中存储放的类

扩展类加载器

是由java语言实现的 继承自ClassLoader.

从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 系统安装目录的

jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的 jar 放在此目录下,也 会自动由扩展类加载器加载.

应用程序类加载器(系统类加载器)

Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现.

派生于 ClassLoader 类.

负责加载用户类

在这里插入图片描述

用户自定义类加载器

例如Tomcat

双亲委派机制

Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会 将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委 派模式.

双亲委派机制

工作原理

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请 求委托给父类的加载器去执行.

  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器

  3. 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完 成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制.

    如果均加载失败,就会抛出 ClassNotFoundException 异常。

优点

1 安全,可避免用户自己编写的类动态替换 Java 的核心类,如 java.lang.String

2 避免全限定命名的类重复加载(使用了 findLoadClass()判断当前类是否已加 载)

类的主动使用/被动使用

JVM 规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自 然就有被动使用

主动使用

1.通过new关键字被导致类的初始化,这是大家经常使用的初始化一个类的方式,他肯定会导致类的加载并且初始化

2.访问类的静态变量,包括读取和更新

3.访问类的静态方法

4.对某个类进行反射操作,会导致类的初始化

5.初始化子类会导致父类的的初始化

6.执行该类的 main 函数

被动使用

1.引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量 是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导 致初始化,比如:

publicfinalstaticint NUMBER=5;//不会导致类初始化,被动使用publicfinalstaticint RANDOM=newRandom().nextInt();//会导致类的初 始化,主动使用

2.构造某个类的数组时不会导致该类的初始化,比如:

Student[] students=newStudent[10];

主动使用和被动使用的区别在于类是否会被初始化

JVM运行时数据区

运行时数据区组成概述

JVM 的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚

拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几

个运行时数据区域

1、程序计数器(Program Counter Register)

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看 作是当前线程所执行的字节码的行号指示器。

2、Java 虚拟机栈(Java Virtual Machine Stacks)

描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。

3、本地方法栈(Native Method Stack)

与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地 方法栈是为虚拟机调用 Native 方法服务的。

4、Java 堆(Java Heap)

是 Java 虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配 内存.

5、方法区(Methed Area)

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

方法区是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应用程序的实时运行.

以HotSpot 虚拟机为例
HotSpot 虚拟机

Java 虚拟机定义了序运行期间会使用到的运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁.另外一些则是与线程一一对应的.这些与 线程对应的区域会随着线程开始和结束而创建销毁.

如图:红色的为多个线程共享,灰色的为单个线程私有的,即线程间共享:堆,方法区.

线程私有:程序计数器,栈,本地方法栈

运行时数据区

程序计数器(Program Counter Register)

jvm中的程序计数器不是cpu中的寄存器, 可以理解为计数器.

是一块非常小的内存空间,运行速度是最快的,不会出现内存溢出情况.

作用:

是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域.

任何时间一个线程都只有一个方法在执行,程序计数器记录当前线程中的方法执行的位置. 以便于cpu在切换执行时,记录程序执行的为位置.

在运行时数据区中唯一一个不会出现内存溢出的区域.

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的 字节码指令.

Java 虚拟机栈(Java Virtual Machine Stacks)

背景

由于跨平台性的设计,Java 的指令都是根据栈来设计的.不同平台CPU 架构不同,所以不能设计为基于寄存器的。

基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实 现同样功能需要更过的指令集

分清栈和堆

栈是运行时的单位,而堆是存储的单位

即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据.

解决的是数据存储的问题,即数据怎么放,放在哪儿

Java 虚拟机栈是什么

Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈.每个线程在创建 时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用.

Java 虚拟机栈是线程私有的.

生命周期和线程一致.

作用

主管 Java 程序的运行,它保存方法的局部变量(8 种基本数据类型,对象的引用地

址),部分结果,并参与方法的调用和返回

举例

虚拟机栈

栈的特点

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

JVM 直接对 java 栈的操作只有两个:1、调用方法,进栈 2、执行结束后出栈

对于栈来说不存在垃圾回收问题.

栈操作

栈中出现的异常

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

栈中存储什么?

每个线程都有自己的栈,栈中的数据都以栈帧为单位存储.

在这个线程上正在执行的每个方法都各自对应一个栈帧.

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息.

栈的运行原理

  • JVM 直接对 java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出/后进先出的原则
  • 在一条活动的线程中,一个时间点上,只会有一个活动栈.即只有当前在执行的方 法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈 帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前 类(Current Class).
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作.
  • 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧.

栈的方法调用

不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中 引用另一个线程的栈帧(方法).

如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧.Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是 抛出异常.不管哪种方式,都会导致栈帧被弹出.

栈帧的内部结构

局部变量表、操作数栈、动态链接、方法返回地址

局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局 部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。

操作数栈(Operand Stack)(或表达式栈)

栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)

因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)

当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保 存一个方法返回地址。

栈帧的内部结构

本地方法栈

  • Java 虚拟机栈管理 java 方法的调用,而本地方法栈用于管理本地方法的调用.

  • 本地方法栈也是线程私有的.

  • 允许被实现成固定或者是可动态扩展的内存大小.内存溢出方面也是相同的.

    如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出

    StackOverflowError.

  • 本地方法是用 C 语言写的.

  • 它的具体做法是在 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库.

Java 堆内存

概述

堆

  • 一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域

  • Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理的最大一块内存空间.

  • 堆内存的大小是可以调节.

    例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)

    一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率.

  • 《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的.

  • 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.

  • 《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分配在堆上.

  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.

  • 堆是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.

堆内存区域划分

Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)

新生区分为 Eden(伊甸园)区和 Survivor(幸存者)区

堆内存区域划分

为什么分区(代)

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫 描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

对象创建内存分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片

  1. new 的新对象先放到伊甸园区,此区大小有限制.

  2. 当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.

  3. 然后将伊甸园区中的剩余对象移动到幸存者 0 区.

  4. 如果再次出发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.

  5. 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区

  6. 什么时候去养老区呢?默认是 15 次,也可以设置参数,最大值为 15 -XX:MaxTenuringThreshold=

    在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111,即为 15。所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。

  7. 在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清 理.

  8. 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space

新生区与老年区配置比例

配置新生代与老年代在堆结构的占比(一般不会调)

  1. 默认-XX:NewRatio=2,表示新生代占 1,老年代占 2,新生代占整 个堆的 1/3
  2. 可以修改-XX:NewRatio=4,表示新生代占 1,老年代占 4,新生代 占整个堆的 1/5
  3. 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年 代的大小,来进行调优

各区比例

在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。比 如-XX:SurvivorRatio=8

新生区的对象默认生命周期超过 15 ,就会去养老区养老

堆中对象生命周期

分代收集思想 Minor GC、Major GC、Full GC

JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的 都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。

部分收集:是不完整收集整个java 堆的垃圾收集。其中又分为:

新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集。

老年区收集(Major GC / Old GC):只是老年区的垃圾收集。

整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。

整堆收集出席县的情况:

System.gc();时

老年区空间不足

方法区空间不足

开发期间尽量避免整堆收集.

TLAB 机制

为什么有 TLAB(Thread Local Allocation Buffer)

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据.

由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存 空间是线程不安全的.

为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度.

什么是 TLAB?

TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区, 这是一个线程专用的内存分配区域。

如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块 指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如 果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

JVM 使用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自 己的 TLAB,这样可以避免线程同步,提高了对象分配的效率。

TLAB 空间的内存非常小,缺省情况下仅占有整个 Eden 空间的 1%,也可以 通过选项-XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的 百分比大小。

TABL机制

堆空间的参数设置

官网地址:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+PrintFlagsInitial 查看所有参数的默认初始值

-XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)

-Xms:初始堆空间内存(默认为物理内存的 1/64)

-Xmx:最大堆空间内存(默认为物理内存的 1/4)

-Xmn:设置新生代的大小(初始值及最大值)

-XX:NewRatio:配置新生代与老年代在堆结构的占比

-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例

-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄

-XX:+PrintGCDetails 输出详细的 GC 处理日志

字符串常量池

jdk7之前,将字符串常量池位置在 方法区(永久代)中存储. jdk8之后 方法区又称为元空间

jdk8之后,将字符串常量池的位置 放到了堆空间. 因为方法区只有触发FUll GC时才会回收.

因为程序中大量的需要使用字符串,所以将字符串常量池的位置改变到了堆中,可以及时回收无效的字符串常量.

方法区

概述

方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、 class/method/field 等元数据、static final 常量、static 变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。

Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。

所以,方法区看做是一块独立于 java 堆的内存空间.

方法区

方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的.

方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展.

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误

关闭 JVM 就会释放这个区域的内存.

方法区,栈,堆的交互关系

方法区,栈,堆的交互关系

方法区大小设置

Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.

  • 元数据区大小可以使用参数-XX:MetaspaceSize 和 -XX:MaxMataspaceSize 指定,替代上述原有的两个参数
  • 默认值依赖于平台,windows 下,-XXMetaspaceSize 是 21MB,
  • -XX:MaxMetaspaceSize 的值是-1,级没有限制.
  • 这个-XX:MetaspaceSize 初始值是 21M 也称为高水位线 一旦触及 就 会触发 Full GC.
  • 因此为了减少 FullGC 那么这个-XX:MetaspaceSize 可以设置一个较高 的值

方法区的内部结构

方法区的内部结构

方法区它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,运行常量池等。

通过反编译字节码文件查看.

反编译字节码文件,并输出值文本文件中,便于查看。参数 -p 确保能查看 private 权限类型的字段或方法

javap-v-pDemo.class> test.txt

方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。

下面也称作类卸载

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被 使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。

2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。

3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

本地方法接口

什么是本地方法

一个 Native Method 就是一个 java 调用非 java 代码的接口

非java语言实现的,例如 C C++

在定义一个 native method 时,并不提供实现体(有些像定义一个 Java interface),因为其实现体是由非 java 语言在外面实现的。

关键字 native 可以与其他所有的 java 标识符连用,但是 abstract 除外。

为什么要使用 Native Method

Java 使用起来非常方便,然而有些层次的任务用 java 实现起来不容易(与硬件交互),或者我们对程序的效率很在意时,问题就来了。

1.与 java 环境外交互:

有时 java 应用需要与 java 外面的环境交互,这是本地方法存在的主要原因。 你可以想想 java 需要与一些底层系统,如某些硬件交换信息时的情况。本地方法 正式这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 java 应用之外的繁琐细节。

2.Sun’s Java

Sun 的解释器是用 C 实现的,这使得它能像一些普通的 C 一样与外部交互。jre 大部分是用 java 实现的,它也通过一些本地方法与外界交互。例如:类 java.lang.Thread 的 setPriority()方法是用 Java 实现的,但是它实现调用的事该类里的本地方法 setPriority0()。

执行引擎

概述

1.执行引擎是 Java 虚拟机核心的组成部分之一。

2.JVM 的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被 JVM 所识别的字节码指令、符号表,以及其他辅助信息。

3.那么,如果想要让一个 Java 程序运行起来,执行引擎(Execution Engine) 的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。

注意区分概念:

1.前端编译:从 Java 程序员-字节码文件的这个过程叫前端编译.

2.执行引擎这里有两种行为:一种是解释执行,一种是编译执行(这里的是后端编译)。

什么是解释器?什么是JIT编译器

解释器:当 Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方 式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

JIT(Just In Time Compiler)编译器:就是虚拟机将源代码一次性直接编译成和本地机器平台相关的机器语言,但并不是马上执行。

为什么Java是半编译半解释型语言?

起初将 Java 语言定位为“解释执行”还是比较准确的。再后来,Java 也发展出 可以直接生成本地代码的编译器。现在 JVM 在执行 Java 代码的时候,通常都会 将解释执行与编译执行二者结合起来进行。

原因:

JVM 设计者们的初衷仅仅只是单纯地为了满足 Java 程序实现跨平台特性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。

解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的 内容“翻译”为对应平台的本地机器指令执行,执行效率低。

JIT 编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区 的 JIT 代码缓存中(执行效率更高了)。

是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。

JIT 编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其 直接编译为对应平台的本地机器指令,以此提升 Java 程序的执行性能。

一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以 被称之为“热点代码”。

目前 HotSpot VM 所采用的热点探测方式是基于计数器的热点探测。

JIT 编译器执行效率高为什么还需要解释器?

1.当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行。

2.编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取一个平衡点。

垃圾回收

垃圾回收概述

概述

1.Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语

言没有垃圾收集技术,需要程序员手动的收集。

2.垃圾收集,不是 Java 语言的伴生产物。早在 1960 年,第一门开始使用内存动态分配和垃圾收集技术的 Lisp 语言诞生。

3.关于垃圾收集有三个经典问题:

哪些内存需要回收?

频繁回收内存,较少回收方法区 栈,本地方法栈,程序计数器没有垃圾回收

什么时候回收?

如何回收?

4.垃圾收集机制是 Java 的招牌能力,极大地提高了开发效率。如今,垃圾收集 几乎成为现代语言的标配,即使经过如此长时间的发展,Java 的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战。

什么是垃圾

垃圾是指在运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾。

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出

为什么需要GC?

想要学习 GC,首先需要理解为什么需要 GC?

1.对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。

2.除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理 将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。

3.随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有 GC

就不能保证应用程序的正常进行

早期垃圾回收

在早期的 C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用 new关键字进行内存申请,并使用 delete 关键字进行内存释放。比如以下代码:

MibBridge*pBridge=newcmBaseGroupBridge()//如果注册失败,使用 Delete 释放该对象所占内存区域if(pBridge->Register(kDestroy)=NO ERROR)
    	delete pBridge

这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。

有了垃圾回收机制后,上述代码极有可能变成这样

MibBridge*pBridge=newcmBaseGroupBridge(); 
pBridge->Register(kDestroy)

现在,除了 Java 以外,C#、Python、Ruby 等语言都使用了自动垃圾回收的思想,也是未来发展趋势,可以说这种自动化的内存分配和来及回收方式已经成为了现代开发语言必备的标准。

Java 垃圾回收机制

自动内存管理

优点

自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险

自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发.

担忧

1.对于 Java 开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于 “自动”,那么这将会是一场灾难,最严重的就会弱化 Java 开发人员在程序出现内存溢出时定位问题和解决问题的能力。

2.此时,了解 JVM 的自动内存分配和内存回收原理就显得非常重要,只有在真正了解 JVM 是如何管理内存后,我们才能够在遇见 OutofMemoryError 时,快速地根据错误异常日志定位问题和解决问题。

3.当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节

应该关心哪些区域的回收?

垃圾回收区域

垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收,其中,Java 堆是垃圾收集器的工作重点

从次数上讲:

频繁收集 Young 区

较少收集 Old 区

基本不收集元空间(方法区)

内存溢出与内存泄漏

溢出: 内存不够用了

泄漏: 有些对象已经在程序不被使用了,但是垃圾回收机制并不能判定其为垃圾对象,不能将其回收掉,

这样的对象越积越多, 长久也会导致内存不够用.

例如: 与数据库连接完之后,需要关闭连接通道,但是没有关闭.

io 读写完成后没有关闭

垃圾回收相关算法

垃圾标记阶段算法

标记阶段目的

垃圾标记阶段:主要是为了判断对象是否存活

1、在堆里几乎存放着几乎所有的java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段

2.那么在 JVM 中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。

3.判断对象存活一般有两种方式:引用计数算法和可达性分析算法

引用计数算法

  1. 引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
  2. 对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1; 当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,即表示对象 A 不可能再被使用,可进行回收。
  3. 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  4. 缺点:

1.它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。

2.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。

3.引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在.Java 的垃圾回收器中没有使用这类算法

引用计数法

可达性分析算法

可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集。

1.相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。

2.相较于引用计数算法,这里的可达性分析就是 Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)

可达性分析实现思路

所谓"GCRoots根集合就是一组必须活跃的引用

其基本思路如下:

  1. 可达性分析算法是以根对象(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference C
  • 作者:金清泽
  • 原文链接:https://blog.csdn.net/m0_53752829/article/details/122886360
    更新时间:2022-10-01 08:45:18