关于JVM的那些事儿(A)

什么是 JVM:

            JVM是Java Virtual Machine的缩写。它是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

JDK、JRE、JVM关系:

      JRE:JavaRuntimeEnvironment,所有编译过后的Java 程序都要在JRE下才能运行。普通用户只需要按照JRE,运行class文件即可。
      JDK:Java Development Kit是developers用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。
     JVM:JavaVirtualMachine,为JRE的一部分,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。使用JVM就是为了支持与操作系统无关,实现跨平台。

 

JVM class 加载机制:

         类加载机制,大致可分为:加载、验证、准备、解析、初始化、使用、卸载等七个过程,而类的加载由Bootstrap ClassLoader 及其子类完成,以下将详细介绍其具体过程。

         编译:此过程将把.java的文件编译成.class 文件,前者为源文件,后者字节码文件,此过程主要检验编译错误,例如赋值错误、缺少字符、包引用错误、对象、函数引用错误等。

         类加载:类的加载主要由根加载器及其子类完成,分为Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader、以及自定义的加载器。在类的加载阶段,将在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。需要注意的是,类的加载并非只能从class文件或者,也能从jar包或war包等包中获取,当然也能通过动态代理以及其它文件(如jsp)生成。当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

        验证:这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

       准备:该阶段是正式为类变量分配内存以及为基础类型设置初始值,即在方法区中分配这些变量所使用的内存空间。例如public static int a=1000,此时a将被初始化为0而非1000。

      解析:该阶段,将常量池中的符号引用,改为直接引用。其中,符号引用对于目标地址是否存于内存中无硬性要求,直接引用,则需要保证其存在。

      初始化:该阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。类的初始化,是自上由下的进行,即先进行父类初始化,再进行子类初始化。在类内部,其初始化步骤为:静态变量、静态初始化块、变量、初始化块、构造器,当存在继承时,其初始化步骤为:父类–静态变量、父类–静态初始化块、子类–静态变量、子类–静态初始化块、父类–变量、父类–初始化块、父类–构造器、子类–变量、子类–初始化块、子类–构造器。

图1.类加载流程

JVM执行引擎:

        1.代码解释器。 
        2.JIT代码生成器。该方式第一次执行的字节码会编译成本地的机器代码,被缓存在系统中,以后可以随时调用。
        3.自适应的优化器。这种方法里,虚拟机开始的时候解释字节码,但是会监控运行中程序的活动,并记录下使用最频繁的代码段,虚拟机会把这些活动最频繁的代码段编译成本地代码。
其实以上提的都是软件实现的虚拟机,还有一种虚拟机是由硬件芯片构成,它用本地方法执行java字节码。

JVM内存划分:

           JVM内存主要划分为六大块,即方法区、java堆、java栈、程序计数器、本地方法栈。

          方法区:存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息(其中静态变量放于常量池中)。当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

         JVM堆:JVM中的堆内存,为所有线程共享,用于存放对象实例,几乎所有的对象都在此处分配内存。堆内存 占据了JVM大多数内存空间,其存于一片不连续存储块上,而我们熟知的GC即垃圾回收,发生在该空间,由于垃圾回收机制采用分代回收算法,因此可将堆细分为新生代、老年代。新生代又有Eden空间、From Survivor空间、To Survivor空间三部分。而永久代(非堆内存,JDK1.8之前)的数据不会被更新,例如intern()产生的对象,并不会被回收,因此程序操作不当,可能导致永久代数据量过大,可能会导致OOM问题。在jdk1.7以后,引入了元空间,废除了永久代。关于各引用的GC,读者可自行阅读之前的文章。

      JVM栈:JVM栈分为本地方法栈与java栈,栈为线程私有,其生命周期与线程相同,每个方法被执行时,都会创建一个栈帧,用于保护现场,其中存储局部变量表、操作栈、动态链接、方法出口等信息。当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常。当java需要调用c、c++等非java程序时,会创建本地方法栈,对于大多数程序来说,基本上不会用到,当然其原理与前者大致类似,在此不做赘述。

      程序计数器:线程私有,它的生命周期与线程相同。可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,如:分支、循环、跳转、异常处理、线程恢复(多线程切换)等基础功能。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(undefined)。
程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以此区域不会出现OutOfMemoryError的情况。

git历史分支修复

