笔记整理——深入理解JVM原理

Posted 茀园日记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了笔记整理——深入理解JVM原理相关的知识,希望对你有一定的参考价值。

1. 什么是JVM?

    JVM是Java Virtual Machine(Java虚拟机)的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是Java能够“一次编译,到处运行的”原因。

2. JRE,JDK,JVM之间的关系

JRE(Java Runtime Environment, Java运行环境)Java平台,所有的程序都要在JRE下才能够运行。包括JVM和Java核心类库和支持文件。

JDK(Java Development Kit,Java开发工具包)是用来编译、调试Java程序的开发工具包。包括Java工具(javac/java/jdb等)和Java基础的类库(java API )。

JVM(Java Virtual Machine, Java虚拟机)JRE的一部分。JVM主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。Java语言是跨平台运行的,不同的操作系统会有不同的JVM映射规则,使之与操作系统无关,完成跨平台性。

下图表示了JDK、JRE和JVM三者间的关系:

1. JVM原理

Java 体系结构:

 

Java实例对应一个独立运行的Java程序(进程级别

1.启动。启动一个Java程序,一个JVM实例就产生。拥有public static void main(String[] args)函数的class可以作为JVM实例运行的起点。

2.运行main()作为程序初始线程的起点,任何其他线程均可由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM使用,程序可以指定创建的线程为守护线程。

3.消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。

JVM执行引擎实例则对应了属于用户运行程序线程它是线程级别的。

Java类加载器:

Java加载类的过程:

笔记整理——深入理解JVM原理

    1.装载(loading):负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名、ClassLoader完成类的加载。因此,标识一个被加载了的类:类名 + 包名 + ClassLoader实例ID。

    2.链接(linking):负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口。

完成校验后,JVM初始化类中的静态变量,并将其赋值为默认值。

最后对比类中的所有属性、方法进行验证,以确保要调用的属性、方法存在,以及具备访问权限(例如private、public等),否则会造成NoSuchMethodError、NoSuchFieldError等错误信息。

    3.初始化(initializing):负责执行类中的静态初始化代码、构造器代码以及静态属性的初始化,以下四种情况初始化过程会被触发。

调用 new

反射调用了类中的方法

子类调用了初始化

JVM启动过程终止定的初始化类

JVM类加载顺序:

层级结构

笔记整理——深入理解JVM原理

1.Booststrap ClassLoader

    跟ClassLoader,C++实现,JVM启动时初始化此ClassLoader,并由此完成$JAVA_HONE中jre/lib/rt.jar(Sun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。

2.Extension ClassLoader

    JVM用此classloader来加载扩展功能的一些jar包

3.System ClassLoader

    JVM用此ClassLoader来加载启动参数中指定的ClassPath中的jar包以及目录,在Sun JDK中ClassLoader对应的类名为AppClassLoader。

4.User-Defined ClassLoader

    User-Defined ClassLoader是Java开发人员继承ClassLoader抽象类实现的ClassLoader,基于自定义的ClassLoader可用于加载非ClassPath中的jar以及目录。

委派模式(Delegation Mode)


笔记整理——深入理解JVM原理

JVM加载一个类的时候,下层的加载器会将任务给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个类,如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。检查之后,按照相反的顺序进行加载。如果Bootstrap加载器不到这个类,则往下委托,直到找到这个类。一个类可以被不同的类加载器加载。

    可见性限制:下层的加载器能够看到上层加载器中的类,反之则不行,委派只能从下到上。

      不允许卸载类:类加载器可以加载一个类,但不能够卸载一个类。但是类加载器可以被创建或者删除。

JVM执行引擎

      类加载器将字节码载入内存后,执行引擎以java字节码为单元,读取java字节码。java字节码机器读不懂,必须将字节码转化为平台相关的机器码。这个过程就是由执行引擎完成的。

笔记整理——深入理解JVM原理


在执行方法时JVM提供了四种指令来执行:

  • invokestatic:调用类的static方法。

  • invokevirtual:调用对象实例的方法。

  • invokeinterface:将属性定义为接口来进行调用。

  • invokespecial:JVM对于初始化对象(Java构造器的方法为:)以及调用对象实例的私有方法时。

  • 主要的执行计数:

  • 解释,即时执行,自适应优化、芯片级直接执行。

  • 解释属于第一代JVM

  • 即时编译JIT属于第二代JVM


    自适应优化(目前sun的HotspotJVM采用这种技术),吸取第一代JVM和第二代JVM的经验,采用两者结合的方式,开始对所有的代码都采用解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行优化。若方法不再频繁使用,则取消编译过代码,仍对其进行解释执行。

Java运行时数据区

    我们都知道Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都各司其职,以及创建和销毁的时间,在《Java虚拟机规范(Java SE 8版)》的规定中,Java虚拟机所管理的内存包括五大部分区域:分别是程序计数器、虚拟机栈、本地方法栈、堆和方法区。

笔记整理——深入理解JVM原理

我们把程序代码抽象一下,可以理解为由三个部分组成,分别是数据、指令、控制流,所谓数据,可以理解为定义的成员变量,静态变量,常量;指令理解为在方法中执行的语句,控制流理解为分支、循环、跳转、异常处理、线程恢复等。我们在编写代码的过程中,可以理解为都是围绕着这三部分来展开组织代码的。

程序计数器

 

Java虚拟机栈

    Java虚拟机栈也是线程独立的,多个线程有独立的Java虚拟机栈,栈的生命周期与线程相同,在线程启动时被创建,线程结束时被销毁,栈是用来存储Java方法运行时数据的,那栈中存储的数据是什么方式来组织的呢?其实在栈中存储的数据结构是一个数据单位来体现的,这个数据单位称为栈帧(Stack Frame),当程序执行一个方法时,会创建一个栈帧,我们称为入栈,当方法执行结束后,栈帧就会被销毁,我们称为出栈。在一个栈帧里,用于存储局部变量表、操作数栈、动态链接、方法出口和一些额外的附加信息。

    也许你会跟我一样,栈帧里的这些东东是什么鬼?不要捉急,下面我们将详细介绍运行时栈帧的结构。

 

 

    前面我们说过,栈帧是虚拟机在方法调用执行时存储在虚拟机栈的数据结构,也可以称为栈元素,一个方法对应一个栈帧,一个栈帧概念结构如下图所示。

笔记整理——深入理解JVM原理

 

    局部变量表可以理解为是一组变量值的存储空间,目的是为了存放方法参数和方法内部定义的局部变量。当程序被编译成Class文件时,该方法会有一个Code属性的max_locals数据项来确定该方法所需要分配的局部变量表的最大容量,所以,栈帧中需要多大的局部变量表,在编译后就已经确定了,并且在程序运行期变量表的容量不用改变,所以我们在基础入门时讲的,栈中存储的数据是确定大小的,就是这个原因了。

 

    操作数栈是一个先入后出,同局部变量表一样,操作数栈的最大深度也在编译的时候写到方法的Code属性的max_stacks数据项中,操作数栈可以理解为正在操作中需要处理的数据和结果,看个例子哈,很简单的两数相加操作,加法的字节码指令是iadd,在运行的时候操作数栈中最接近栈顶的两个元素例如已经存入了两个int型的数值,执行iadd指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。

 

    动态连接,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,有这个引用是为了支持方法调用过程中的动态连接,因为Class文件的常量池有很多符号引用,这些符号有一部分将在每一次运行期转化为直接引用,称为动态连接,还有一部分是在类加载或第一次使用时转化为直接引用,称为静态解析。说白了,动态连接,就是通过符号的方式来引用常量池。

 

本地方法栈

    本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机机行 Java方法(字节码)服务,而本地方法栈则为虚拟机便用到的Native方法服务。

在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(Sun Hotspot)直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和 OutOfMemoryError异常。

 

Java 

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

这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。但是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。后面详细介绍 Java堆内存结构。

 

方法区

    方法区(Method Area)与Java堆一样,是各有个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范把方法区描述为堆的一个逻辑部分,但方法区有一个别名叫做非堆(Non-Heap),目的是与 Java堆区分开来。

 

Java程序中堆和堆栈内存结构示例

笔记整理——深入理解JVM原理

引用官方的解释如下:

As soon as we run the program, it loads all the Runtime classes into the Heap space. When main() method is found at line 1, Java Runtime creates stack memory to be used by main() method thread.

We are creating primitive local variable at line 2, so it’s created and stored in the stack memory of main() method.

Since we are creating an Object in line 3, it’s created in Heap memory and stack memory contains the reference for it. Similar process occurs when we create Memory object in line 4.

Now when we call foo() method in line 5, a block in the top of the stack is created to be used by foo() method. Since Java is pass by value, a new reference to Object is created in the foo() stack block in line 6.

A string is created in line 7, it goes in the String Pool in the heap space and a reference is created in the foo() stack space for it.

foo() method is terminated in line 8, at this time memory block allocated for foo() in stack becomes free.

In line 9, main() method terminates and the stack memory created for main() method is destroyed. Also the program ends at this line, hence Java Runtime frees all the memory and end the execution of the program.

翻译:

一旦我们运行这个程序,它就会将所有的运行时类加载到堆空间中。在第1行找到main()方法时,Java运行时将创建供main()方法线程使用的堆栈内存。

 

我们正在第2行创建原始本地变量,因此它被创建并存储在main()方法的堆栈内存中。

 

因为我们正在第3行中创建一个对象,所以它是在堆内存中创建的,堆栈内存包含对它的引用。在第4行创建内存对象时也会发生类似的过程。

 

现在,当我们在第5行调用foo()方法时,将创建堆栈顶部的一个块,用于foo()方法。由于Java是通过值传递的,所以在第6行的foo()堆栈块中创建了一个新的对象引用。

 

7行创建了一个字符串,它进入堆空间中的字符串池,并在foo()堆栈空间中为它创建一个引用。

 

foo()方法在第8行终止,此时堆栈中为foo()分配的内存块变为空闲。

 

在第9行中,main()方法终止,并销毁为main()方法创建的堆栈内存。程序在这一行结束,因此Java运行时释放所有内存并结束程序的执行。

 

以上代码实现的内存完整结构图:

笔记整理——深入理解JVM原理

Java堆内部结构

1、Young(年轻代)大多数情况下JAVA程序中创建的对象是从新生代分配内存,新生代有两部分组成:Eden Space和两块大小相等的Survivor Space(S0和S1)。可以通过参数-Xmn来指定新生代的大小,通过-XX:SurivorRatio来指定Eden Space和Survivor Space的大小。

2、Tenured(年老代)年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。 

3、Perm(持久代)用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

 

新生区       

    新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1去也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullGC),进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。     

 

    如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。     

代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

 

养老区

    养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。    

 

永久区

    永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。

原因有二:     

    程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。     

大量动态反射生成的类不断被加载,最终导致Perm区被占满。     

说明:     

Jdk1.6及之前:常量池分配在永久代 。     

Jdk1.7:有,但已经逐步“去永久代” 。     

Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。

 

永久代(PermGen)和元空间的区别(Metaspace)

    类的元数据, 字符串池, 类的静态变量将会从永久代移除,放入Java heap或者native memory。其中建议JVM的实现中将类的元数据放入 native memory,将字符串池和类的静态变量放入Java堆中。这样可以加载多少类的元数据就不在由MaxPermSize控制,而由系统的实际可用空间来控制。为什么这么做呢? 减少OOM只是表因,更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有一个叫永久代的东西,但是运行良好,也不需要开发运维人员设置这么一个永久代的大小。

 

    这块内存大小可以通过两个参数进行指定:-Xms和-Xmx。

-Xms表示JVM启动时申请的最小heap内存,默认为物理内存的1/64但小于1G。

-Xmx表示JVM可申请的最大heap内存,默认为物理内存的1/4但小于1G。

默认空闲堆内存小于40%时,JVM会增大heap到-Xmx指定的大小,这个比例可以通过参数-XX:MinHeapFreeRatio=来指定;默认当空闲堆内存大于70%时,JVM会减少heap到-Xms指定的大小,这个比例可以通过参数-XX:MaxHeapFreeRatio=来指定。建议将-Xms和-Xmx设置为相同的值,以避免频繁调整JVM堆大小。由于不同对象在JVM中存活的时间不同,有的很快就可以回收,有的可能生命周期贯穿整个JVM的生命周期,所以在Sun JDK从1.2开始就对堆内存进行分代管理。

Java内存结构完整结构图:


以上是关于笔记整理——深入理解JVM原理的主要内容,如果未能解决你的问题,请参考以下文章

《深入理解JVM虚拟机》读书笔记

java内存区域——深入理解JVM读书笔记

深入理解jvm原理之逃逸分析

深入理解Java虚拟机(jvm性能调优+内存模型+虚拟机原理)视频教程

《深入理解 JVM 3ed》读书笔记

HashMap工作原理深入理解JVM正则