JVM入门这一篇就够了

1、JVM体系结构

在这里插入图片描述

2、JVM存在的位置

JVM 存在于操作系统上的 JRE 构建环境中
在这里插入图片描述

3、类加载过程

类加载器
在这里插入图片描述

  • 类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载进JVM中,同一个类就不会被再次载入了,是唯一的标识。
  • 在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用==其全限定类名和其类加载器==作为其唯一标识。

JVM中有三种类加载器

系统(应用)类加载器----->扩展类加载器----->根类加载器

1、根类加载器(bootstrap class loader)(rt.jar):它用来加载 Java 的核心类,由C++实现,不是ClassLoader子类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
**2、扩展类加载器(extensions class loader):**它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

3、系统类加载器(system class loader):被称为系统(也称为应用)类加载器,程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

双亲委派机制

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
  4. 即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:

  1. Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  2. 安全因素,java核心api中定义类型不会被随意替换

类加载过程

在这里插入图片描述

1.加载

  • **加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,**也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

  • 类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器

2.链接

  • 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责**把类的二进制数据合并到JRE中**。类连接又可分为如下3个阶段。

1)验证验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。

  • 其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

2)准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

3)解析:类的二进制数据中的符号引用替换成直接引用。

  • 说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

3.初始化

  • 初始化是为类的静态变量赋予正确的初始值
    • 如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

4、native、方法区

native:

  • native是与C++联合开发的时候用的!java自己开发不用!凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库。会进入本地方法栈,通过JNI调用本地方法
  • **native方法不能与abstract方法一起使用,**因为native表示这些方法是有实现体的,但是abstract却表示这些方法是没有实现体的,那么两者矛盾,肯定也不能一起使用

JNI作用:扩展java的使用,融合不同的编程语言为Java所用

  • 在最早java诞生的时候,c/c++横行,java想要立足,必须要调用c/c++的程序,所以它在内存区域中专门开辟了一块标记区域:Native Method Stack(本地方法栈),登记native方法
  • 在最终执行的时候,通过JNI加载本地方法库中的方法

方法区:

  • 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间
  • 静态变量、常量、类信息(构造方法,接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
  • java 虚拟机规范把方法区描述为堆的一个逻辑部分,方法区也被称为永久代

5、栈和堆

在这里插入图片描述

一、栈内存

存放基本类型的变量、对象的引用(对象实例的地址)和方法调用,遵循先入后出的原则。

栈内存在函数中定义的“一些基本类型的变量和对象的引用变量”都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

二、堆内存

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆

在这里插入图片描述

GC垃圾回收主要在Eden区域和老生代区

JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。

堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

  1. java.lang.OutOfMemoryError: GC Overhead Limit Exceeded : 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值
    • 可以通过参数-Xms、-Xmx来调整-Xms1024m -Xmx1024m -XX:+PrintGCDetails

新生区

  • ==新生区是类的诞生、成长、消亡的区域,==一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。

  • 新生区又分为伊甸区(Eden)和幸存者区,幸存者区又分为from区和to区

  • 所有的类都是在伊甸区被new出来的

  1. 当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收**(轻GC)**,将伊甸园区中的==不再被其他对象所引用的对象进行销毁。==
  2. 然后将伊甸园中的剩余对象复制到幸存to区。from中的对象年龄+1,(在同一个时间点上,from和to只能有一个区有数据,另外一个是空的。
  3. from区中还能存活的对象会有两个去处。
    • 若对象年龄达到之前设置好的年龄阈值(默认年龄为15岁,可以自行设置参数XX:+MaxTenuringThreshold),此时对象会被复制到老年区
    • 如果没有达到阈值,则会和Eden区一起被复制到to区,然后from和to交换角色
  4. 轻GC一直会重复这个操作,直到需要放到的幸存to区也满了,==然后对伊甸园区和幸存者区进行清理,==再复制到老年区。

永久代

永久代和方法区的关系

  • 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。
  1. 永久存储区(jdk1.7之前)是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区的数据是不会被垃圾回收器回收掉的,关闭了JVM才会释放此区所占用的内存。

  2. 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

当元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace

  1. 元空间里面**存放的是类的元数据,**这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

运行时常量池

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

  1. JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
  2. JDK1.7 字符串常量池被从方法区拿到了堆中, 运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代
  3. JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

堆内存和栈内存的区别:

1、应用程序所有的部分都使用堆内存,然后栈内存通过一个线程运行来使用。

2、**不论对象什么时候创建,他都会存储在堆内存中,栈内存包含它的引用。**栈内存只包含原始值变量好和堆中对象变量的引用。

3、存储在堆中的对象是全局可以被访问的,然而栈内存不能被其他线程所访问。

4、栈中的内存管理使用LIFO的方式完成,而堆内存的管理要更复杂了,因为它是全局被访问的。

5、栈内存是生命周期很短的,然而堆内存的生命周期从程序的运行开始到运行结束。

6、栈内存与堆内存相比是非常小的,存取速度比堆要快,仅次于寄存器,栈数据可以共享

6、GC回收算法

判断对象存活状态

  1. 引用计数法
    • 在对象头处维护一个引用计数,每增加一次对该对象的引用,计数器自加,如果对该对象的引用失联,则计数器自减。当引用计数为0时,表明该对象已经被废弃,不处于存活状态。
      • 优点:判定效率高;
      • 缺点:不完全准确,当两个对象相互引用的时候就无法回收,导致内存泄漏。
  2. 可达性分析算法
    • 通过一系列为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的。
    • 如果对象在进行可行性分析后发现没有与GC Roots相连的引用链,也不会立刻死亡,它会暂时被标记上并且进行一次筛选,筛选的条件是是否与必要执行finalize()方法。如果被判定有必要执行finaliza()方法,就会进入F-Queue队列中,并有一个虚拟机自动建立的、低优先级的线程去执行它。稍后GC将对F-Queue中的对象进行第二次小规模标记。如果这时还是没有新的关联出现,那基本上就真的被回收了。

复制算法

  • 复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另外一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾回收。

  • **优点:**在垃圾对象多的情况下,效率较高。清理后,内存无碎片

  • 缺点:浪费了内存空间,多了一半永远是空的内存

标记清除算法

  1. 首先标记出所有存活的对象,
  2. 将没有标记的对象全部清除掉
  • 优点:不需要额外的空间
  • 缺点:两次扫描,浪费时间,还会产生内存碎

标记压缩算法

防止内存碎片产生,再次扫描,向一端移动存活的对象,多了一个移动成本

总结:

内存效率:复制算法 > 标记清除算法 > 标记压缩算法

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:标记清除算法 = 标记压缩算法 > 复制算法

猜你喜欢

转载自blog.csdn.net/l1341886243/article/details/121588540