背景介绍:某人A与某人B在开发特性分支完成后,往验证分支合并时,出现了冲突,但是A或者B在合并时,并没发觉,当分支上了主干分支后,特性分支被删除,但某一天在合并其它分支或代码检查时,发现主干分支代码存在问题,此时需要做分支fix,即将A与B编写的分支代码合并一次,然后再往主分支以及其它验证分支合并。然而令人意想不到得是:此时特性分支已经删除!面对此情此景,那么我们该如何修复主干或者验证分支代码呢?是拉取主干分支,重新编写一次代码呢?还是在直接在主干分支修改呢?

当然,这个案例可能比较特殊,我们现在不妨假设特性分支并未删除,我相信很多人都能想到解决方案,即将两个特性分支合并,再往验证分支合并,待验证通过后,往主干分支合并即可。那么现在的问题,即是怎么找回分支!我相信大家可能有各种各样不同的解决方式,现在就个人意见,分享下本人的处理方案。其处理流程如下图1所示。

修复流程图
图1   修复流程图

正如上图所示,其核心命令即为:git checkout commit_id -b newBranchName,其中commit_id 为commit 提交记录id,可通过git log 找寻,若集成到了第三编辑器,可直接查看文件提交commit.根据commit id 复制的分支,与原有分支一模一样,因此可安全使用。而其它操作例如合并分支,提交分支,由于过于简单,在此不做介绍,若不懂之处,可自行百度,在此不做过多介绍。

 

关于ThreadLocal

一、概述

ThreadLocal虽与thread相似,然而它并不是一个Thread,而是线程局部变量,顾名思义,它就是为每一个使用该变量的线程都提供一个变量值的副本,其底层为弱引用map,它是Java中一种较为特殊的线程绑定机制,可以简单的理解为以当前线程为key的map(与真正实现稍有差异)。在多线程情况下,每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。与锁机制相比,使用ThreadLocal消耗了内存,节约了cpu,属于以空间换时间的应用。

从thread的角度看,每个thread都保持一个对其threadlocal副本的引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在thread完成之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用,而关于何时GC,可自行百度java四类引用)。而通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多thread环境常出现的并发访问问题提供了一种隔离机制。那么ThreadLocal是如何做到为每一个thred维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个ThreadLocalMap,用于存储每一个线程的变量的副本。

二、API说明

ThreadLocal()
创建一个线程本地变量。

T get()
返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。

protected T initialValue()
返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。而在程序中一般都重写initialValue方法,以给定一个特定的初始值。若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。

void remove()
移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。

void set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。

三、实例

package concurrent;

import java.util.concurrent.atomic.AtomicInteger;
public class ThreadLocalDemo {
      public static void main(String []args){
             for(int i=0;i<5;i++){
            final Thread t = new Thread(){
           @Override
           public void run(){
          System.out.println(“当前线程:”+Thread.currentThread().getName()+”已分配 ID:”+ThreadId.get());
        }
   };
         t.start();
   }
}
static class ThreadId{
//一个递增的序列,使用AtomicInger原子变量保证线程安全
      private static final AtomicInteger nextId = new AtomicInteger(0);
//线程本地变量,为每个线程关联一个唯一的序号
      private static final ThreadLocal<Integer> threadId =
      new ThreadLocal<Integer>() {
     @Override
    protected Integer initialValue() {
         //相当于nextId++,由于nextId++这种操作是个复合操作而非原子操作,会有线程            //安全问题(可能在初始化时就获取到相同的ID,所以使用原子变量
        return nextId.getAndIncrement();
  }
};
//返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,后面看源码就了解了
       public static int get() {
          return threadId.get();
     }
   }
}
运行结果:
当前线程:Thread-4,已分配ID:1
当前线程:Thread-0,已分配ID:0
当前线程:Thread-2,已分配ID:3
当前线程:Thread-1,已分配ID:4
当前线程:Thread-3,已分配ID:2
这个程序通过妙用ThreadLocal,既实现多线程并发,游兼顾数据的安全性。

四、总结

ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。

ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。

ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。

五、ThreadLocal使用的一般步骤

1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

六、threadlocal内存泄漏
强引用:内存不足时,对象不会被GC回收,对象为null时,可被GC回收;
软引用:内存不足时,对象可以被GC回收;
弱引用:GC发现了,就会被回收,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作;
虚引用:虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。而其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。同时,虚引用创建的时候,必须带有ReferenceQueue

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread –> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。但JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。