何为Java虚拟机?

Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境。

JVM是在计算机操作系统内存上开辟的一块区域,逻辑上又给JVM运行时数据区分成了如下五个部分,如下图。

这里要提及的是,黄色部分为线程私有数据,不会有线程安全问题;绿色部分为线程共享数据,可能会引发线程安全问题。

如何查看java虚拟机路径 java虚拟机在哪里_JVM


图中元素的关系:如图,Java源文件首先在硬盘上经过编译,成 .class(字节码)文件之后,加载进内存,然后由执行引擎(CPU)分配时间调度去执行。

线程运行

假设现在有两个线程(如下图)要运行,我们可以看看其中的内存情况。

如何查看java虚拟机路径 java虚拟机在哪里_jvm_02


解释一下:每创建一个线程,都会在JVM运行时数据区对应位置分配一小块位置给他,这些位置总的加起来就构成了这个线程的内存分配状况。我们看图可以知道:在“ 栈”, “本地方法栈”, “程序计数器”这三块位置分别都分配了一小块位置。(这三块位置都是线程私有的,我好像明白了线程私有的另一层含义)

现在用main线程来举个例子

假设有这么一段代码要执行:

class HelloWorld{
public int add(){
	int a = 1;
	int b = 2;
	int c = (a+b) * 100;
	return c;
}

public static void main(String[] args){
	HelloWorld app = new HelloWorld();
	int result = app.add();
	System.out.println(result);
}

}
  • :main方法压栈运行,途中 add() 方法压栈运行,add()方法运行完毕出栈,main方法运行完毕出栈,运行结束。

栈的细节图:

如何查看java虚拟机路径 java虚拟机在哪里_JVM_03


      宏观的来说,就是上面说的那个样子,但是细说,栈里面的情况其实里面还有上图那么多细节。这里就稍微讲一下,因为涉及了太多的汇编语言太复杂了。

      情况就是,javap命令对字节码文件进行反汇编生成汇编命令(如下图),JVM运行下图这些命令对成员变量进行一系列操作,这些操作就会用到上图的 ”局部变量表“, ”操作数栈“, ”方法出口“。

如何查看java虚拟机路径 java虚拟机在哪里_jvm_04


附图一张看看这些命令的含义:(下图中的栈指的是上上图中的”操作数栈“)

如何查看java虚拟机路径 java虚拟机在哪里_jvm_05


对于局部变量表和方法出口:add方法的方法出口是个int值(也就是返回值),代码中又是int result = app.add();这么写的,所以这个add方法的返回值又会被存到main方法的局部变量表中。(不懂就看上面那个栈的细节图)

  • 本地方法栈::JVM会分配一块叫”本地方法栈“的区域去存用 Native关键字修饰的方法。当然我写的这个例子中没有体现,记住就好了。
  • 程序计数器执行当前线程所执行的字节码指令的(地址)行号。其实有了上上图(就是黑色的那个cmd命令的那个图),程序计数器就很好理解了,它就是图中每个命令前面的那个序号(也叫行号)。它就是用来告诉执行引擎(CPU)应该运行序号对应的那一句指令。(每执行完一句指令程序计数器就会+1)
  • 线程共享区域,堆和方法区:为了方便,我用我之前截的图,但是这应该也不影响对堆和方法区的理解?:

图的解释:(当然这里面写的堆栈方法区和上面写的是一一对应的!)

  1. 首先编译成.class文件加载进方法区;
  2. 然后主方法压栈并运行;
  3. 对象创建在堆中完成(如图);
  4. 方法调用如图中绿色地址进行查找,找到对应方法即马上压栈运行,运行结束立即出栈。

堆底层的划分(JDK1.8中取消了持久代,取而代之的是元空间)

如何查看java虚拟机路径 java虚拟机在哪里_如何查看java虚拟机路径_06

  • 老年代占堆内存的三分之二,新生代占三分之一;
  • 新生代中的内存占比是 Eden:from:to = 8:1:1
由阿里面试题“为什么Java需要性能调优?”引发的讨论

产生的问题:物理内存会随着JVM分配内存的增大最后而被耗尽。

如何查看java虚拟机路径 java虚拟机在哪里_如何查看java虚拟机路径_07

变成 |          |


如何查看java虚拟机路径 java虚拟机在哪里_java_08


于是引入“回收内存”:即JVM分配的内存达到一个临界值,就对JVM的内存进行压缩,其实压缩的过程中就是性能调优。如下图:

如何查看java虚拟机路径 java虚拟机在哪里_JVM_09

即:“在有限的空间做无限的事情”

Java是怎么进行性能调优的?(垃圾回收)

