深入理解JVM之Java运行时环境以及对象的的分配问题

主要感谢Java性能调优以及实战Java虚拟机和深入理解java虚拟机这几本书的作者。

2 深入理解JVM底层

2.1 编译、加载过程

13457029-44937d615905e943
image

2.2 java运行时环境

1) 方法区(Method Area)

主要存储已经被虚拟机加载的类信息、常量、静态变量、即使编译器编译的代码等数据。虚拟机规范中说这块区域是个逻辑部分。称之为Non-Heap(非堆)。为了和堆区分开。注意这里只是逻辑上堆分开,物理层面上其实还是存储在堆中。

方法区中可以使用不连续的内存和可以选择固定大小或者可扩展。如果方法区无法满足内存分配需求或者超过了指定的最大内存,就会抛出OutOfMemory。

很多人愿意将方法区称之为永久区,原因是由于方法区中可以是用参数-XX:MaxPermSize设置内存大小进行动态扩展之外它不会像堆区中的数据一样频繁的被GC执行。而且我们还可以指定是否需要在程序运行时回收方法区中的数据。所以我们将其称之为永久区的原因。

这里有几个问题:

  • 1:实现方法区的细节不属于虚拟机规范约束,所以不同的虚拟机实现中对于方法区的实现是不同的。

  • 2:在Hotspot中逐步采用Native Memory来实现方法区的规划。在jdk1.7中将原来存储到方法区中的字符串常量池已经移除了。在jdk1.8中,已经去除了方法区,而是通过元空间代替,元空间是直接使用本地内存的。

方法区是线程共享的

2) 堆(Heap)

Java的引用传递的实现依靠的就是堆内存,同一块堆内存空间可以被不同的栈内存所指向。虚拟机规范中定义堆:所有的对象和数组的空间分配都是在堆中的。

随着JIT技术的成型,栈上分配,标量替换等优化技术的成型,所有对象不一定全部都分配在堆中。比如阿里基于OpenJDK深度定制的TaoBaoJVM,使用的GCIH(GC invisible heap)实现了off-heap。将生命周期过大的对象从heap中移动到了heap之外。这样既可以提高GC的回收频率也可以提高GC回收效率。

堆内存中的空间在物理上是可以不连续的。主要逻辑上是连续的就可以。在java虚拟机规范中指定的既可以是固定大小也可以是可扩展的(主流)。如果堆中的空间无法扩展,将会抛出OutOFMemoryError错误。

目前大多数jvm采用的回收算法都是按代手机,分为新生代(Eden、Survivor01、Survivor02)、老年代。

虚拟机栈也是线程共享的。

3) 虚拟机栈(VM Stack)

是程序的运行单位,里面存储的信息都使与当前线程有关的内容, 包括:局部变量、程序的运行状态、方法返回值。这里的内容会存储到栈桢中,随着方法的调用完毕,栈桢也会随着出栈。大多数场景下所说的栈其实都是虚拟机栈中的局部变量表。

在java虚拟机规范中规定了虚拟机栈中的局部变量表存储的内容是在编译器就决定了,基本数据类型除了double、long栈两个局部变量空间,其他都只占一个局部变量空间。对象引用(可能是句柄也可能是对象的起始地址不一定是对象本身)和returnAddress类型也会存储到局部变量表中。

在java虚拟机规范中,定义了如果线程请求栈的深度大于虚拟机所允许的深度,则会抛出StackOverFlowError。大部分虚拟机都支持动态扩展,如果扩展时没有申请到足够的内存,则会抛出OutOfMemoryError异常。

虚拟机栈也是线程私有的。

PS:在java虚拟机规范中,returnAddress是唯一一个虚拟机规范和java规范中没有匹配的数据类型。占一个字节,是虚拟机内部的原始数据类型。表示的是一条字节码执行的操作码。jdk1.7之前这个类型用于finally字句的实现。

4) 本地方法栈(Native Method Stack)

管理本地方法的调用。本地方法不是java实现的,而是通过c实现的。当调用一个本地方法时,就会进入一个全新的不会再受到虚拟机限制的环境。此时它和虚拟机具备同样的能力。可以直接从本地内存的对重分配任意数量的内存,甚至可以直接调用本地处理器中的寄存器。这里注意本地方法栈和方法栈的区别,本地方法栈不会像虚拟机栈一样,调用时,会将一个新的栈桢存储到java栈中,它不会在线程的java栈中压如新的桢,而是通过简单的动态链接直接指定本地方法。

PS:注意,在SUN的Hotspot中,不区分本地方法栈和虚拟机栈,所以和虚拟机栈一样,本地方法栈也会抛出Stack Overflow和OutofMemory。

5) 程序计数器(Program Counter Register)

是一个非常小的内存空间,这个空间主要是进行一个计数的操作,对象的晋升问题(依靠的就是计数器)。字节码解释器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复登记处功能都需要依赖计数器。该内存中主要存储的是下一条指令的需要执行的地址,所以是线程私有的,因为每一条线程都需要一个自己特点的计数器来确定下条指令的存储地址,不然cpu切换工作之后,再次切换回来就不确定要执行那条指令了。这块内存是唯一一个在虚拟机规范会没有定义OutOfMemoryError的区域。类似物理机中的寄存器。

