1.为什么Java具有可移植性而C++没有?
- 可移植性:指的是代码一次编写,处处运行。即相同的代码可以在linux、windows或者mac上运行且结果一致;
- JVM实现:我们知道很多JVM的native方法由C++实现,但是对不同系统,甚至32,64位都有不同的jdk版本,是不是也正好证明了不同的系统需要编写不同的C++代码来实现,而Java框架则不需要选择系统或版本。
- 根本原因:暂时没想到
2.jvm与os的关系?
jvm相当于在os上又构建了一个小型操作系统,只为运行Java程序,对程序的资源请求等提供统一的接口。JVM提供了一套自己的字节码指令集,类似于汇编码,JVM执行时会将字节码解释为机器码,调用系统的指令集完成程序运行。
Java有自己的操作栈,iadd时,取出栈顶数据,相加 -> 机器码是将内存数据load进寄存器,相加得到结果,存会操作栈。
istore->将栈顶结果保存到局部变量表。
3.jvm内存模型
堆区和方法区(jkd9元数据区)为线程共有区域,虚拟机栈、本地方法栈和程序计数器为线程私有;
其中虚拟机栈是核心,里面最主要的是局部变量表和操作栈,执行运算时将局部变量表中数据load进操作栈,执行字节码指令,然后store回局部变量表;
4.jvm的垃圾回收
主要针对堆区,堆区又分为新生代和老年代,新生代中又有eden区和两个Survivor区,垃圾回收算法主要有,标记删除,复制算法,标记整理
5.各种垃圾回收算法
方法区,或者说永久代,也是有垃圾回收的,主要回收废弃常量和无用的类。
最基础的算法时“标记-清除”,主要问题是1,.效率不高(标记和清除),2.产生内存碎片。
“复制”算法:将内存分为大小相等的两块,每次使用一块。用完了将存活对象复制到另一块,原来的清理掉。简单高效,但是内存只有一半在使用。
“标记-整理”算法:当存活对象较多时,复制算法效率也不高,所以老年代一般不能使用这种算法。根据老年代的特点,提出了标记-整理,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
“分代收集”:根据对象存活周期不同将内存划分为几块。一般新生代和老年代。新生代选用复制算法,老年代用“标记-清除”或“标记-整理”。
垃圾收集器:
1.Serial:新生代的单线程收集器。收集时会“stop-the-world”,优点是简单高效。
2.ParNew:Serial的多线程版,除Serial外唯一能和CMS搭配的收集器,默认使用计算机所有核,可以限制其收集线程数。
3.Parallel Scavenge:新生代收集器,也是复制算法,控制吞吐量,运行用户代码时间/(用户代码时间+垃圾回收时间)
4.Serial Old:Serial的老年代版,也会STW,但是使用“标记-整理”算法。
5.Parallel Old:是Parallel Scavenge的老年代版,使用多线程和“标记-整理”算法。
6.CMS(concurrent mark sweep):基于“标记-清除”算法,有四个阶段,初始标记,并发标记,重新标记和并发清除,其中初始标记和重新标记仍然会STW。初始标记是标记GC Roots能直接关联到的对象,速度很快。并发标记是tracing的过程,并发标记则是修正并发标记时用户变动,这个时间要比初始标记长,但远比并发标记时间短。
CMS缺点:1.对CPU资源敏感,核数少时会影响用户程序吞吐;2.无法处理浮动垃圾,即并发清理时用户线程产生的垃圾;3.标记清除算法会产生内存碎片;
7.G1收集器:1.7后期开始商用。具有特点如:1.并行与并发2.独立分代收集,不需要其他收集器合作3.无碎片。整体看是基于标记整理,局部看是基于复制;4.可预测停顿,用户可指定时间段内垃圾收集消耗时间。
G1是跟踪各个Region里垃圾堆积的价值大小,在后台维护一个优先队列,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage-First名称的由来)。
6.如何看GC日志
21120825.117: [GC pause (G1 Evacuation Pause) (young), 0.0170627 secs]
[Parallel Time: 11.2 ms, GC Workers: 28]
[GC Worker Start (ms): Min: 21120825118.0, Avg: 21120825118.4, Max: 21120825118.8, Diff: 0.9]
[Ext Root Scanning (ms): Min: 0.8, Avg: 1.2, Max: 1.8, Diff: 1.1, Sum: 33.6]
[Update RS (ms): Min: 1.5, Avg: 1.8, Max: 2.2, Diff: 0.7, Sum: 51.0]
[Processed Buffers: Min: 1, Avg: 8.3, Max: 19, Diff: 18, Sum: 232]
[Scan RS (ms): Min: 1.9, Avg: 2.2, Max: 2.4, Diff: 0.5, Sum: 61.6]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Object Copy (ms): Min: 4.9, Avg: 5.0, Max: 5.1, Diff: 0.2, Sum: 140.6]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.8]
[Termination Attempts: Min: 1, Avg: 3.5, Max: 6, Diff: 5, Sum: 98]
[GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 3.4]
[GC Worker Total (ms): Min: 10.0, Avg: 10.4, Max: 11.0, Diff: 1.0, Sum: 291.0]
[GC Worker End (ms): Min: 21120825128.7, Avg: 21120825128.8, Max: 21120825128.9, Diff: 0.2]
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.8 ms]
[Other: 5.0 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.5 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.4 ms]
[Humongous Register: 0.9 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 2.5 ms]
[Eden: 2018.0M(2018.0M)->0.0B(2022.0M) Survivors: 30.0M->26.0M Heap: 3101.8M(4096.0M)->1081.3M(4096.0M)]
[Times: user=0.29 sys=0.00, real=0.02 secs]
第一行数字21120825.117表示GC发生的时间,也就是JVM启动以来经过的秒数。 最后一行的Times中user, sys,real和Linux的time命令输出的时间含义一致,分别代表用户态消耗的CPU时间,内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。
7.一些GC参数
SurvivorRatio:新生代Eden与Survivor比例,默认=8,即 8:1:1
PretenureSizeThreshold:直接晋升老年代的对象大小
MaxTenuringThreshold:晋升老年代的对象年龄
UseAdaptiveSizePolicy:动态调整Java堆中各个区域的大小及进入老年代的年龄;
HandlePromotionFailure:是否允许分配担保失败,即老年代剩余空间不足以应付新生代整个Eden和Survivor区所有对象都存活的极端情况
ParallelGCThreads:并行GC时进行内存回收的线程数
GCTimeRatio:GC时间占总时间的比例,默认99,仅在Parallel Scavenge时有效
MaxGCPauseMillis:GC最大停顿时间。仅在Parallel Scavenge时有效
CMSInitiationOccupancyFraction:CMS在老年代空间被使用多少后触发垃圾回收。默认68%
UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。
CMSFullGCsBeforeCompaction:设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。
8.性能监控与故障处理
给系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里的数据包括:运行日志、异常堆栈、GC日志、线程快照和堆转储快照等。 借助tool.jar类库里的接口,可以在应用程序中实现功能强大的监控分析。
名称 | 主要作用 |
jvm process status tool,显示指定系统内所有的hotspot虚拟机进程 | |
jvm statistics monitoring tool,用于收集hotspot虚拟机各方面的运行数据 | |
configuration info for java,显示虚拟机配置信息 | |
memory map for java,生成虚拟机的内存转储快照(heapdump文件) | |
jvm heap dump browser,用于分析heapmap文件,它会建立一个http/html服务器让用户可以在浏览器上查看分析结果 | |
stack trace for java ,显示虚拟机的线程快照 |
其中,jstat是运行期定位虚拟机性能问题的首选工具。它可以显示本地货远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jinfo的作用是实时查看和调整虚拟机的各项参数。
jmap用于生成堆转储快照。还可以查询finalize执行队列,java堆和永久代的详细信息,如空间使用率、当前用的是那种收集器等。
jhat与jmap搭配使用,来分析jmap生成的堆转储快照。
jstack生成线程快照,是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环等。
8.Java字节码
可以通过javap指令查看class的字节码内容
javap -verbose TestClass
java虚拟机的解释器可以使用下面的伪代码表示
do{
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作数;
if(字节码存在操作数)从字节码流中取出操作数;
执行操作码所定义的操作;
}while(字节码流长度>0);
11 JVM类加载机制
首先需要区分类对象和实例对象两个概念。类对象,指的是我们写的类的Class对象,通过clinit方法生成;调用它的构造方法,可以得到实例对象,即我们常说的Object。
Java语言中,类型的加载、连接和初始化都是在程序运行期间完成的。
类从加载到卸载经历的阶段有加载、验证、准备、解析、初始化、使用和卸载。其中初始化指的是类的初始化,五种情况下必须初始化,例如1.实例化该类的代码;2.使用反射调用该类;3.初始化一个类时,父类未初始化;4.虚拟机启动时main类;5.static方法调用时;
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这时候进行内存分配的仅包括类变量(static修饰的,而实例变量会在对象实例化时分配在堆上);其次这里的初始值是各种数据类型的零值(真正的赋值是在初始化阶段)。
解析
初始化是类加载过程的最后一步,才真正开始执行类中定义的Java程序代码。初始化过程是执行类构造器<clinit>方法的过程,
<clinit>()方法是编译器自动收集类中的所有类变量和静态语句块中的语句合并产生的。
如果类中没有静态语句块,编译器可以不为这个类生成clinit方法。
类加载器是Java语言的一项创新,也是Java流行的重要原因之一。
对任意一个类,都需要有加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。 (namespace),唯一性来判断“相等”,包括类对象的equals, isAssignableFrom, isInstance等方法用到。
启动类加载器,扩展类加载器,应用类加载器。使用“组合”来复用父类加载器的代码。
启动类加载器加载的路径是rt.jar,不同类加载器加载类的路径不同,双亲委派模型使得不管是哪个类加载器来加载rt.jar包中的Object类,最终都是由启动类加载器来加载,所以整个Java项目中只有一个java.lang.Object对象。
12.虚拟机字节码执行引擎
“虚拟机”是一个相对“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的;而虚拟机的执行引擎是自己实现的。因此可以自行制定指令集与执行引擎的结构体系。
这是代码执行过程。
分为解释执行和编译执行两种执行方法。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。栈顶的帧是当前线程有效的帧,与之对应的方法称为当前方法。执行引擎的所有字节码指令都只针对当前栈帧。
操作数栈与局部变量表一样,最大深度在编译的时候写入到code属性中。
方法退出的过程实际上就等同于把当前栈帧出栈,同时:恢复上层方法的局部变量表和操作数栈,将返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
class编译过程中不包含传统编译中的连接步骤,一切方法调用在CLass文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址。这给Java带来了更强大的动态扩展能力。甚至到运行期间才能确定目标方法的直接引用。
13 指令集
Java编译器输出的指令流,基本上是一种基于栈的指令集,而常用的操作系统上的指令集则是基于寄存器的指令集。
比如 Java 1+1的指令集会是
iconst_1 // 将常量1放入栈中
iconst_1 //
iadd // 取出栈顶两个元素相加,结果放回栈顶
istore_0 // 取出栈顶元素,放回到局部变量表第0个slot中
而基于寄存器的1+1的指令集会是:
mov eax, 1 // 将eax寄存器值设置为1
add eax, 1 // 将eax寄存器值加1,结果保存在eax寄存器中
基于栈的指令集主要的优点是可移植,寄存器由硬件直接提供,程序如果直接依赖硬件寄存器则不可避免地要受到硬件约束。 对jvm来说,用户程序不会直接使用寄存器,由虚拟机实现来自行决定把一些访问最频繁的数据(PC,栈顶缓存等)放到寄存器中获取尽量好的性能,这样实现也更加简单一些。 栈架构的指令集还有其他优点,比如代码更紧凑,编译器实现更简单。
基于栈的指令集的主要缺点是运行相对慢,另一个是代码虽然紧凑,但是指令数量多,因为有频繁的出入栈操作。更重要的是,频繁的内存访问,造成执行的速度极大的受内存访问速度的限制。 因此,指令数量和内存访问是基于栈指令运行速度慢的主要原因。
14 类的字节码
包括魔数,大小版本号,常量表,字段表,方法表,属性表(每个表前面必须有表中item数量),其他还有access属性,接口属性等。
常量池的计数从1开始(比如22表明有21个常量),为了在特定情况下需要表达出“不引用任何一个常量池项目”的含义,这时将索引位置为0;而其他表的计数均是从0开始。
常量池存放两大类常量:字面量(Literal)和符号引用(Symbolic References),字面量就是Java的常量,符号引用则属于编译原理方面的概念,包括:类和接口的全限定符,字段的名称和描述符,方法的名称和描述符。(这些符号引用需要经过运行期转换)
常量池的每一项常量都是一个表。,这些表的共同特点是,第一位是一个u1类型符号位(256,现在有21种表),
- 比如类名表的组成是u1标志位,和u2的名称索引,指向类名的常量索引。
- 字符串的表组成是u1标志位,u2长度位,length个bytes;u2最多65535
属性表,在Class文件,字段表,方发表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。一些属性中常用的部分有:code,位于方发表,Java代码编译成的字节码指令;ConstantValue,字段表,final关键字定义的常量值;Deprecated,类、方发表、字段表,被声明为deprecated的方法和字段;Exceptions,方发表,方法抛出的异常;EnclosingMethod,类文件,仅当一个类为局部类或者匿名类时才能拥有这个属性。。。还有其他10多项
15 字节码指令
- 加载和存储指令,用于将数据在栈帧中到局部变量表和操作数栈中来回传输。如,iload,lload,istore,lstore,bipush,sipush等。
- 运算指令,用于对两个操作数栈上到值进行某种特定运算,分整形和浮点型,iadd,ladd,fadd,dadd,isub,imul,idiv,irem,ineg,ishl,ior,iand,ixor等。
- 类型转换指令,将两种不同数值类型进行相互转换,一般用于用户代码中的显式转换。i2b,i2c,l2i等。
- 对象创建与访问指令,对象和数组使用了不同的指令,如new, newarray, getfield,baload,数组元素加载,arraylength, instanceof等。。
- 操作数栈管理指令,就像操作普通数据结构中到栈一样,如pop,pop2, dup, dup2, swap栈顶互换。
- 控制转移指令,可以让Java虚拟机有条件或无条件地从指定位置指令,而不是控制转移指令的下一条指令继续执行指令。类似与goto吧。如条件分支,ifeq,iflt,ifne等,复合分支,tableswitch等。无条件分支goto,goto_w等
- 方法调用和返回指令,如invokevirtual, invokeinterface, invokespecial, invokestatic, invoedynamic等。。
- 异常处理指令,athrow,
- 同步指令,monitor实现。