深刻理解 JVM:让你轻松搞定这些问题所在

一:JVM体系概述前端

1:JVM是运行在操做系统之上的,他与硬件没有直接的交互。java

深刻理解 JVM:让你轻松搞定这些问题所在

 

二:JVM内存结构算法

Java虚拟机在运行时,会把内存空间分为若干个区域。Java虚拟机所管理的内存区域分为以下部分:方法区、堆内存、虚拟机栈、本地方法栈、程序计数器。安全

深刻理解 JVM:让你轻松搞定这些问题所在

 

一、类装载器ClassLoader前端框架

负责加载class文件,class文件在文件开头有特定的文件标识,而且ClassLoader只负责class文件的加载,至于它是否能够运行,则是由执行引擎(Execution Engine)决定。框架

虚拟机自带的加载器:spa

启动类加载器(Bootstrap):由C++编写,不是前端框架Bootstrap。操作系统

扩展类加载器(Extension):由Java语言编写线程

应用程序类加载器(App):由Java语言编写,也叫系统类加载器,加载当前应用的classpath的全部类。3d

用户自定义加载器

Java.lang.ClassLoader的之类,用户能够定制的加载方式。

类加载器的双亲委派机制

某个特定的类加载器在加载类的请求时,首先将加载任务委托给父类加载器,依次递归,若是父类加载器能够完成类加载任务,就成功返回;只有父类加载器没法完成该加载任务时,才本身去加载。

沙箱机制(防止恶意代码对java自己的破坏)

当用户命名了和Java同样的类时,Java会首先加载自带的类。

二、方法区

方法区是线程共享的,一般用来保存装载的类的元结构信息。主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据。

在jdk1.7及其以前,方法区是堆的一个“逻辑部分”(一片连续的堆空间)。也有人用“永久代”表示方法区。

在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并无处于堆内存上,而是直接占用的本地内存(NativeMemory)。

深刻理解 JVM:让你轻松搞定这些问题所在

 

3:堆内存

一个JVM实例只存在一个堆内存,堆内存的大小是能够调节的。类加载器读取了类文件后,须要把类,方法,穿变量放到堆内存中去,

它是JVM管理的内存中最大的一块区域,堆内存和方法区都被全部线程共享,在虚拟机启动时建立。在垃圾收集的层面上来看,因为如今收集器基本上都采用分代收集算法,所以堆还能够分为新生代(YoungGeneration)和老年代(OldGeneration),新生代还能够分为Eden、From Survivor、To Survivor。

JAVA1.7以下图,但在Java1.8中,其余基本没变,只是将Perm变成了元空间

深刻理解 JVM:让你轻松搞定这些问题所在

 

4:程序计数器

程序计数器是一块很是小的内存空间,能够看作是当前线程执行字节码的行号指示器,每一个线程都有一个独立的程序计数器,所以程序计数器是线程私有的一块空间,此外,程序计数器是Java虚拟机规定的惟一不会发生内存溢出的区域。

5:虚拟机栈

虚拟机栈也是每一个线程私有的一块内存空间,它描述的是方法的内存模型。

虚拟机会为每一个线程分配一个虚拟机栈,每一个虚拟机栈中都有若干个栈帧,每一个栈帧中存储了局部变量表、操做数栈、动态连接、返回地址等。一个栈帧就对应Java代码中的一个方法,当线程执行到一个方法时,就表明这个方法对应的栈帧已经进入虚拟机栈而且处于栈顶的位置,每个Java方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。

六、本地方法栈

虚拟机栈执行的是Java方法,本地方法栈执行的是本地方法(Native Method),其余基本上一致

7:元空间

上面说到,jdk1.8中,已经不存在永久代(方法区),替代它的一块空间叫作“元空间”,和永久代相似,都是JVM规范对方法区的实现,可是元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制,但能够经过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定元空间的大小。

三:垃圾回收机制

垃圾回收,就是经过垃圾收集器把内存中没用的对象清理掉。垃圾回收涉及到的内容有:一、判断对象是否已死;二、选择垃圾收集算法;三、选择垃圾收集的时间;四、选择适当的垃圾收集器清理垃圾(已死的对象)。

1:判断对象是否以死