2.3 为什么要研究JVM底层

目前的内存分配以及回收策略都已经很成熟了。我们为什么还需要去了
解GC和内存分配呢?就是为了排查内存溢出、内存泄漏问题时以及系统
达到更高并发量瓶颈的时候,我们需要对于这些进行监控和调节。
13457029-04e91c4c34eae442.png
image

1) 执行步骤

  • 1: 一个对象被创建之后首先会被分配到Eden区域。(这里要注意,如果是一个过大的对象,则会执行分配到老年代中,关于什么样的对象分配到老年代稍后介绍)

  • 2: 当进行一次GC操作时(这里发生的是Minor GC),会将Eden区域90%以上的对象进行回收。然后会将对象放入到S0区域。

  • 3:当再进行一次GC操作时,Eden区域没有回收的对象以及S0中的对象会被复制到S1区域,其实就是保证S0和S1这两块区域中一定有一个区域是空置的状态。

  • 4:每个对象创建之后都会存在一个年龄计数器。如果对象在Eden出生之后经过第一次Minor GC之后仍然存活,那么会被移动到Survivor0中,且设置对象的年龄为1.每经过一次Minor GC后年龄就增加一岁。当增加到15岁是就会被谨慎到老年代。(通过-XX:MaxTenuringThreshold)

  • 5:如果超过15对对象或者是大对象则会直接被分配到老年代。大对象一般情况下指的就是很长的字符串以及数组。应该避免使用大对象,不然会导致内存还有不少空间则会提前触发GC保证有连续空间存储这些大对象。哪些又臭又硬的对象,这些对象都已经经历了无数次的 GC 之后依然被保留 下来的对象。于是这些对象很难被清除。但是有可能也会被清除。同时如果是一 个很大的对象,那么默认的也会直接保存到老年代,如果现在老年代空间不足了, 会出现 MajorGC(FullGC),进行老年代的清理(这样的清理是非常耗费性能的), 所以这也是为什么不去使用 System()方法。

2) 常见的注意点

  • 1: 虚拟机不会永远都等到对象的年龄超过之后才会将对象放入到老年代,如果Survivor空间中相同年龄对象的大小大于Survivor空间的一般,那么年龄大于或等于该年龄的对象直接进入到老年代。

  • 2: 在发生MinorGC之前,虚拟机会检查老年代的最大可用连续空间是否大于新生代对象的总空间,如果条件成立,证明MinorGC是安全的。

  • 3: 如果不成立,就需要查看是通过(HandlePromotionFailure)是否开启,如果开启则证明是可以允许担保的,会查看老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于执行MinorCC反之则执行FullGC。

  • 但是第三步的操作是存在问题的,如果某次MinorGC的之后出现了大量对象幸存突增,而老年代不存在这个大的连续空间,则此时会出现担保失败,触发FullGC。

  • 3: 在jdk7之后,只要老年代的连续空间大于新生代对象总大于或者历次晋升的平均大小就会进行MinorGC,否则会执行FullGC。

  • 4: 为了加快内存的分派,我们通过BTP(bump-the-pointer)和TLAB(Thread Local Allocation Buffer)线程本地缓存操作。

BTP:指针碰撞:假设对象分配的堆内存空间是绝对规整的,所有用过的内 存都被放到一边,空闲的内存放到另外一边,中间放着一个指针来作为分界 点的指示器,那么所分配内存就是仅仅把那个指针指向空闲空间的那边挪动一段域对象相等的距离。注意指针碰撞技术是分配给Eden去的新对象,这些对象位于Eden区的顶部。后续有对象创建,只需要检查Eden是否有足够大的空间存储该对象,如果空间够用,那么放置在Eden区域,所以我们只需要检查最后被添加的对象,是否有足够的空间就可以了。但是对于多线程情况下的,则需要进行加锁操作,此时性能就会下降。

TLAB:允许每个线程在自己的Eden区域有一个自己的私有空间,每个线程只能访问自己的TLAB,所以这个区域甚至可以是用无锁的指针碰撞技术进行内存分配。如果堆内存不是规整则就无法是用BTP。

3) 一些常见名词的对比

  • 1 不同GC名称 >从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 >Minor GC。java对象大多数具备朝生夕灭的特性,所以MinorGC很频繁。

Major GC 是清理永久代

Full GC 是清理整个堆空间—包括年轻代和永久代

PS:总结一下,我们应该避免出现FullGC,而Major GC的触发一般都是
由于Minor GC导致的。
  • 2 不同的jvm的标准

Jave 是一个开源的编程语言,实际上在世界的技术公司里面有三个所谓的虚拟机标准:

SUN(被 Oracle 收购了):所推出的 JVM 是基于HotSpot标准的虚拟机;

BEA(被 Oracle收购了):JRockit;

IBM(曾经打算收购 SUN 公司):JVM's、J9。 Oracle不可能花费额外费用去维护两个虚拟机标准,所以未来的发展趋势: HotSpot + JRockit,而现在所使用的 JVM 实际上也全部都是 HotSpot 标准

猜你喜欢

转载自blog.csdn.net/weixin_34174105/article/details/87490173