本系列文章讲解 面试中常见的 JVM 问题。这些问题之因此常见,是由于很基础,对于一个有点逼格的程序猿来讲, JVM 的相关特性和原理在工做也须要熟知。笔者也在面试的过程当中屡屡受挫,屡败屡战,总结一些常见知识点,这些知识点既能够应付面试,也能够帮助读者深刻了解 JVM 提供大纲。html
在用 C 之类的编程语言时,程序员须要本身手动分配和释放内存。而 Java 不同,它有垃圾回收器,释放内存由回收器负责。java
Java 虚拟机在执行 Java 程序的过程当中会把它管理的内存划分红若干个不一样的数据区域。那咱们来简单看一下 Java 程序具体执行的过程:程序员
首先 Java 源代码文件(.java 后缀)会被 Java 编译器编译为字节码文件(.class 后缀),而后由 JVM 中的类加载器加载各个类的字节码文件,加载完毕以后,交由 JVM 执行引擎执行。在整个程序执行过程当中,JVM 会用一段空间来存储程序执行期间须要用到的数据和相关信息,这段空间通常被称做为 Runtime Data Area(运行时数据区),也就是咱们常说的 JVM 内存。所以,在 Java 中咱们经常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。面试
本文的主要内容:算法
运行时数据区分为线程私有和共享数据区两大类。其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,全部线程共享的数据区包含 Java 堆、方法区,在方法区内有一个常量池。编程
下面咱们依次介绍这些数据区。数组
堆用于存放对象实例,全部的对象和数组都要在堆上分配。是 JVM 所管理的内存中最大的一块区域。Java 堆是全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例以及数组都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,所以也被称做 GC 堆(Garbage Collected Heap).从垃圾回收的角度,因为如今收集器基本都采用分代垃圾收集算法,因此 Java 堆还能够细分为:新生代和老年代。新生代具体划分有:Eden 空间、From Survivor、To Survivor 空间等,进一步划分的目的是更好地回收内存,或者更快地分配内存。缓存
方法区与 Java 堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即编译器编译后的代码等数据。
HotSpot 虚拟机中方法区也常被称为永久代
,本质上二者并不等价。仅仅是由于 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就能够像管理 Java 堆同样管理这部份内存了。可是这并非一个好主意,由于这样更容易遇到内存溢出问题。相对而言,垃圾收集行为在这个区域是较少出现的,但并不是数据进入方法区后就永久存在了。安全
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各类字面量和符号引用)微信
Java 虚拟机栈是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。 Java 内存能够粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是如今说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。存储局部变量表、操做数栈、动态连接和方法出口等信息。 局部变量表主要存放了编译器可知的各类数据类型、对象引用。
和虚拟机栈所发挥的做用很是类似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 一个 Native Method 就是一个 Java 程序调用非 Java 代码的接口。在定义一个 Native method 时,并不提供实现体(有些像定义一个java interface),由于其实现体是由非 Java 语言在外面实现的。标识符native能够与全部其它的 Java 标识符连用,可是 abstract 除外。
咱们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,而且只会回载一次。在这个被加载的字节码的入口维持着一个该类全部方法描述符的 list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public 等)等等。
若是一个方法描述符内有 native,这个描述符块将有一个指向该方法的实现的指针。这些实如今一些 DLL 文件内,可是它们会被操做系统加载到 Java 程序的地址空间。当一个带有本地方法的类被加载时,其相关的 DLL 并未被加载,所以指向方法实现的指针并不会被设置。当本地方法被调用以前,这些 DLL 才会被加载,这是经过调用 java.system.loadLibrary()
实现的。
须要提示的是,使用本地方法是有开销的,它丧失了 Java 的不少好处。若是别无选择,咱们能够选择使用本地方法。
程序计数器是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都须要依赖这个计数器来完。 另外,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
在 Java 中一个线程就会相应有一个线程栈与之对应,这点很容易理解,由于不一样的线程执行逻辑有所不一样,所以须要一个独立的线程栈。而堆则是全部线程共享的。栈由于是运行单位,所以里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
Java 的堆是一个运行时数据区,类的(对象从中分配空间。这些对象经过 new、newarray、anewarray 和 multianewarray 等指令创建,它们不须要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优点是能够动态地分配内存大小,生存期也没必要事先告诉编译器,由于它是在运行时 动态分配内存的,Java 的垃圾收集器会自动收走这些再也不使用的数据。但缺点是,因为要在运行时动态分配内存,存取速度较慢。栈的优点是,存取速度比堆要快,仅次于寄存器,栈数据能够共享。但缺点是,存在栈中的数据大小与生存期必须是肯定的,缺少灵活性。栈中主要存放一些基本类 型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄。
在 Java 中当咱们要对数据进行更底层的操做时,通常是操做数据的字节(byte)形式,这时常常会用到 ByteBuffer 这样一个类。ByteBuffer 提供了两种静态实例方式:
public static ByteBuffer allocate(int capacity) public static ByteBuffer allocateDirect(int capacity) 复制代码
为何要提供两种方式呢?这与 Java 的内存使用机制有关。ByteBuffer 有两种,一种是 heap ByteBuffer,该类对象分配在 JVM 的堆内存里面,直接由 Java 虚拟机负责垃圾回收;一种是 direct ByteBuffer 是经过 JNI 在虚拟机外内存中分配的。JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它能够直接使用 Native 函数库直接分配堆外内存,而后经过一个存储在 Java 堆中的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样就能在一些场景中显著提升性能,由于避免了在 Java 堆和 Native 堆之间来回复制数据。本机直接内存的分配不会收到 Java 堆的限制,可是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。经过 Jmap 没法查看该快内存的使用状况。只能经过 top 来看它的内存使用状况。
直接内存并非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用。并且也可能致使 OutOfMemoryError 异常出现。 DirectMemory 容量能够经过 -XX:MaxDirectMemorySize
指定,若是不指定,则默认为与 Java 堆的最大值。
direct ByteBuffer 经过 full gc 来回收内存,direct ByteBuffer 会本身检测状况而调用 system.gc()
,可是若是参数中使用了 -DisableExplicitGC
那么就没法回收该快内存了,-XX:+DisableExplicitGC
标志自动将 System.gc()
调用转换成一个空操做,就是应用中调用 System.gc()
会变成一个空操做,所以须要咱们手动来回收内存了。
@Test
public void testGcDirectBuffer() throws NoSuchFieldException, IllegalAccessException {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
cleanerField.setAccessible(true);
Cleaner cleaner = (Cleaner) cleanerField.get(buffer);
cleaner.clean();
}
复制代码
除此以外,CMS GC 也会回收 Direct ByteBuffer 的内存,CMS 主要是针对老年代空间的垃圾回收。
在 Java 中,类型的加载、链接和初始化过程都在程序运行期间完成的,这种策略虽然会使类加载时增长一些性能开销,可是提供了高度的灵活性,Java 天生能够动态扩展的语言就是依赖于运行期动态加载和动态链接的特色实现的。
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的 Java 类型,这就是 Java 虚拟机的类加载机制。Class 文件是一串二进制的字节流。实际上,每一个 Class 文件都有可能表明着 Java 语言中的一个类或者接口。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为链接(Linking)。
加载
查找并加载类的二进制数据。 加载是类加载过程的第一个阶段,虚拟机在这一阶段须要完成如下三件事情:
验证
确保被加载的类的正确性。 这一阶段是确保 Class 文件的字节流中包含的信息符合当前虚拟机的规范,而且不会损害虚拟机自身的安全。包含了四个验证动做:文件格式验证,元数据验证,字节码验证,符号引用验证。
java.lang.IncompatibleClassChangeError
异常的子类,如 IllegalAccessError
、NoSuchfiledError
、NoSuchMethodError
等。准备
为类的静态变量分配内存,并将其初始化为默认值。 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
解析
把类中的符号引用转换为直接引用。 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
初始化
类变量进行初始化 为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。
除此以外,还有用户自定义类加载器,是 java.lang.ClassLoader 的子类。在程序运行期间,经过java.lang.ClassLoader 的子类动态加载 class 文件,体现 Java 动态实时类装入特性.
双亲委派模型的工做流程是:若是一个类加载器收到了类加载的请求,它首先不会本身去加载这个类,而是把请求委托给父加载器去完成,依次向上。所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器没有找到所需的类时,子加载器才会尝试去加载该类。
经过带有优先级的层级关系能够避免类的重复加载; 保证 Java 程序安全稳定运行,Java 核心 API 定义类型不会被随意替换。
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,而且检查这个符号引用表明的类是否已被加载、解析和初始化过。若是没有,那必须先执行相应的类加载过程。
在类加载检查经过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后即可肯定,为对象分配空间的任务等同于把一块肯定大小的内存从Java堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种:
选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。虚拟机采用CAS配上失败重试的方式保证更新操做的原子性。
在 Hotspot 虚拟机中,对象在内存中的布局能够分为3块区域:对象头、实例数据和对齐填充。
对象头,Hotspot 虚拟机中的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC 分代年龄、锁状态标志等等);另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是那个类的实例。
实例数据,是对象真正存储的有效信息,也是在程序中所定义的各类类型的字段内容。
对齐填充部分,不是必然存在的,也没有什么特别的含义,仅仅起占位做用。 由于Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),所以,当对象实例数据部分没有对齐时,就须要经过对齐填充来补全。
创建对象就是为了使用对象,咱们的Java程序经过栈上的reference数据来操做堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有句柄和直接指针两种:
这两种对象访问方式各有优点。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针。而 reference 自己不须要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
本文主要讲了 JVM 中运行时数据区的划分以及类加载机制。JVM 中的对象建立以后,如何回收无用的对象呢?JVM 的垃圾回收算法和多种垃圾收集器是怎么样的呢?下篇文章将会具体讲解。