浅析`Java`虚拟机结构与机制

JVM的主要结构

类加载器子系统(Class Loader

类加载器子系统负责加载编译好的.class字节码文件,并装入内存,使JVM可以实例化或以其它方式使用加载后的类

JVM的类加载子系统支持在运行时的动态加载。

动态加载的优点有很多,例如可以节省内存空间、灵活地从网络上加载类;另一好处是可以通过命名空间的分割来实现类的隔离,增强了整个系统的安全性。

ClassLoader的分类:

  • 启动类加载器(Bootstrap class loader):负责加载rt.jar文件中所有的Java类,即Java的核心类都是由该ClassLoader加载。
  • 扩展类加载器(Extension class loader):负责加载一些扩展功能的jar包。
  • 系统类加载器(System class loader):负责加载启动参数中指定的classpath中的jar包及目录,通常我们自己写的Java类也是由该ClassLoader加载。
  • 用户自定义类加载器(User defined class loader):由用户自定义类的加载规则,可以手动控制加载过程中的步骤。

ClassLoader的工作原理

类加载分为装载、链接、初始化三步。

加载

通过类的全限定名和ClassLoader加载类,主要是将指定的.class文件加载至JVM当类被加载以后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。

在内存中,ClassLoader实例和类的实例都位于堆中,它们的类信息都位于方法区。

装载过程采用一种被称为“双亲委派模型(Parent Delegation Model)”的方式,当一个ClassLoader要加载类时,它会先请求它的双亲ClassLoader加载类,而它的双亲ClassLoader会继续把加载请求提交再上一级的ClassLoader,直到启动类加载器。只有其双亲ClassLoader无法加载指定的类时,它才会自己加载类。

双亲委派模型是JVM的第一道安全防线,它保证了类的安全加载,这里同时依赖了类加载器隔离的原理:不同类加载器加载的类之间是无法直接交互的,即使是同一个类,被不同的ClassLoader加载,它们也无法感知到彼此的存在。这样即使有恶意的类冒充自己在核心包(java.lang)下,由于它无法被启动类加载器加载,也造成不了危害

链接

链接的任务是把二进制的类型信息合并到JVM运行时状态中去。

链接分为以下三步:

  • 验证:校验.class文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM使用。
  • 准备:为类分配内存,同时初始化类中的静态变量赋值为默认值。
  • 解析(可选):主要是把类的常量池中的符号引用解析为直接引用,这一步可以在用到相应的引用时再解析。
初始化

初始化类中的静态变量,并执行类中的static代码、构造函数。

JVM规范严格定义了何时需要对类进行初始化:

  • 通过new关键字、反射、clone、反序列化机制实例化对象时
  • 调用类的静态方法时
  • 调用类的静态字段或对其赋值时
  • 通过反射调用类的方法时
  • 初始化该类的子类时(初始化子类前其父类必须已经被初始化)
  • JVM启动时被标记为启动类的类

JAVA栈(Java Stack

Java栈由栈帧组成,一个帧对应一个方法调用。调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。Java栈的主要任务是存储方法参数、局部变量、中间运算结果,并且提供部分其它模块工作需要的数据。

Java栈是线程私有的,这就保证了线程安全性,使得程序员无需考虑栈同步访问的问题,只有线程本身可以访问它自己的局部变量区。

它分为三部分:局部变量区、操作数栈、桢数据区。

局部变量区

局部变量区是以字长为单位的数组,在这里,byteshortchar类型会被转换成int类型存储,除了longdouble类型占两个字长以外,其余类型都只占用一个字长。

boolean类型在编译时会被转换成intbyte类型,boolean数据会被当做byte类型数据来处理。局部变量区也会包含对象的引用,包括类引用、接口引用以及数组引用。

局部变量区包含了方法参数和局部变量,此外,实例方法隐含第一个局部变量this,它指向调用该方法的对象引用。对于对象,局部变量区中永远只有指向堆的引用。

操作数栈

操作数栈也是以字长为单位的数组,但是正如其名,它只能进行入栈出栈的基本操作。在进行计算时,操作数被弹出栈,计算完毕后再入栈。

帧数据区

帧数据区的任务主要有:

  • 记录指向类的常量池的指针,以便于解析
  • 帮助方法的正常返回,包括恢复调用该方法的栈帧,设置PC寄存器指向调用方法对应的下一条指令,把返回值压入调用栈帧的操作数栈中
  • 记录异常表,发生异常时将控制权交由对应异常的catch字句,如果没有找到对应的catch字句,会恢复调用方法的栈帧并重新抛出异常。

局部变量区和操作数栈的大小依照具体方法在编译时就已经确定。调用方法时会从方法区中找到对应类的类型信息,从中得到具体方法的局部变量区和操作数栈的大小,依次分配栈帧内存,压入Java

本地方法栈(Native Method Stack)

本地方法栈类似于Java栈,主要存储了本地方法调用的状态。

方法区(Methods Area)

类型信息和类的静态变量都存储在方法区中。

方法区中对于每个类存储了以下数据:

  • 类及其父类的全限定名
  • 类的类型(class还是interface)
  • 访问修饰符(public,abstract,final)
  • 实现的接口的全限定名的列表
  • 常量池
  • 字段信息
  • 方法信息
  • 静态常量
  • ClassLoader引用
  • Class引用

可见类的所有信息都存储在方法区中。由于方法区是所有线程共享的,所以必须保证线程安全。举例来说,如果两个类同时要加载一个尚未被加载的类,那么一个类会请求它的ClassLoader去加载需要的类,另一个类只能等待而不会重复加载。

此外,为了加快调用方法的速度,通常还会为每个非抽象类创建私有的方法表,方法表是一个数组,存放了实例可能被调用的实例方法的直接引用。

堆(Heap)

堆用于存储对象实例以及数组值。

堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。

堆中还可能存放了指向方法表的指针。

堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。

新生代(New Generation)

大多数情况下,新对象都被分配在新生代中,新生代由Eden Space和两块相同大小的Survivor Space组成,后两者主要用于Minor GC时对象复制。

JVM在Eden Space中会开辟一小块独立的TLAB(Thread Local Allocation Buffer)区域用于更高效的内存分配。

在堆上分配内存需要锁定整个堆,而在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配,以提高效率。

旧生代(Old Generation/Tenuring Generation)

在新生代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有新生代高。

执行引擎

执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。

JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能紧凑,便于快速地在网络上传输,同时也很容易适应通用寄存器较少的平台,并且有利于代码优化,由于Java栈PC寄存器是线程私有的,线程之间无法互相干涉彼此的栈。每个线程拥有独立的JVM执行引擎实例。

JVM指令由单字节操作码和若干操作数组成。对于需要操作数的指令,通常是先把操作数压入操作数栈,即使是对局部变量赋值,也会先入栈再赋值。

解释执行

解释执行中有几种优化方式:

  • 栈顶缓存

将位于操作数栈顶的值直接缓存在寄存器上,对于大部分只需要一个操作数的指令而言,就无需再入栈,可以直接在寄存器上进行计算,结果压入操作数栈。这样便减少了寄存器和内存的交换开销。

  • 部分栈帧共享

被调用方法可将调用方法栈帧中的操作数栈作为自己的局部变量区,这样在获取方法参数时减少了复制参数的开销。

  • 执行机器指令

在一些特殊情况下,JVM会执行机器指令以提高速度。

编译执行

为了提升执行速度,SUN JDK提供了将字节码编译为机器指令的支持,主要利用了JIT编译器在运行时进行编译,它会在第一次执行时编译字节码为机器码并缓存,之后就可以重复利用了。

自适应优化执行

自适应优化执行的思想是程序中10%-20%的代码占据了80%-90%的执行时间,所以通过将那少部分代码编译为优化过的机器码就可以大大提升执行效率。

2016/3/4 posted in  Java

Java static初始化顺序

初始化顺序

class中,总是先初始化字段,字段定义的先后顺序决定了初始化的顺序,然后再初始化构造器

class Window {
  Window(int marker) { print("Window(" + marker + ")"); }
}

class House {
  Window w1 = new Window(1); // Before constructor
  House() {
    // Show that we're in the constructor:
    print("House()");
    w3 = new Window(33); // Reinitialize w3
  }
  Window w2 = new Window(2); // After constructor
  void f() { print("f()"); }
  Window w3 = new Window(3); // At end
}

public class OrderOfInitialization {
  public static void main(String[] args) {
    House h = new House();
    h.f(); // Shows that construction is done
  }
} 
/* Output:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()

static数据的初始化

加上static字段后,这个字段的拥有者不再是对象而是类。无论创建多少个对象,static数据都只有一份。

在class中总是先初始化static字段,再初始化一般字段,接着初始化构造器。

如果不创建这个类的对象,那么这个对象不会初始化;初始化也之后执行一次。

2016/3/3 posted in  Java

Java习惯用法总结

实现equals()

class Person {
  String name;
  int birthYear;
  byte[] raw;
 
  public boolean equals(Object obj) {
    if (!obj instanceof Person)
      return false;
 
    Person other = (Person)obj;
    return name.equals(other.name)
        && birthYear == other.birthYear
        && Arrays.equals(raw, other.raw);
  }
 
  public int hashCode() { ... }
}
  • foo.equals(null)必须返回falsenull instanceof 任意类总是返回false
  • 覆盖equals()时,记得要相应地覆盖hashCode()

实现hashCode()

class Person {
  String a;
  Object b;
  byte c;
  int[] d;
 
  public int hashCode() {
    return a.hashCode() + b.hashCode() + c + Arrays.hashCode(d);
  }
 
  public boolean equals(Object o) { ... }
}
  • 使用Arrays.hashCode(一个数组)

实现compareTo()

class Person implements Comparable<Person> {
  String firstName;
  String lastName;
  int birthdate;
 
  // Compare by firstName, break ties by lastName, finally break ties by birthdate
  public int compareTo(Person other) {
    if (firstName.compareTo(other.firstName) != 0)
      return firstName.compareTo(other.firstName);
    else if (lastName.compareTo(other.lastName) != 0)
      return lastName.compareTo(other.lastName);
    else if (birthdate < other.birthdate)
      return -1;
    else if (birthdate > other.birthdate)
      return 1;
    else
      return 0;
  }
}
  • 总是实现泛型版本Comparable而不是实现原始类型Comparable

实现clone()

class Values implements Cloneable {
  String abc;
  double foo;
  int[] bars;
  Date hired;
 
  public Values clone() {
    try {
      Values result = (Values)super.clone();
      result.bars = result.bars.clone();
      result.hired = result.hired.clone();
      return result;
    } catch (CloneNotSupportedException e) {  // Impossible
      throw new AssertionError(e);
    }
  }
}
  • 使用super.clone()Object类负责创建新的对象
  • 基本类型域都可以正确地复制了。同样,我们不需要去克隆StringBigInteger等不可变类型

反转字符串

String reverse(String s) {
  return new StringBuilder(s).reverse().toString();
}
  • 使用reverse()方法
2016/3/2 posted in  Java

从`ACID`到`CAP`到`BASE`

ACID

  1. Atomic原子性

事务必须是一个原子的操作序列单元,事务中包含的各项操作在一次执行过程中,要么全部执行成功,要么全部不执行,任何一项失败,整个事务回滚,只有全部都执行成功,整个事务才算成功。

  1. Consistency一致性

事务的执行不能破坏数据库数据的完整性和一致性,事务在执行之前和之后,数据库都必须处于一致性的状态

  1. Isolation隔离性

在并发环境中,并发的事务是相互隔离的,一个事务的执行不能被其他事务干扰。

  1. Durability持久性

一个事务一旦提交,它对数据库中对应数据的状态变更就应该是永久性的,即使发生系统崩溃或机器宕机,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态。

SQL中的4个事务隔离级别

  • 读未提交:允许脏读。如果一个事务正在处理某一数据,并对其进行了更新,但同时尚未完成事务,因此事务没有提交。
  • 读已提交:允许不可重复读。只允许读到已经提交的数据。
  • 可重复读:允许幻读。即同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。
  • 串行化:最严格的事务,要求所有事务被串行执行,不能并发执行。

CAP定理

一个分布式系统不可能同时满足一致性Consistency、可用性Availability、分区容错性Partition tolerance这三个基本需求,最多只能同时满足其中的两项。

一致性

分布式环境中,一致性是指多个副本之间能否保持一致的特征。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处理一致的状态。

可用性

系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回正常结果

分区容错性

分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

BASE定理

Basically Available(基本可用)、Soft State(软状态)、Eventually Consistent(最终一致性),基于CAP定理演化而来,核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

Basically Available(基本可用)

分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。

Soft State(软状态)

允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

Eventually Consistent(最终一致性)

强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

2016/3/2 posted in  数据库

初探Java字符串

字符串对象内部是用字符数组存储的

    String m = "hello,world";
    String n = "hello,world";
    String u = new String(m);
    String v = new String("hello,world");

这些语句会发生这么些事情:
1. 会分配一个长度为11的char数组,并在常量池分配一个由这个char数组组成的字符串,然后由m去引用这个字符串
2. 用n去引用常量池里面的字符串,所以和n引用的是同一个对象
3. 生成一个新的字符串,但是内部的字符数组引用着m内部的字符数组
4. 生成一个新的字符串,但是内部的字符数组引用常量池里面的字符串内部的字符数组

字符串常量通常是在编译的时候就确定好的,定义在类的方法区里面;也就是说,不同的类,即使用了同样的字符串,还是属于不同的对象。

字符串的操作细节

    String m = "hello,world";
    String u = m.substring(2,10);
    String v = u.substring(4,7);

m,n,v是三个不同的字符串对象,但引用的value数组其实是同一个。

但是字符串操作时,可能需要修改原来的字符串数组内容或者原数组没法容纳的时候,就会使用另外一个新的数组(例如replaceconcat+等操作)

    String m = "hello,";
    String u = m.concat("world");
    String v = new String(m.substring(0,2));

常量池中字符串的产生

    String m = "hello,world";
    String u = m + ".";
    String v = "hello,world.";

uv虽然是一样的内容的字符串,但是内部的字符数组不是同一个。

如果将m定义为final,编译器就会把u和v做成一样的了。

Reference

http://www.importnew.com/17034.html

2016/2/23 posted in  Java