建立了一个技术类公众号: 一块儿源码分析,里面会分享最新的开源代码、源码解读、开发技巧等,欢迎你们关注。 java
JVM已是Java开发的必备技能了,JVM至关于Java的操做系统。python
JVM,java virtual machine, 即Java虚拟机,是运行java class文件的程序。web
Java代码通过Java编译器编译,会编译成class文件,一种平台无关的代码格式,class文件按照jvm规范,包括了java代码运行所需的元数据和代码等内容。jvm加载class文件后,就能够执行java代码了。算法
JVM有不一样的实现,有咱们熟悉的Hotspot虚拟机,JRockit等。在各个操做系统上,又回有各自的虚拟机实现,从而造成了Java代码 > class文件 > JVM规范 > JVM实现的层次。再加上其余语言如scala、groovy也可以生成class文件,这样不只实现了平台无关性,也实现了语言无关性。编程
JVM体系,分为JVM内存结构,Class文件结构,Java ByteCode,垃圾收集算法和实现,调优和监控工具,以及Java内存模型(JMM)。 vim
<!-- more -->数组
一般,认为大概分为线程共享的区域和线程私有的区域。共享区域在JVM启动时建立, 私有区域伴随这线程的启动和结束。缓存
一个线程拥有的结构有安全
Java天生支持多线程,多线程会有线程切换的问题,当一个线程从可运行状态获得CPU调度进入运行状态,CPU须要知道从哪里开始执行,而且Java是一种基于栈的执行架构(区别于基于寄存器的架构)。网络
当执行一个Java方法时,PC会指向下一条指令的位置。执行native方法时,PC是未定义。操做指令可能会有0个或多个操做数。JVM的执行流程大概能够描述为:
while(true) { opcode = code[pc]; oprand = getOperan(opcode); pc = code[pc + len(oprand)]; execute(opcode, oprand); }
Java虚拟机栈,或者叫方法栈,会伴随这方法的调用和返回进行相应的入栈和出栈。栈的元素是栈帧(Stack Frame), 栈帧中的内容包括: 操做数栈,本地变量表,动态连接等信息。当线程调用一个方法的时候,会组装对应的栈帧入栈。
本地变量表存储方法的参数、方法内部建立的局部变量。本地变量表的大小在编译时就肯定了。本地变量表会根据变量的做用范围选择重用一个位置。本地变量表会存放 int,char,byte,float,double,long,address(实例引用)。其中除了double和long其余变量占用一个slot,一个slot指一个抽象的位置,在32位虚拟机中是32bit大小, double和long占用两个slot。 值得注意的时,若是一个方法是实例方法,Java编译器会将this做为第一个参数传入本地变量表。另外Java中面向对象,方法调用能够这样理解
实例方法 obj.method(var1, var2, var3) => method invoke obj var1 var2 var3
操做数栈用于方法内执行保存中间结果,Java方法中的代码逻辑就是经过操做数栈来实现的。和本地方法表同样,操做数栈也是在编译时就肯定最大大小了,即最大深度。操做数栈能够和本地变量表交互,进行数据的存放和读取。下面用一个简单的例子展现一下。
int add(int a, int b) { return a + b; }
这个实例方法通过Java编译器编译后生成的字节码
本地变量表 slot0 this slot1 a slot2 b 方法字节码 iload_1 #读位置是1的本地变量(本地变量表从0开始,位置0是this引用) 此时操做数栈是 a iload_2 #读位置是2的本地变量,即b 此时操做数栈是 a b iadd #进行int类型的add操做,会取出栈头的两个元素取出进行相加并将结果入栈。 此时操做数栈是 c (相加的结果) ireturn #ireturn指令会将栈头元素返回给调用方法的栈帧
建立的对象(包括普通实例和数组)都分配在Heap区(不考虑一些虚拟机的栈上分配优化技术)。在细分的话,通常还分红年轻代和老年代。这是基于这样一个相似28原理的统计,90%多的对象都是很快成为垃圾的对象。因此化为成两个区域,分别使用不一样的收集算法,年轻代的收集频率更高,所需空间也相对较小。内存分配时,多个线程会有并发问题,主要经过两种方式解决:1.CAS加上失败重试分配内存地址。2. TLAB, 即Thread Local Allocation Buffer, 为每一个线程分配一块缓冲区域进行对象分配。年轻代还能够分为两个大小相等的Survivor和一个Eden区域。对象在几种状况下会进入老年代:1. 大对象,超过Eden大小或者PretenureSizeThreshold. 2. 在年轻代的年龄(经历的GC次数)超过设定的值的时候 3. To Survivor存放不下的对象
方法区存放加载的类信息和运行时常量池等。
在Java应用中不须要也不能经过代码对内存进行手动释放,JVM中的垃圾器帮助咱们自动回收没有程序引用的对象。除了进行内存释放,JVM还需对内存进行整理,由于有内存碎片的问题。GC的优势是加快开发效率,不须要关心内存释放,而且避免了不少内存安全问题。缺点是会带来性能损耗。 GC必需要作两件事情,找出垃圾对象和回收它们的内存。
通常来讲,当某个区域内存不够的时候就会进行垃圾收集。如当Eden区域分配不下对象时,就会进行年轻代的收集。还有其余的状况,如使用CMS收集器时配置CMSInitiatingOccupancyFraction设置何时触发Old区的回收。
即判断一个对象再也不使用,再也不使用能够是没有有效的引用。 通常来讲,主要有两种判断方式
当有对象引用自身时,就会计数器加1,删除一个引用就减一,当计数为0时便可判断为垃圾。python等语言使用引用计数。引用计数存在循环引用问题,如两个落单的A和B互相引用,可是没有其余对象指向它们这种状况.
经过一些根节点开始,分析引用链,没有被引用的对象均可以被标记为垃圾对象。根节点是方法栈中的引用、常量等。根节点集合和具体的实现相关,可是会包括: 线程栈帧中的本地变量和操做数栈中的对象引用,静态变量、常量以及已经加载的类的常量池中的队形引用等。全部可以经过引用链引用到的对象都被认为是活对象。 JVM中广泛使用的是可达性分析。
对非垃圾对象进行标记都,清除其余的对象。这种方式对对内存空间形成空隙,即内存碎片,最终致使有空余空间,但没有连续的足够大小的空间分配内存。
标记非垃圾对象后,将这些对象整理好,依次排列内存。这样内存就是整齐的了。可是由于会形成对象移动,因此效率会有下降。
即组合两种方式,在若干次清除后进行一次整理。
划分红两个相同大小的区域,收集时,将第一个区域的活对象复制到另外一个区域,这样不会有内存碎片问题。可是最多只能存放一半内存,并且全部的活对象都须要拷贝。
为了保证明际GC过程当中对象的一致性,GC每每须要停顿全部的Java应用线程,也就是常说的StopTheWorld。 目前主流的虚拟机能够知道哪一个位置保存着对象引用,在HotSpot中,经过OopMap的数据结构在快速的GC Root枚举。 安全点(Safe Point): 程序并不是在全部时刻都能停顿下来开始GC,只有到达安全点才能暂停。安全点知识程序可能长时间执行的可能的指令,例如方法调用、循环跳转、异常跳转等。发生GC时须要让全部线程停下来,有抢先式中断和主动式中断两种方式。为了解决主动式中断线程一直不响应中断请求的问题,又引入了安全区域(Safe Region)的概念,安全区域是在一段代码片断之中引用关系不会发生变化,线程离开安全区域时,要检查系统是否已经完成了根节点枚举,若是没有则一直等待。
垃圾收集器就是垃圾收集算法的相应实现。 在大多数的应用中,有基本能统计到如下的现象:
新生代单线程的收集器,是Client模式默认的垃圾收集器
Serial New的多线程版本。ParNew常和CMS拉配使用。这里说明一些Parallel和Concurrent即并行和并发在垃圾收集这里的表示的不一样,并行表示有多个线程同时进行垃圾收集,并发是指垃圾收集线程和应用线程能够并发执行。
PS收集器是注重吞吐量(ThroughPut)的收集器。
老年代的单线程收集器
Serial Old的多线程版本,因为Parallel Scavenge不能和CMS搭配使用,因此会是使用PS时的一种选择。
注重延迟latency的收集器,在交互式应用中,如面向用户的web应用,须要尽量减小垃圾收集形成的停顿时间。在总的统计上,吞吐量可能没有PS收集器高。 细分上,CMS还分为4个阶段
具备大内存收集和目标效率时间等控制能力,目标是代替CMS。G1经过将内存划分红不一样的区域(Region),并对不一样区域计算分数,分析那个Region最具备收集价值。
经常使用的参数设置有
Nio中的DirectByteBuffer就是堆外内存的一部分,这部份内存只能经过Full Gc进行清理。一些框架会经过System.gc调用手动触发gc,可是在启动参数中可能设置了禁止调用System.gc()。另外当设置堆过大时可能会形成堆外内存不够致使OOM。
监控工具帮助咱们在运行时或问题发生后分析现场,分析内存分布状态,哪里致使内存泄漏等(本该被释放的对象仍然被引用)。
HotspotJVM的bin目录下有不少可用的工具。
jps jps -l jps -lv
即java版的ps,能够查看当前用户启动了哪些java进程。
pid指jps命令查看的java进程号
jstat -gcutil pid 1000 10
jstat是一个多种用途的工具,更多须要man jstat或直接输入jstat查看提示。
jmap能够查看内存情况
jmap -histo:live pid jmap -dump:file=dump.bin,format=b,live jmap -dump:file=dump.bin,format=b dump下来的内存文件能够经过MAT进行分析,经过分析引用链等分析内存泄漏位置
查看Java线程情况
jstack pid jstack -F pid 能够查看线程的状态、名称、代码位置
javap 能够用可读的方法查看class文件内容,在遇到线上class文件问题,如NoSucheMethodError发生时,能够快速进行判断分析。如分析一个A.class文件,查看它的私有方法和字段。
javap -p -c -v A.class
$JAVA_HOME/bin/jvisualvm
$JAVA_HOME/bin/jmc
$JAVA_HOME/bin/jconsole
Java编译器将Java代码编译成class文件格式。 其中步骤包括了咱们熟悉的词法分析将源文件转换成token流。语法分析将token流转换成抽象语法树(AST)。语义分析分析语义是否正确。源代码优化。目标代码生成和目标代码优化等步骤。最终获得了class文件。以后在虚拟机中,class文件能够经过解释器解释执行和经过即时编译器(JIT-just in time)编译成native代码执行两种方式执行。 class文件是有严格定义的。符合定义的class文件才可以被JVM加载、验证、初始化、执行。 咱们经过javap能够查看一个class文件的内容。 Class文件能够分为如下几个部分
下面以一个简单的类
public class Inc { public static void main() { } private int count; public void inc() { count++; } }
看一下它的class文件,经过vim打开,在Normal模式下,按: 输入%!xxd,便可转换成16进制表示。而后能够经过%!xxd -r转换回来
0000000: cafe babe 0000 0034 0013 0a00 0400 0f09 .......4........ 0000010: 0003 0010 0700 1107 0012 0100 0563 6f75 .............cou 0000020: 6e74 0100 0149 0100 063c 696e 6974 3e01 nt...I...<init>. 0000030: 0003 2829 5601 0004 436f 6465 0100 0f4c ..()V...Code...L 0000040: 696e 654e 756d 6265 7254 6162 6c65 0100 ineNumberTable.. 0000050: 046d 6169 6e01 0003 696e 6301 000a 536f .main...inc...So 0000060: 7572 6365 4669 6c65 0100 0849 6e63 2e6a urceFile...Inc.j 0000070: 6176 610c 0007 0008 0c00 0500 0601 0003 ava............. 0000080: 496e 6301 0010 6a61 7661 2f6c 616e 672f Inc...java/lang/ 0000090: 4f62 6a65 6374 0021 0003 0004 0000 0001 Object.!........ 00000a0: 0002 0005 0006 0000 0003 0001 0007 0008 ................ 00000b0: 0001 0009 0000 001d 0001 0001 0000 0005 ................ 00000c0: 2ab7 0001 b100 0000 0100 0a00 0000 0600 *............... 00000d0: 0100 0000 0100 0900 0b00 0800 0100 0900 ................ 00000e0: 0000 1900 0000 0000 0000 01b1 0000 0001 ................ 00000f0: 000a 0000 0006 0001 0000 0003 0001 000c ................ 0000100: 0008 0001 0009 0000 0027 0003 0001 0000 .........'...... 0000110: 000b 2a59 b400 0204 60b5 0002 b100 0000 ..*Y....`....... 0000120: 0100 0a00 0000 0a00 0200 0000 0700 0a00 ................ 0000130: 0800 0100 0d00 0000 0200 0e0a ............
经过javap来看一下它的结构
javap -v -p -c -s -l Inc Classfile /Users/liuzhengyang/study/java/Inc.class Last modified Oct 6, 2016; size 315 bytes MD5 checksum 770dcaa972162765744184ffc14bc3c6 Compiled from "Inc.java" public class Inc minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#15 // java/lang/Object."<init>":()V #2 = Fieldref #3.#16 // Inc.count:I #3 = Class #17 // Inc #4 = Class #18 // java/lang/Object #5 = Utf8 count #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 inc #13 = Utf8 SourceFile #14 = Utf8 Inc.java #15 = NameAndType #7:#8 // "<init>":()V #16 = NameAndType #5:#6 // count:I #17 = Utf8 Inc #18 = Utf8 java/lang/Object { private int count; descriptor: I flags: ACC_PRIVATE public Inc(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void main(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=0, locals=0, args_size=0 0: return LineNumberTable: line 3: 0 public void inc(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field count:I 5: iconst_1 6: iadd 7: putfield #2 // Field count:I 10: return LineNumberTable: line 7: 0 line 8: 10 } SourceFile: "Inc.java"
bytecode保存在class文件的方法的Code属性中。用一个byte表示操做指令,因此最多有256个指令。一个指令可能会有多个操做数。 操做指令能够分为如下几类:
现代计算机的一本基本思想是分层模型,例如网络上的分层。在存储上,为解决CPU和内存磁盘的速度有指数级差异的问题加入了不少缓存,利用局部性原理加快速度,从CPU寄存器到L1Cache、L2Cache、内存、磁盘,各个层的速度依次下降、空间增大、单位bit造价下降。最近CPU的处理能力的垂直增长彷佛遇到瓶颈,转而向多核方向发展,多个cpu核可能各自缓存本身的内容,又出现了缓存一致性问题。CPU有一些缓存一致性协议,MESI等。CPU还可能会对机器指令进行乱序执行。JVM为了屏蔽底层的这些差别,提出了Java内存模型,即JMM(Java Memory Model),来保证Write One Run Anywhere。开发者面向JMM编程,经过JMM提供的一致性保证和工具,就能保证一致性问题。 JMM模型中,每一个线程会有一个私有的内存区域用于缓存读和写,各个线程共享一个主内存。 一个重要的概念是happen-before原则。 happen-before用来描述两个操做的偏序关系,若是Ahappen-beforeB,那个A的操做的结果、产生的影响可以被B看到。 若是咱们有两个动做x和y,咱们记hb(x,y)为x happen before y JMM提供的基础的happen-before规则有
happen-before并不要求在以前发生,只需可以看到操做的结果便可,对应的实现能够进行重排序或消除锁,只要保证外观正确。
以上的总结梳理权当抛砖引入,帮助你们梳理知识结构,更多细节还需经过查看源码、亲自探索,真像就在那代码中。并且每一个知识点又可以引出一篇笔记分析,以后后补充更多细节文章。
博客地址: liuzhengyang