JVM笔记

概述

  JVM是一个虚构出来的计算机,可运行Java代码。JVM是运行在操作系统之上的,它与硬件没有直接的交互。

运行过程

  Java源文件===>编译器===>字节码文件===>JVM===>机器码

体系结构

  Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口垃圾收集模块

 1.类加载器子系统

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称连接。类加载工作由ClassLoader及其子类负责。如下图:

               Java类生命周期

(1)加载

加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:

  • 通过“类全名”来获取定义此类的二进制字节流。
  • 将类.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,
  • 然后在堆中创建一个java.lang.Class对象,用来封装类在方法区的数据结构。

  加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由 虚拟机实现 自行定义,虚拟机并未规定此区域的具体数据结构。然后在Java堆中实例化一个Java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。
   JVM设计者把类加载阶段中的通过类全名来获取此类的二进制字节流这个动作放到Java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类。实现这个动作的代码模块成为“类加载器”。类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它

  类加载器可以大致划分为以下三类

  • 启动类加载器:Bootstrap ClassLoader,它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的Java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由Java.ext.dirs系统变量指定的路径中的所有类库(如Javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的Java class文件。

           类加载器的双亲委派模型

过程:如果一个类加载器收到了类请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一层都是如此,因此所有类加载的请求都会传到启动类加载器,只有当父加载器无法完成该请求时,子加载器才去自己加载。

好处:双亲委派的好处 : 主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,由于每个类加载都会经过最顶层的启动类加载器,比如 java.lang.Object这样的类在各个类加载器下都是同一个类(只有当两个类是由同一个类加载器加载的才有意义,这两个类才相等。)

(2)验证

  验证是连接阶段的第一步,这一步主要的目的是为了保证从加载阶段获取的字节流中包含的信息是符合虚拟机要求,并且对虚拟机来说是安全的。这个阶段主要完成4个方面的校验工作,包括文件格式验证、元数据验证、字节码验证和符号引用验证。

  • 文件格式验证主要是验证字节流是否符合Class文件格式规范,以及字节流能否被虚拟机处理,例如字节流文件是否以魔数0xCAFEBABE开头、主次版本号是否是虚拟机能处理的版本等。
  • 元数据验证主要是对字节码描述的信息进行语义分析,用来确保字节码描述信息符合Java语言规范,例如类的继承关系、类是否继承了不允许被继承的类、类实现了接口是否实现了接口中的所有方法等。
  • 字节码验证主要是通过数据流和控制流来分析和确定程序的语义是否合法、符合逻辑,例如验证操作数栈中数据的存取是否会出现类型不匹配、跳转指令是否会跳转到方法体以外的字节码指令上等。
  • 符号引用验证主要是确保解析动作能够正常执行,主要对常量池中的符号引用进行匹配性校验,例如符号引用中的类、属性和方法的访问性是否可以被当前类访问、能否通过字符串描述的全限定名来找到对应的类等。

(3) 准备

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

这个阶段中有两个容易产生混淆的知识点

  • 首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
  • 其次是这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。假设一个类变量定义为:  public static int value = 1024;那么变量value在准备阶段过后的初始值为0而不是1024,因为这时候尚未开始执行任何Java方法,而把value赋值为1024的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为1024的动作将在初始化阶段才会被执行。

注:如果是常量的话(被final修饰),就会在准备阶段变量value就会被初始化为ConstantValue属性所指定的值1024

(4) 解析

  解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。

  • 符号引用:符号引用是一组符号来描述所引用的对象,符号可以是任何形式的字面量,只要使用时能定位到目标即可,符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
  • 直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄(一种特殊的智能指针)。直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中。

  解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在.运行时常量池中记录直接引用,并把常标示为已解析状态),从而避免解析动作重复进行。
  

 (5)初始化

  初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码(初始化成为代码设定的默认值)。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源

 2.Native Interface 本地接口

         本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体作法是Native Method Stack中登记native方法,在Execution Engine执行时加载Native Method Libraies。
3. Execution Engine 执行引擎
  执行包在装载类的方法中的指令,也就是方法。 
 
