浅谈jvm

简单的介绍一下JVM(Java Virtual Machine)吧,它也叫Java虚拟机。虽然它叫虚拟机,但是实际上不是我们所理解的虚拟机,它更像操作系统中的一个进程。JVM屏蔽了各个操作系统底层的相关的东西,Java程序只需要生成对应的字节码文件,然后由JVM来负责解释运行。

介绍几个容易混淆的概念,JDK(Java Development Kit) 可以算是整个Java的核心,其中有编译、调试的工具包和基础类库,它也包含了JRE。

JRE(Java Runtime Environment),包含了JVM和基础类库。而JVM就是我们今天要聊的主角,开篇聊到,JVM负责解释运行,它会将自己的指令映射到当前不同设备的CPU指令集上,所以只需要在不同的操作系统上装不同版本的虚拟机即可。这也给了Java跨平台的能力。

JVM的发展
就跟我们用三方库一样,同样的功能有不同的实现。JVM也是一样的,第一款JVM是Sun公司的Classic VM,JDK1.2之前JVM都是采用的Classic VM,而之后,逐渐被我们都知道的HotSpot给替代,直到JDK1.4,Classic VM才完全被弃用。

HotSpot应该是目前使用最广泛的虚拟机(自信点,把应该去掉),也是OpenJDK中所带的虚拟机。但是你可能不知道,HotSpot最开始并不是由Sun公司开发,而是由一家小公司设计并实现的,而且最初也不是为Java语言设计的。Sun公司看到了这个虚拟机在JIT上的优势,于是就收购了这家公司,从而获得了HotSpot VM。

运行时内存区域
可能你经历过被灵魂拷问是什么滋味,如果线上发生了OOM(Out Of Memory),该怎么排查?如果要你来对一个JVM的运行参数进行调优,你该怎么做?

不像C++可以自己来主宰内存,同时扮演上帝和最底层劳工的角色,Java里我们把内存管理交给了JVM,如果我们不能了解其中具体的运行时内存分布以及垃圾回收的原理,那等到问题真正出现了,很可能就无从查起。这也是要深入的了解JVM的必要性。

Java在运行时会将内存分成如下几个区域进行管理,堆、方法区、虚拟机栈、本地方法栈和程序计数器。


-

堆(Java Heap)是JVM所管理的内存中最大的一块了。我们平常开发中使用new关键字来进行实例化的对象几乎都会在堆中分配内存,所有线程都可以共享被分配在堆上的对象。

堆也是JVM垃圾回收的主要区域,正因为垃圾回收的分代机制,其实堆中还可以分为更细的新生代、老年代。GC这块后面会细讲。

那为什么是几乎呢?在JVM本身的规范中是规定了所有的对象都会在堆上分配内存的,但是随着JIT(Just In Time)编译器和逃逸分析技术的成熟,所有对象都在堆上分配内存就变得没有那么绝对了。

JIT编译器
不知道你有没有听说过,二八定律在我们的程序中也同样适用,那就是20%的代码占用了系统运行中80%的资源。在我们写的代码中,就可能会存在一些热点代码,频繁的被调用。除了被频繁的调用的代码,还有被执行多次的循环体也算热点代码。

那此时JIT编译器就会对这部分的代码进行优化,将它们编译成Machine Code,并做一些对应的优化。不熟悉的同学可能会说,我们的代码不都已经被编译成了字节码了吗?怎么又被编译成了Machine Code?

因为字节码只是一个中间状态,真正的运行是JVM在运行的时候,就跟解释型语言一样将字节码逐条的翻译成了Machine Code,这个Machine Code才是操作系统能够识别直接运行的指令。而JIT就会把编译好的热点代码所对应的Machine Code保存下来, 下载再调用时就省去了从字节码编译到Machine Code的过程,效率自然也就提高了。

逃逸分析
我们刚刚提到过,Java中几乎所有的对象都在堆上分配空间,堆中的内存空间是所有线程共享的,所以在多线程下才需要去考虑同步的相关问题。那如果这个变量是个局部变量,只会在某个函数中被访问到呢?

这种局部变量就是未逃逸的变量,而这个变量如果在其他的地方也能被访问到呢?这说明这个变量逃逸出了当前的作用域。通过逃逸分析我们可以知道哪些变量没有逃逸出当前作用域,那这个对象内存就可以在栈中分配,随着调用的结束,随着线程的继续执行完成,栈空间被回收,这个局部变量分配的内存也会一起被回收。

方法区
方法区存放了被加载的Class信息、常量、静态变量和JIT编译之后的结果等数据,与堆一样,方法区也是被所有线程共享的内存区域。但与堆不同,相对于堆的GC力度,这块的垃圾回收力度可以说是小了非常多,但是仍然有针对常量的GC。

虚拟机栈
虚拟机栈是线程私有的,所以在多线程下不需要做同步的操作,是线程安全的。当每个方法执行时,就会在当前线程中虚拟机栈中创建一个栈帧,每个方法从调用到结束的过程,就对应了栈帧在虚拟机栈中的入栈、出栈的过程。那自然而然,栈帧中应该存放的就是方法的局部变量、操作数栈、动态链接和对应的返回信息。

不知道你遇到过在方法内写递归时,由于退出条件一直没有达到,导致程序陷入了无限循环,然后就会看到程序抛出了一个StackOverflow的错误。其所对应的栈就是上面提到的操作数栈。

当然这是在内存足够的情况下,如果内存不够,则会直接抛出OutOfMemory,也就是常说的OOM。

本地方法栈
本地方法栈的功能与虚拟机栈类似,区别在于虚拟机栈是服务于JVM中的Java方法,而本地方法栈则服务于Native的方法。

GC
其实堆中的区域还可以划分为新生代和老年代,再分割的细一点,可以到Eden、From Survivor、To Survivor。首先分配的对象实例会到Eden区,在新生代这块区域一般是最大的,与From Survivor的比例是8:1,当然这个比例可以通过JVM参数来改变。而且当分配的对象实体很大的时候将会直接进入到老年代。

为什么要对堆进行更加细致的内存区域划分,其实是为了让垃圾回收更加的高效。

垃圾识别机制
那JVM是如何判断哪些对象是“垃圾”需要被回收呢?我们就需要来了解一下JVM是如何来判断哪些内存需要进行回收的。

引用计数
实现的思路是,给每个对象添加一个引用计数器,每当有其他的对象引用了这个对象,就把引用计数器的值+1,如果一个对象的引用计数为0则说明没有对象引用它。

乍一看是没有问题的,那为什么Java并没有采取这种呢?

想象一下这个场景,一个函数中定义了两个对象O1和O2,然后O1引用了O2,O1又引用了O1,这样一来,两个对象的引用计数器都不为0,但是实际上这两个对象再也不会被访问到了。

所以我们需要另外一种方案来解决这个问题。

可达性分析
可达性分析可以理解为一棵树的遍历,根节点是一个对象,而其子节点是引用了当前对象的对象。从根节点开始做遍历,如果发现从所有根节点出发的遍历都已经完成了,但是仍然有对象没有被访问到,那么说明这些对象是不可用的,需要将内存回收掉。

这些根节点有个名字叫做GC Roots,哪些资源可以被当作GC Roots呢?

栈帧中的局部变量所引用的对象
方法区中类静态属性所引用的对象
方法区中常量所引用的对象
本地方法栈所引用的对象
我们刚刚聊过,在引用计数中,如果其引用计数器的值为0,则占用的内存会被回收掉。而在可达性分析中,如果没有某个对象没有任何引用,它也不一定会被回收掉。

相关推荐