判断对象是否已死就是找出哪些对象是已经死掉的,之后不会再用到的,就像地上有废纸、饮料瓶和百元大钞,扫地前要先判断出地上废纸和饮料瓶是垃圾,百元大钞不是垃圾。判断对象是否已死有 引用计数算法 和 可达性分析算法。

(1)引用计数算法

给每个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;每当有一个地方再也不引用它时,计数器值减1,这样只要计数器的值不为0,就说明还有地方引用它,它就不是无用的对象。以下图,对象2有1个引用,它的引用计数器值为1,对象1有两个地方引用,它的引用计数器值为2 。

这种方法看起来很是简单,但目前许多主流的虚拟机都没有选用这种算法来管理内存,缘由就是当某些对象之间互相引用时,没法判断出这些对象是否已死,以下图,对象1和对象2都没有被堆外的变量引用,而是被对方互相引用,这时他们虽然没有用处了,可是引用计数器的值仍然是1,没法判断他们是死对象,垃圾回收器也就没法回收。

(2)可达性分析算法

了解可达性分析算法以前先了解一个概念——GC Roots,垃圾收集的起点,能够做为GC Roots的有虚拟机栈中本地变量表中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。

当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,就说明此对象是不可用的,是死对象。以下图:object一、object二、object三、object4和GC Roots之间有可达路径,这些对象不会被回收,但object五、object六、object7到GC Roots之间没有可达路径,这些对象就被判了死刑。

(3)方法区回收

上面说的都是对堆内存中对象的判断,方法区中主要回收的是废弃的常量和无用的类。

判断常量是否废弃能够判断是否有地方引用这个常量,若是没有引用则为废弃的常量。

判断类是否废弃须要同时知足以下条件:

  • 该类全部的实例已经被回收(堆中不存在任何该类的实例)
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象在任何地方没有被引用(没法经过反射访问该类的方法)

二、经常使用垃圾回收算法

(1)标记-清除算法:分为标记和清除两个阶段,首先标记出全部须要回收的对象,标记完成后统一回收全部被标记的对象,以下图。

缺点:标记和清除两个过程效率都不高;标记清除以后会产生大量不连续的内存碎片。

(2)复制算法:把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象所有复制到另外一块上,同时把使用过的这块内存空间所有清理掉,往复循环,以下图。

缺点:实际可以使用的内存空间缩小为原来的一半,比较适合。

(3)标记-整理算法:先对可用的对象进行标记,而后全部被标记的对象向一段移动,最后清除可用对象边界之外的内存,以下图。

(4)分代收集算法:把堆内存分为新生代和老年代,新生代又分为Eden区、From Survivor和To Survivor。通常新生代中的对象基本上都是朝生夕灭的,每次只有少许对象存活,所以采用复制算法,只须要复制那些少许存活的对象就能够完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。

三、选择垃圾收集的时间

当程序运行时,各类数据、对象、线程、内存等都时刻在发生变化,当下达垃圾收集命令后就马上进行收集吗?确定不是,他们要在保证线程安全的前提下进行垃圾回收

安全点:从线程角度看,安全点能够理解为是在代码执行过程当中的一些特殊位置,当线程执行到安全点的时候,说明虚拟机当前的状态是安全的,若是有须要,能够在这里暂停用户线程。当垃圾收集时,若是须要暂停当前的用户线程,但用户线程当时没在安全点上,则应该等待这些线程执行到安全点再暂停。理论上,解释器的每条字节码的边界上均可以放一个安全点,实际上,安全点基本上以“是否具备让程序长时间执行的特征”为标准进行选定。

安全区:安全点是相对于运行中的线程来讲的,对于如sleep或blocked等状态的线程,收集器不会等待这些线程被分配CPU时间,这时候只要线程处于安全区中,就能够算是安全的。安全区就是在一段代码片断中,引用关系不会发生变化,能够看做是被扩展、拉长了的安全点。

四、常见垃圾收集器

新生代收集器: Serial 、 ParNew 、 Parallel Scavenge

老年代收集器: Serial Old 、 CMS 、 Parallel Old

堆内存垃圾收集器: G1

这些垃圾收集器一样很重要,能够自行百度了解其原理。

相关文章
相关标签/搜索