4. Runtime Data Area 运行时数据区
      虚拟机内存或者JVM内存,从整个计算机内存中开辟一块内存存储JVM需要用到的对象,变量等,运行区数据有分很多小区,分别为:方法区,虚拟机栈,本地方法栈,堆,程序计数器

(1)Native Method Stack 本地方法栈

         跟虚拟机栈很像,不过它是为虚拟机使用到的Native方法服务。在Execution Engine执行时加载native libraies本地方法库。

(2)PC Register 程序计数器

         每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
  由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。

(3)Method Area 方法区

         方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
         静态变量+常量+类信息+运行时常量池存在方法区中,实例变量存在堆内存中。

(4)Stack 栈

     ① 栈是什么
         栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。
          基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
     ② 栈存储什么?
     栈帧中主要保存3类数据:
          本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
          栈操作(Operand Stack):记录出栈、入栈的操作;
          栈帧数据(Frame Data):包括类文件、方法等等。
     ③ 栈运行原理
     栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进......F3栈帧,再弹出F2栈帧,再弹出F1栈帧。
     遵循“先进后出”/“后进先出”原则。

5)Heap 堆

     堆这块区域是JVM中最大的,应用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 gc 主要的回收区,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分

为什么要分代勒,因为针对每个年龄代,都有不同的垃圾回收算法,以及内存分配机制。如果将所有对象放在一起,第一是会造成频繁遍历判断回收的开销,第二是会造成复制、移动的开销,为什么会有复制、移动,因为回收内存必然会造成内存碎片,而内存碎片会导致空间浪费,所以必须通过复制、移动来清理随便,使得空闲内存连续。

① 新生代
       新生代是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生代又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。
  幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。【每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。】当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。如果1区的内存不足以存储剩余的对象,再移动到老年代。若老年代也满了,那么这个时候将产生FullGCC,进行新生代和老年代的内存清理。若执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
【HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代 容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被 “浪费”的。】
注:
  • 从年轻代 (包括 Eden 和 Survivor 区域) 回收内存被称为 Minor GC;从老年代 GC 称为 Major GC;同时对新生代、老年代和永久代进行垃圾回收叫做 Full GC;
  • Survivor 的存在意义,就是减少被送到老年代的对象,进而减少 Full GC 的发生。
  • 年轻代 gc 使用 “停止 - 复制” 算法,停止指的是,发生 GC 的时候会暂停除了 GC 线程以外的所有线程的运行。所以年轻代频繁 gc 会极大影响系统吞吐量
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
      a.Java虚拟机的堆内存设置不够,可以通过参数-Xms设置初始堆内存的大小)、-XmxXmx设置最大堆内存的大小)来调整。
      b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
 
② 老年代
        老年代用于保存从新生代筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。对象在年轻代存活超过一定时间(可以设置)没有被回收掉,就会被复制到老年代。老年代的空间比新生代大,发生的 GC 次数也比新生代少。但是 gc 时间大约是新生代的 10 倍。新生代空间太小会导致对象直接进入 old 区 ,old 区满了就会触发 full gc。
注:
  • 老年代使用 “标记 - 整理” 算法,即将存活的对象向一边移动,以此来保证回收后,内存依然是连续的,不会出现内存碎片。
  • 每次年轻代的 Eden 发生 Minor GC 时,虚拟机都会检查每次晋级老年代的大小是否大于老年代的剩余大小,如果大于则会触发 FULL GC
③ 永久代
         永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
 
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。原因有二:
       a. 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
       b. 大量动态反射生成的类不断被加载,最终导致Perm区被占满。
 

 

 说明:

   HotSpot虚拟机在1.8之后已经取消了永久代,改为元空间,类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。 

  这项改造也是有必要的,永久代的调优是很困难的,虽然可以设置永久代的大小,但是很难确定一个合适的大小,因为其中的影响因素很多,比如类数量的多少、常量数量的多少等。永久代中的元数据的位置也会随着一次full GC发生移动,比较消耗虚拟机性能。同时,HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。

 



GC是什么?为什么要有GC?

答:GC是垃圾收集的意思(Garbage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。

补充

垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域.

posted @   danielzzz  阅读(51)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示