Java内存模型

物理硬件高速缓存

    1.概念:将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待处理缓慢的内存读写了。

(更多…)

03-类加载器

类加载器

    
 
    类加载定义:通过一个类的全限定名来获取描述此类的二进制流来获取所需要的类的动作
        
    类从被加载到虚拟机内存中开始,到卸载出内存生命周期分为以下7个阶段
        加载(Loading) -> 【验证(Verification) -> 准备(Preparation) -> 解析(Resolution)】 -> 初始化(Initialization) -> 使用(Using) -> 卸载(Unloading)
        验证、准备和解析统称为连接(Linking)
    
    主动引用(会触发类初始化的引用):
        1.遇到new,getstatic,putstatic或invokestatic这4条指令时,若类没有初始化会被触发初始化(new对象时、读取或者设置类静态非常量字段时、调用类静态方法)
        2.反射调用类时
        3.初始化类,其父类还未被初始化时触发父类初始化
        4.虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个类
    被动引用(不会触发类初始化的引用):
        1.通过子类引用父类的静态字段
        2.通过定义数组来引用类
        3.在A类中用到了B类的静态常量,这里B类不会被初始化。常量在编译阶段会存到调用类的常量池中,本质上没有直接调用定义常量的类
    
    双亲委派模型:
        1.虚拟机的角度看类加载器种类:

                a.启动类加载器(Bootstrap ClassLoader),用C++实现。
                b.所有的其他类加载器,都由Java实现。并且都继承于java.lang.ClassLoader类
        2.开发人员的角度看类加载器种类:
                a.启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>/lib目录下。(按照文件名识别)
                b.扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中所有的类库
                c.应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现,由于ClassLoader的getSystemClassLoader方法返回值也是它,所以一般它也被称为系统加载器。
 
        工作过程:
            如果一个类收到了类加载的请求,它首先不会自己去尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。
    因此所有的类加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
    
       双亲委派模型带来的好处:
            1.[稳定性]同一个类始终会给同一个类加载器去加载,例如java.lang.Object在rt.jar中。那么在各种类加载器环境中都会是委派给启动类加载器,所以系统中也只会有一个Objec类
    
 
         ClassLoader实现双亲委派模型源码:
/**
*使用指定的二进制名称来加载类。此方法的默认实现将按以下顺序搜索类: 
1.调用 findLoadedClass(String) 来检查是否已经加载类。
2.在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。 
3.调用 findClass(String) 方法查找类。
如果使用上述步骤找到类,并且 resolve 标志为真,则此方法将在得到的 Class 对象上调用 resolveClass(Class) 方法。 
鼓励用 ClassLoader 的子类重写 findClass(String),而不是使用此方法,ClassLoader的findClass方法默认是抛出ClassNotFoundException

**/ 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

02-Reference & GC

一、引用
    Java中引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这个定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保存在内存中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
    在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为:
        以下四种引用强度依次逐渐减弱
        1.强引用(Strong Reference)
            强引用就是指在程序代码中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
        2.软引用(Soft Reference)
            软引用用来描述一些还有用,但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
        3.弱引用(Weak Reference)
            弱引用也是用来描述非必须对象的,但是它的强度比弱引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集器发生之前。当垃圾收集器工作时,无论当前的内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
        4.虚引用(Phantom Reference)
            虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference来实现虚引用。
二、判断对象已死算法
    1.引用计数算法(Reference Counting)
        定义:给对象中添加一个引用计数器,每当有个地方引用它时,计算器值就加一;当引用失效时,计数器值就减1;任何时候计算器都为0的对象就是不可能再被使用的。
        客观地说,引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,但是Java中没有选用它来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题
    2.根搜索算法(GC Roots Tracing)
        基本思路:通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始往下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
        在Java语言里,可作为GC Roots的对象包括以下几种:
        1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
        2.方法区中的类静态属性引用的对象。
        3.方法区中的常量引用的对象。
        4.本地方法栈JNI(即一般说的Native方法)的引用的对象。
三、对象死亡的两次标记过程
    在根搜索算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经过两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法或者finalize已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
    经过上面的判定如果有必要执行finalize方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象在finalize方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize中拯救自己–只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类的变量或对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那它就真的离死不远了。
代码测试如下:
package com.billstudy.jvm;
/**
 * Created by Bill on 2015-07-16 16:11
 * 测试GC自救,finalize只会被调用一次
 * Email: LuckyBigBill@gmail.com
 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("yes, i am still alive :)");
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        SAVE_HOOK = this;
    }
    public static void main(String[] args) throws Exception{
        SAVE_HOOK = new FinalizeEscapeGC();
        // 对象第一次拯救自己
        SAVE_HOOK = null;
        System.gc();
        // Finalizer方法优先级低,暂停等会儿它
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead :(");
        }
        // 和上面代码相同,这次自救失败了
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead :(");
        }
    }
}
// 输出结果
finalize method executed!
yes, i am still alive : )
no, i am dead : (

(更多…)

01-Jvm 内存区域

5849dbe1-8f05-4fd8-b327-5d6f62100d52

  1.程序计数器(Program Counter Register)
        在虚拟机中一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。 在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
        由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。我们称这类内存区域为”线程私有”的内存
    如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行虚拟机字节码指令的地址;如果正在执行的Native方法,这个计算器值则为空(Undefined),此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
 
    2.Java虚拟机栈(Java Virtual Machine Stacks)    
        与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中充入栈到出栈的过程
        平常大家说堆(Heap)栈(Stack),这种划分比较粗糙。所说的“栈”指的就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分。局部变量表存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(Reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个执行对象起始地址的引用指针,也可能是执行一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
    64位长度的Long和Double类型的数据会占用2个局部变量空间(Slot),其他的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,也就是一个方法的局部变量空间是完成确定的,在方法运行期间不会改变局部变量表的大小。
 
    3.本地方法栈(Native Method Stacks)
        和虚拟机栈发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机只想你个Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Nativce方法服务。虚拟机规范中对本地方法栈的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
    4.Java堆
        对大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。词内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规划中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在对上也渐渐变得不是那么“绝对了”。
    同时Java堆还是垃圾收集器管理的主要区域,因此很多时候也被称作”GC 堆”(Garbage Collected Heap)。如果从内存回收的角度看,由于现在的收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代
 
    5.方法区
        方法区(Method Area)和Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
    6.运行时常量池(Runtime Constant Pool)
        运行时常量池是方法区是一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内存将在类加载后存放到方法区的运行时常量池中。
        相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内存才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern方法。
        既然运行时常量池是方法区的一部分,自然会收到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemryError异常。
    
    7.直接内存(Direct Memory)
        直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
        在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
        显然,本机直接的内存分配不会收到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。