最近 web 应用,接触了 java。
一直一来对 java 提不起什么兴趣,不过最近为了能写简单 vm ,学了学 java 虚拟机 jvm。学着学着就喜欢上了 JVM。今天我们一起总体看一下 JVM 的架构。尤其对 JIT 喜欢的不得了。
编写一个 java 文件,然后通常做法我们会用 javac 把文件编译为 class 文件。最后用 java 来执行 class 文件我们就运行程序。
JVM 就是在我们执行 java 来运行 class 文件时介入,这个时候会创建一个 JVM 的实例。JVM 负责加载 class 文件并将其执行。
类加载器负责加载刚刚用 javac 生成的 class 文件。同时也负责加载 JVM 中内置的 class 文件,例如 String.class Object.class 等我们可以直接调用 Java API 对应的 class 文件。
执行引擎读取方法区的字节码自适应解析,边解析边运行,运行时会调用 JVM 操作系统所提供系统命令以执行。
我们一个一个来看,先看一下类加载是如何分配内存给字节码的。
从图上中看,JVM 架构主要是由三部分(类加载器,运行时数据区和执行引擎)组成的。运行时数据区域又包含了5部分。我们先把类加载器放大来看一看其内部结构。
方法区: 存储已被虚拟机加载的类信息、常量、静态变量、即时编译后代码等数据。常量池位于方法区,并使用永久代来实现方法区。
堆区: 我们常说用于存放对象的区域。
虚拟机栈: 方法执行时创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法一个栈帧,互不干扰
本地方法栈: 用于存放执行native方法的运行数据。
程序计数器: 当前线程所执行的字节码的指示器,通过改变计数器来选取下一条需要执行的字节码指令。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
第一个阶段是加载阶段,就是读取不同位置 class 文件或是包含若干个 class 文件的 jar 文件,将其加载。
启动类加载器:该 类加载器负责将放在JAVA_HOME/lib目录中的,或被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。
扩展类加载器:该加载器由sun.misc.Lanucher$ExtClassLoader实现,负责 加载JAVA_HOME/lib/ext目录中的或者是java.ext.dirs系统变量所指定的路径中的所有 类库,开发者可以直接使用扩展类加载器。
应用程序类加载器:该类加载器由sun.misc.Lanucher$AppClassLoader实现。该类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一 一般 也称为系统类加载器。它负责加载用户类路径上 所指定的 类库 ,开发者可以直接使用该类加载器 ,若 程序中没有自定义过自己 的类加载器,一般都是程序中 的默认类 加载器。
初始化: 类初始化阶段是类加载过程的最后一步。在这一步,才真正开始执行类中定义的Java代码。初始化阶段是执行类构造器()方法的过程。
文件格式验证:验证字节码是否符合 Class 文件的格式规范,且能被当前版本的虚拟机处理。只有通过该阶段的验证,字节流才会进入内存的方法区进行存储,后续的三个验证阶段全部都是基于方法区的存储结构进行的,不会在直接操作字节流。
元数据验证:对字节码描述的信息进行语义分析,以保证描述的信息符合Java语言规范要求。
字节码验证:该阶段的目的是通过数据流和控制流分析,确定程序语义是否是合法、符合逻辑的。该阶段对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出对虚拟机安全有危害的事。
符号引用验证:该阶段校验发生在虚拟机将符号引用转化为直接引用的时候,该转化动作将在连接的第三阶段-解析阶段发生。符号引用验证可视为对类自身以外的信息进行匹配性校验,它的目的是确保解析动作能正常执行。
解析阶段是虚拟机将常量池内 的符号引用替换为直接 引用的过程。
由于 PermGen 内存管理的效果远没有达到预期,所以JCP已经着手去除PermGen的工作。在JDK7中,字符串常量已经从永久代移除。
方法区: 存储已被虚拟机加载的类信息、常量、静态变量、即时编译后代码等数据。常量池位于方法区,并使用永久代来实现方法区。
堆区: 我们常说用于存放对象的区域
现今JDK8(我喜欢) 中 PermGen 已经被彻底移除,取而代之的是 metaspace 数据区,使用 native 内存,申请和释放由虚拟机负责管理。
在JDK8下,旧的参数 -XX:PermSize 和 -XX:MaxPermSize 会被忽略并显示警告。
新的 Metaspace 通过参数-XX:MetaspaceSize 和-XX:MaxMetaspaceSize 设定。
堆区是开发过程中接触最多的JVM内存区域,也是我们相对关注的。该区域被所有线程共享,所有创建(new)的对象都在这个区域分配内存并初始化。
堆区中内存分配和回收要消耗非常多的性能和时间。相比之下,栈更容易管理且轻巧。
有些在堆区保存的对象,通过一定的技术手段,自动转变为在栈中完成生命周期,这种技术就是逃逸分析。把本来存放在堆内存的数据分配到栈中。这样,数据的生命周期就能随着入栈和出栈而完成管理,不需要像堆内存一样进行内存繁杂的回收操作,减轻堆内存的压力。
程序计数器可看做当前线程所执行字节码行号的指示器。每个线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。
如果当前线程执行的是Native方法,则这个计数器为空。执行Java方法时,这个计数器记录执行字节码指令地址。
这里要说太多,以后慢慢说...
作者:zidea
链接:https://www.jianshu.com/p/b6feb121a450
共同学习,写下你的评论
评论加载中...
作者其他优质文章