JVM基础知识整理(一)java程序执行流程及内存模型

编程

        对于JAVA开发人员来说,可能体会到这样的怪象:在计算机课程和平时工作过程中都几乎没怎么学习实践过JVM、高并发、多线程、写注解,但在工作年限稍久之后,出来面试时总能碰到这些知识的"刁难",导致与理想的岗位、加薪无缘,难道平时工作都已经996之后,仍需不断抽时间去学习这些天书一般的知识吗?程序员的能力进阶靠的是全天候的工作+自学吗?其实程序员的日常并不是说需要长时间的学习,学习的驱动力应该是源自个人的兴趣,并且是由工作实践中而来就最好,然后在业余时间中对知识进行针对性深入提炼,工作年限越久,其实学习过程中的感悟就会越深。

        在职业生涯初期,面对JVM苦涩难懂、实用性不高,本人是不选择深入理解这块知识的,后来随着在项目开发、特别是生产环境中遇到的项目启动报内存不足 、项目运行过程中内存溢出、项目访问慢隔断时间需重启等等这些熟悉的OOM难题经历得越来越多时,本人意识到是时候需要对JVM的知识进行梳理学习,以便日后更好地排查问题及调优。本人以《深入理解JAVA虚拟机》书本学习为主对JVM知识进行总结,重实际问题的解决,轻细枝末节的探讨(如想自己设计一套jvm虚拟机的极客则另论),所以学习的视角有所不同,本文从java程序执行流程开始讲起,先总后分进行阐述。

java程序执行流程总述

        

        如图为常见的JVM内部结构图,一个class文件进入JVM后具体被执行的流程如下:

1.类加载

       目前最常见的JVM即hotspot,对JVM而言,无论什么语言,只要编译为class文件即可被执行。class文件进入JVM首先就需要通过类加载器classloader,顾名思义,JVM设计的classloader目的就是为了class资源的"预加载",以便于后续执行引擎的执行。类加载的过程包括:加载(读取class文件)、验证(检查文件内容是否合规)、准备(为类变量分配内存)、解析、初始化(初始化类变量值、执行构造函数),最后转化为可被执行引擎直接使用的数据。整个类加载过程中,类加载器将会从class文件中提取信息包括加载过程中的class文件字节流、准备过程中的内存分配,都会存放到一个区域-方法区(下面介绍)。

2.执行引擎执行

        执行引擎开始执行时,JVM就会分配相应资源开始配合引擎执行,所有资源所在位置统称为运行时数据区,就像一台机器的核心运转区域,各部件各司其职。运行时数据区就是JVM的核心,熟悉了该区域数据的相互运作才算是真正了解JVM。该数据区用到的数据结构就包堆和栈,其中栈用于存放运行指令,堆用于存放几乎全部实例对象,堆可被线程共享,栈则每个线程独立私有,这也方便实现了线程共享变量。以下为各部件的说明:

       1)方法区Method area。存储类加载器加载后的提取信息,包括类名、访问修饰符、常量、静态变量、方法信息、编译后的代码等。与堆一样,属于各线程共享的区域,在JDK1.7中该区域通过PermGen space(永久代)来实现,到了 JDK1.8版本却又废弃了永久代,这其中的内存结构变化细节具体看下一小节。

       2)堆Heap。执行引擎执行过程中需要对新生对象进行分配内存,划分出来存放对象的区域就叫堆,堆被设计为三块区域以合理使用堆内存,常见的内存溢出OOM几乎都在堆发生,所以该部分是常说的内存调优的重点关注区域。关于堆内存优化后续另开一文详细讲解。

       3)Java栈Java stack。Java栈由栈帧组成,是存储单个进程的方法调用顺序的数据结构,属于后进先出(跟我们日常方法执行顺序一样)。执行引擎开始执行方法时先从方法区里读取指令,然后将执行过程中当前所执行的指令到放到栈帧中,直到到方法执行完成,整个过程就是栈帧在Java栈中入栈到出栈的过程。一个栈帧主要包括局部变量表(方法内局部变量)、操作数栈(参与运算的变量)、动态连接(指向常量池的引用)、方法返回地址 (传递给上层方法的返回值)。Java栈一般需要内存较小,所以栈内存不足、栈调优会较为少处理。

       4)本地方法栈Native method stack。本地方法栈与Java栈所不同的在于调用的是Native方法,如Java方法通过加载dll调用的C++方法。

       5)程序计数器Program counter register。该翻译在国内有时也叫PC寄存器,存储的是当前线程所执行字节码的行号信息。如程序为单线程,执行引擎不需要程序计数器也可以按顺序执行class里的方法,该区域存在的意义在于处理多线程情况下,让线程切换后能恢复到正确的执行位置。跟Java栈一样,程序计数器占用的只是较小一块内存,可以确定的是该区域不可能出现OOM。

        除了运行时数据区外,如执行引擎遇到需要与硬件打交道的C++方法,JVM将会单独划分一区域存储该方法数据,即本地接口JNI,JNI方法调用的是本地库如dll文件。