如何查看java虚拟机路径 java虚拟机在哪里_如何查看java虚拟机路径_10

  1. 新生代:(包括:Eden区、Survivor from区、Survivor to区)
  • 所有对象创建在新生代的Eden区,当Eden区满时会进行一次Minor GC操作将Eden区进行回收,同时会有一个根的可达性判定GC Roots(其中有个可达性分析算法),GC Roots判断存活(存活是指还在被调用)的对象会被复制进入Survivor from区(同时年龄加1),其他对象就回收,这时Eden区清空;之后又可以在Eden区创建对象,满了又进行Minor GC(如果from区有非存活对象这个时候也是直接回收),所有存活对象年龄又加一,一直反复。对于Minor GC十五次之后依然存活的对象直接晋升进入老年代,实际上是为了保证Eden区具有充足的空间可用的一种策略
  • 保证一个Survivor区是空的(为了防止碎片化,即Eden区和from区中的对象内存地址不连续),新生代Minor GC就是在两个Survivor区之间相互复制存活对象,直到Survivor区超过一半为止,对于年龄等于十五的对象直接进入老年代,实际上是对Eden区到Survivor区过度的一种策略,是为了保证Eden区到Survivor区不会频繁的进行复制一直存活的对象且对Survivor区也能保证不会具有太多的一直占据的内存

晋升的条件:1. 对象年龄等于十五; 2. from区对象占用超过百分之五十。

  1. 老年代:当Survivor区也满了之后就通过Minor GC将对象复制到老年代 (在发生MinorGC之前,JVM会判断之前每次晋升到老年代的平均大小是否大于老年代剩余空间的大小,若大于则进行full GC) 。老年代也满了的话,就将触发Full GC,针对整个堆(包括新生代、老年代)进行垃圾回收。
  • Full GC有个问题:触发Full GC会有一个STW(Stop-The-World)现象的出现。
  • Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

Java性能调优:让更少的对象进入老年代,减少STW的次数就比如,正常情况设置新老年代大小比例为1:2,特殊情况比如临时对象创建频繁,需要适当增大新生代大小还可以根据JDK自带的VirtualVM进行Java程序的性能分析

垃圾回收

GC策略解决了哪些问题?

哪些对象可以被回收。

  1. 引用计数算法:此对象有一个引用,则+1;删除一个引用,则-1。只用收集计数为0的对象。但是无法处理循环引用的问题。
  2. 根搜索算法(GC Roots——GC根):这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点(),从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

什么对象可作为GCRoot的对象?——基本分为两大类:全局对象和执行上下文;

  1. 全局对象:
  • 方法区静态属性引用的对象:全局对象的一种,Class对象本身很难被回收,回收的条件也是很苛刻,只要Class不被回收,静态成员不会被回收
  • 方法区常量池引用的对象:全局对象,比如字符串常量池,常量初始化之后不会再次改变
  1. 执行上下文对象:
  • 方法栈的栈帧本地变量表引用的对象:线程方法执行的时候,会将方法打包成一个栈帧入栈执行,方法里得到的局部变量会存放到本地变量表中,只要方法未执行完,还没出栈,即本地变量表还会被访问,GC不应该回收
  • JNI本地方法栈引用的对象:和上面同样的道理
  • 被同步锁持有的对象:被synchronized锁住的对象不可回收,否则锁就失效了,那锁就没意义了

如何查看java虚拟机路径 java虚拟机在哪里_jvm_11

何时回收这些对象?不可达的对象一定会回收吗?(缓刑阶段)

其实被判定为 不可达的对象,也不一定是”非死不可“的,还有一次复活机会,这时是处于缓刑阶段,要真正宣告一个对象死亡,至少要经历再次标记过程(其实就是finalize方法在搞怪)

标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链

  • 第一次标记:
    筛选的条件是这个对象是否有必要执行finalize()方法;若对象未重写这个方法或者已被虚拟机调用过,虚拟机则认为没有必要执行,对象被回收
  • 第二次标记:
    若这个对象有必要执行finalize方法,则这个对象会被放到一个F-Queue队列中,并在稍后由虚拟机自动创建的一个低优先级的finalizer线程去执行;

这里的执行指的是虚拟机会触发这个方法,但是不保证运行完成,这样做的原因是这个方法执行缓慢,也可能出现死循环,严重可能会导致回收系统崩溃。这种方法是不靠谱的,也是不建议使用的

finalize是对象逃脱死亡命运的最后一次机会,稍后GC会对F-Queue中的对象进行二次标记,如果在这里面重新和GC Roots挂上引用关系,则可以逃脱被回收的命运;否则,就肯定GG了

采用什么样的方式回收。

  • 标记/清除算法:如上图,先标记,再清除,但是会产生内存碎片。标记和清除操作效率都低。
  • 复制算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。但是很浪费内存不是吗?
  • 标记/整理算法:和标记/清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是移动所有存活的对象,且按照内存地址次序依次排列(存活和非存活对象交换位置),然后将末端内存地址以后的内存全部回收。但是效率还是低,不如复制算法。