一直以来以为虚拟机是Java最难的一部分,涉及最底层的原理,学起来难度很大,并且工做中基本上用不到这些原理,因此对这部分“敬而远之”。现现在工做五年了,从Java基础到算法、数据结构、网络、数据库、设计模式都有涉猎,虚拟机部分在脑海里仍是空空荡荡,连常常被谈起的垃圾回收机制都不了解,实在是惭愧。了解虚拟机通往高级Java程序员的必由之路,同时学好虚拟机也能提升咱们代码的质量,知道对象是怎么建立的,放在哪里,怎么执行,怎么回收的?明白这些问题让咱们在程序的世界里当一个“明白人”。java
学习java时都知道Java内存分为两大快堆和栈,堆存放对象实例和数组对象,栈存放基本数据类型和对象的引用,这样有点笼统,实际这里说的堆指的是图中左边的Java堆,栈指的是本地方法栈,更具体的应该是栈里面栈帧的局部变量表。程序员
内存区域总共分两大块:左边的堆内存区域和右边的栈内存加计数器,左边的堆内存是线程共享的,只有一份;右边部分每一个线程独立一份,随线程而生,随线程而灭,是线程运行的内存区域。算法
Java堆:是程序中内存管理最大的一部分,主要存放Java中的对象的实例、数组,堆里面为了内存回收方便化分了老年代和新生代区域。数据库
方法区:方法区也能够理解为常说的永久代,和堆相似,只是逻辑上存放的数据不一样,主要存放被虚拟机加载的类信息、常量、静态变量、缓存的常量池等。既然是永久代,通常方法去的内存不多被回收,相对来讲最稳定。设计模式
虚拟机栈:存放线程运行时的上下文信息,栈内部包括栈帧,每一个栈帧表明一个方法调用,方法的调用体如今栈帧的入栈和出栈,每一个栈帧内部都存在一个局部变量表,用于存放方法内的变量,包括基本数据类型和引用数据类型,引用数据类型时这里只存放引用,地址指向的是堆中的一块内存区域。数组
本地方法栈:与虚拟机栈相似,不一样为这里存放的是本地方法调用的运行数据,在java中声明的native方法。缓存
程序计数器:用于记录当前线程执行到那个位置,线程内执行流程的控制依赖程序计算器来完成。安全
虚拟机从加载程序到运行程序都要进行内存分配,分配的时候也伴随的内存的回收,当对象“已死”(无引用)的时候进行回收。网络
一、对象可回收的两种判断算法数据结构
如何判断对象已死呢,通常有两种方式:
引用计数算法:经过创建对象引用的计算器,每增长一个引用引用数+1;引用失效时-1;引用数为0表明这个时候这个对象已经没有被用到了,能够回收。
可达性分析算法:经过路径查找的方式判断对象是否能够到达,经过维护一个“GC Roots”集合表明顶层对象,在此顶层对象的“引用链”以外的对象,说明是一个不能到达的对象,能够放心回收了。
引用计数算法和可达性分析算法各有利弊,引用计数算法实现起来简单,可是须要维护一个引用计数,更新的次数太频繁,并且引用计数表也须要占必定内存;可达性分析是相对更广泛的一种实现方法,在回收时再进行一次检查,不用每次引用发生变化时发生更新,缺点是实现起来更复杂,维护“GC Roots”的算法比较复杂。
二、垃圾收集算法
通常虚拟机实现都采用了分代的方法,把内存划分了老年代和新生代,老年代存放的是相对稳定的对象;新生代存放的是活跃的对象,短时间须要回收的。针对这两类的特色分别做出不一样的策略,提升回收的效率。
标记-清除算法:最基础的一个算法,第一步先标记出须要回收的对象,而后统一清除。标记清除有两个缺点:第一,执行效率不稳定,若是大部分都是须要回收的对象,标记清除效率较低;第二,清除后会形成内存的不连续,大量的碎片,若是建立一个大对象没有连续的内存又须要执行垃圾回收。
标记-复制算法:标记复制算法是为了不标记清除算法对于大部分对象须要回收执行效率率低的问题,把内存区域划分了两部分,把须要回收的一部分复制到另一边,而后执行整块区域的回收,两块区域交替的使用。这种算法缺点是浪费了一半内存空间,因此有一个优化的方案,把内存区域拆分红三快,一块Eden两块Survivor,HotSpot的二者比例是8:1,Eden存放新分配的对象,每次回收时把存放的对象复制到其中一块空闲的Survivor,清除Eden另一块Survivor空间,交替的使用、清除Survivor空间;这种状况下存放数据的区域有90%,只有10%的空间浪费,空间利用很好,可是须要考虑当存活的对象大于10%时,这种状况就须要借用老年代,把它分配到老年代。
标记-整理算法:整理算法是在标记清除和标记复制之间折中的一种算法,使用标记清除,可是按期整理,把不连续的内存整理到一块去,解决了内存的碎片和空间上的浪费。缺点是每次整理是一个很负重的操做,会形成用户程序的暂停。
这三种算法中,标记清除和标记整理适合老年代,须要回收的对象占少部分的状况;标记复制算法适合新生代,每次绝大部分对象须要回收,只须要把小量存活的挪到另外一块位置。
三、内存分配的几条策略
大多数状况下对象在堆中的新生代Eden空间分配,当Eden没有空间时会触发一次GC。
当Eden空间不够或一个大的对象(例如大的数组)建立将分配到老年代。
长期存活的新生代对象会转移到老年代,在新生代的对象每熬过一次GC,年龄加1,默认15岁时将会移动到老年代。
程序经过new、静态方法、静态字段引用、子父类的引用、反射调用等方式会触发类的加载,把类的字节码加载到虚拟机。加载流程:
加载:类的字节码加载到虚拟机,经过类加载器加载到虚拟机,默认经过Java的引导类加载(Bootstrap),也能够经过自定义的类加载器加载,加载的不必定必须是一个本地文件,只要是符合要求的二进制字节码便可,能够来源于网络或数据库。
验证:验证字节码的正确性,是不是一个合格的字节码文件,保证虚拟机的运行安全。
准备:分配内存和初始化零值。
解析:符号引用替换成直接引用,符号引用是字面量的形式,前面已经分配了内存,这里替换成指向的内存地址。
初始化:类加载的最后一步,执行程序代码里的初始化,包括静态代码块,构造方法,默认字段值。
Java内存模型是定义了程序中变量的访问规则。
每一个线程都有一个工做内存,工做内存经过读写操做和主内存交互,达到变量的共享。
交互操做:
lock和unclock: 对主内存的变量进行加锁和解锁,锁定后其余线程将不可操做。
read和load: read从主内存读取一个变量到工做内存,load放入读取的变量放到工做内存中。
store和write: store把一个工做内存的变量传递到主内存中,write把传递过来的变量写入主内存。
use: 把一个工做内存中的变量传递给执行引擎使用。
assign: 把从执行引擎接收到的赋值给工做内存的变量。