3.垃圾回收

       垃圾即无用的对象,在执行引擎运行过程中,JVM会有一个垃圾收集器专门进行垃圾自动回收,也就是GC内存空间清理,这部分就是JVM调优的核心部分,JVM在GC处理后仍有可能出现内存溢出、泄漏问题,此时就需要人为进行必要监控和调节。理想情况下,程序运行到最后,全部垃圾也被回收完毕,JVM运行方法结束,java程序执行完毕。

内存模型的变化

         在使用JDK1.7过程中,如果项目war包中包含了大量的jar、class,启动应用就容易报"java.lang.OutOfMemoryError: PermGen space ",其中 “PermGen space”(永久代)实际上指的就是非堆、方法区,其实只有 HotSpot 才有 “PermGen space”,但并不是说该设计违反JVM规范,而是HotSpot团队希望GC分代收集能拓展至方法区,故从JDK1.6开始,在Java堆外多设计了永久代,来实现方法区的功能,并能让垃圾收集器对此进行管理。
        但在此之后,HotSpot团队却逐渐考虑移除永久代,在JDK1.7中,存储在永久代的部分数据(如静态变量、常量池)就已经转移到了Java堆中,到了JDK1.8,永久代由MetaSpace(元空间)来替换,元空间存储前永久代的部分数据(如class元信息),使用的是本地内存。

        下面,我们可以通过 JDK 1.6、JDK 1.7 和 JDK 1.8 来运行一段代码,来验证常量池的存储区域变化。

static String base = "string";

public static void main(String[] args) {

List<String> list = new ArrayList<String>();

for (int i=0;i< Integer.MAX_VALUE;i++){

String str = base + base;

base = str;

list.add(str.intern());

}

}

        运行结果分别是:

        Jdk1.6:java.lang.OutOfMemoryError : PermGen space

        Jdk1.7:java.lang.OutOfMemoryError : Java heap space

        Jdk1.8:java.lang.OutOfMemoryError : Java heap space

        下面,我们再通过JDK1.7、JDK1.8来验证加载class信息的存储区域变化。

URL url = null;

List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();

try {

url = new File("/tmp").toURI().toURL();

URL[] urls = {url};

while (true){

ClassLoader loader = new URLClassLoader(urls);

classLoaderList.add(loader);

loader.loadClass("com.paddx.test.memory.Test");

}

} catch (Exception e) {

e.printStackTrace();

}

        运行结果分别是:

        Jdk1.7:java.lang.OutOfMemoryError : PermGen space

        Jdk1.8:java.lang.OutOfMemoryError : Meta space

        那为什么从JDK1.6到JDK1.8内存模型会发生这么这样的转变?目的如下:

        1)实现更高效的方法区内存管理。方法区中关于如类总数、常量池的大小和方法数量等,本身对于开发者而言不容易确定,将其全部放到永久代进行管理,PermSize指定太小就很容易造成永久代内存溢出,所以像静态变量和常量池等都在JDK1.7中被并入堆中进行管理。即便如此,永久代瓜分后剩下的数据仍然需要优化管理,所以为了彻底解决永久代为GC带来的不必要的复杂度、回收效率偏低问题,JDK1.8将永久代剩余数据从永久代剥离出来,其内存管理交由元空间虚拟机来完成

        2)后续兼容的考虑。 这个从 Oracle 官方回答可以得知,后续可能会将HotSpot 与 JRockit 合二为一,而JRockit 本身是不存在永久代的。

以上是 JVM基础知识整理(一)java程序执行流程及内存模型 的全部内容, 来源链接: utcz.com/z/518746.html

回到顶部