JVM内存模型
Sun在2006年将Oracle JDK开源最终形成了Open JDK项目,两者在绝大部分的代码上都保持一致。JVM的内存模型是围绕着原子性(操作有且仅有一个结果)、可见性(racing thread读取变量的值永远是最新的)和有序性(指令的执行时有序并且符合happen-before原则的)这三个特性建立的,运行时数据区构成如下:
线程隔离区域:虚拟机栈(Java方法执行时的栈帧,存储本地变量和外部引用),本地方法栈(Native Java方法执行时的栈帧)和程序计数器(保存当前线程的下一条执行地址以及线程恢复地址)。
线程共享区域:方法区(存储被加载的Java类信息,final和static变量,JIT编译代码)和堆(存放所有对象实例)。
前者的空间耗用量在编译时期就是确定的,当然也可能出现stackOverFlowError错误;后者主要由于堆的存在,所以空间耗用量只有在运行时才能确定,可能出现outOfMemoryError错误,也是GC算法主要的作用对象,使用heap的时候如果每个线程都去申请分配空间,则空间分配器可能成为并发的瓶颈,因此JDK引入了线程私有分配缓冲区(Thread Local Allocation Buffer)的概念,线程在创建的时候就会一次性向heap申请一整块空间用于自身对象的创建,运行时如果这块空间不够则会动态申请更多的空间。
由于CPU的计算速度远远超过内存的读写速度,因此内存中的值一般需要提前预载到CPU高速缓存内等待使用;但这样的设计在多CPU架构下会引入高速缓存不一致的问题,也就是不同CPU操作的内存的值可能不是基于最新的值;解决这个问题先后出现了两种方案,一种是总线锁机制,也就是一个CPU在使用某个内存值的时候会阻塞其他CPU对这个内存值的使用,这样的设计极大降低了多CPU带来的性能提升;另一种是缓存一致性协议(Intel的MESI协议),核心思想是当CPU写数据时如果发现变量是多CPU共享变量,则会发送signal通知其他CPU他们持有的变量值已经过期,需要重新从内存中读取。
Volatile是JVM提供的最轻量级的同步机制,当一个变量被volatile修饰后,racing thread在读取volatile变量的时候会先将main内存的值同步到工作内存,然后再进行操作,同时会禁止JVM优化策略带来的指令重排(Instruction Reorder),因此能保证总是最新的值,同样更新变量的值也会立即写回main内存,以保证变量在多个线程之间的可见性;当前thread对普通变量的修改发生在Thread工作内存中,需要到达某个同步点才会写入main内存中,因此不能保证其他线程的可见性。但这并不意味着volatile能解决数据一致性问题,racing thread在修改volatile值的时候并不能保证操作的原子性,因为操作过程中有可能是基于过期的数据进行操作。
JVM内存模型只能保证最基本的读取和赋值具有原子性(32位系统下,64bit数据的读写和赋值有可能不能保证原子性),volatile可以保证单个变量值的可见性和有序性;如果需要更大范围的原子性/可见性/有序性,比如一系列的代码操作,则需要引入Lock或者Synchronized机制。一个利用volatile和synchronized保证单例的sample code,可以在保证多线程安全的前提下最大程度保证并行效率。
1 class App1 { 2 private volatile static App1 instance = null; 3 4 private App1() { 5 } 6 7 public static App1 getInstance() { 8 if (instance == null) { 9 synchronized (App1.class) { 10 if (instance == null) { 11 instance = new App1(); 12 } 13 } 14 } 15 return instance; 16 } 17 }
GC回收算法策略
GC算法如何判断对象是否可被回收,引用计数算法和可达性分析(Reachability Analysis)算法,前者通过计算对象是否至少被一个其他对象引用,无法解决循环引用的问题;后者通过一系列称为GC roots的对象作为起始点(如Object对象),从这些节点开始往下遍历,当一个对象没有被任何基于GC roots的引用链连接时,可以判定这些对象是可以被回收的,可以解决循环引用问题。GC roots对象包含:方法调用栈中引用的对象,方法区中类静态变量和常量变量引用的对象。
当前商业JVM的GC算法一般都会选择分代收集算法(Generational Collection)。由于总有一部分对象需要长期存在,一部分对象仅存在很短时间,所以首先将可用heap划分成两个部分Senior和Junior,Junior又划分成Survivor1和Survivor2;初始的时候仅在Survivor1上分配内存,一段时间后将Survivor1上存活的对象规整复制到Survivor2上,这时候使用Survivor2分配内存,然后一段时间后将Survivor2上存活的对象规整复制到Survivor1上,如此循环反复;经过几次循环之后将还存活的已经被复制过多次的对象复制到Senior上,表示长期保存。一段时间后对于Senior也可以经过标记,回收,规整等GC回收策略。
GC一般分成minor GC和full GC,前者代价较小,一般仅发生在Junior区,后者可能导致整个JVM停顿,是对Senior的规整。GC的执行时间点也会选择安全点(SafePoint),也就是选择即将发生如方法调用、循环执行、异常处理等重复指令或者长时间执行等待的时间点,GC同样提供安全区域(SafeRegion),也就是JVM判定在一定指令执行范围内不会有对象reference的变化。因此在构建服务的时候需要避免创建内存耗用大但生命周期短的对象(容易频繁触发GC),一台具有较好配置的物理机适合分割成多台虚拟机,这样可以最大程度减少单台虚拟机在full GC的时候对服务造成的影响。
JVM类加载策略
JVM类加载一般分为5个阶段:加载,验证,准备,解析,初始化。
加载阶段主要使用类加载器(Class Loader)通过一个类的全限定名来获取描述此类的二进制字节流,对于任意一个类,都需要加载它的Class Loader和类本身一起确立它在JVM中的唯一性,也就是说每一个Class Loader都拥有一个独立的类命名空间,即使同一个类,被不同Class Loader加载,JVM也认为是不同的类。绝大部分Java程序都会用到3种系统提供的类加载器:
启动类加载器(Bootstrap):加载$JAVA_HOMElib
扩展类加载器(Extension):加载$JAVA_HOMElibext
应用程序类加载器(Application):加载用户类路径上的类,getSystemClassLoader()可以获取
类加载器之间保持着一种称为双亲委派模型(Parents Delegation Model)的组合关系,也就是一个ClassLoader准备加载一个类的时候会优先给其父加载器发送一个加载请求,如此递归到启动类加载器,仅当父加载器指定的路径找不到对应的类,子加载器才会在自己负责的类路径上查找。这样设计最大的好处在于天然保持了一种带有优先级的类层次关系,也就是说越是核心系统提供的类越是优先使用,可以最大程度保证系统的稳定性,java.lang.ClassLoader.loadClass()中实现。当然JVM也允许用户自定义的ClassLoader,主要目的是系统提供的ClassLoader的class路径在系统启动的时候就是固定的,如果需要加载其他路径(网络class字节流)的class文件,则需要自定义ClassLoader。
验证阶段是为了确保class文件的字节流复合当前JVM的执行需求,包含文件格式验证,元数据验证,字节码验证和符号引用验证,主要验证是否满足method area的存储结构要求。准备阶段是为类static变量和全局变量在method area分配内存和设置初始值。解析阶段是JVM将常量池中的符号引用转换成直接引用。初始化阶段是为类成员变量设置初始化值,执行statick代码块,调用构造函数
常用的JDK自带工具
% jps:查看当前系统运行的JVM运行进程和对应的PID,仅显示各JVM内main()入口的主类。
% jstat -gc 95987 250 20:查看指定PID的JVM的GC收集情况,每250毫秒看一次,总计看20次
% jinfo 95987:查看并调整指定PID的JVM实例的运行参数。
% jmap 95987:生成指定PID的JVM的堆转存储快照(Heapdump)
% jhat 95987:将堆转存储快照生成HTML文件,配合jmap使用
% jstack 95987:生成JVM当前时刻的线程快照(Threaddump),也就是方法堆栈集合
% JConsole:是一款图形界面的JVM运行状态查看工具。
% VisualVM: