之前的关于OOM异常的学习笔记,强迫自己整理了异常及对应的参数。本片学习笔记梳理下相关背景知识。
一背景:
作为java码农,对于常见的编码,编译,执行比较熟悉了。更加关注框架跟业务实现,但是回头想想,当我们执行java命令后究竟发生了什么,就是我们通过JVM与机器交互,Java通过使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java具备了一次编写,多处运行的特性。

JVM主要由 类加载器子系统、运行时数据区(内存空间)、执行引擎以及与本地方法接口 等组成。
由此可以看出,类加载器把.class文件加载入内存,供以后实例化。此处与后面的内存区域有关。内存空间又跟java内存模型有关,更是牵扯出垃圾回收,OOM异常,内存参数优化等。 执行引擎牵扯到指令优化就是JMM相关。所以jvm既是难点又是重点,理解了这些,才会有豁然开朗的感觉。当然我目前还在学习中,好多不理解的。比如沿着一个思路,同步背后的锁,再底层的aqs,在底层的CAS,再底层的CPU实现机制。可以一层层的熟悉,跟操作系统有关的了。好了,扯远了。
二 JVM运行时数据区
 好了,现在看看笔记整理的重点。

上图分为两个部分:线程私有区(pc计数器、栈、本地方法栈 ),线程共享区(方法区、堆)。下面分别介绍。
  2.1程序计数器(The pc Register)
JVM一次能支持很多线程执行。每一个JVM线程有它自己的程序计数器。PC寄存器里保存有当前正在执行的JVM指令的地址。注意:( 一个JVM的线程都正在执行当前线程的方法代码。如果这个方法不是本地( native )方法,程序计数器包含当前被执行的JVM地址。如果线程正在执行本地( native )方法,程序计数器的值为未定义。)上面这句话只是给出结果,不是很理解。

2.2栈(JVM Stacks)

每个JVM的线程在创建的时候,都会创建一个栈。一个栈包含很多栈桢。JVM的栈好比传统语言C的栈,它维持(存储)本地变量和部分结果,并在方法调用和返回中(被)使用。这个栈是一个后进先出的数据结构,所以当前正在执行的方法在栈的顶端,每当一个方法被调用时,一个新的栈帧就会被创建然后放在了栈的顶端。当方法正常返回或者发生了未捕获的异常,栈帧就会从栈里移除

2.2.1 栈帧(stack frame

JVM为每个方法调用创建一个新的栈帧并推到每个方法调用的栈顶。当方法正常返回或者遇到了未捕获的异常,这个栈帧将被移除。

每个栈帧包含了:局部变量表、返回值、操作数栈、当前方法所在的类的运行时常量池引用。见下图


 2.2.2 局部变量表(Local variable array)

局部变量表包含了这个方法执行期间所有用到的变量,包括this引用,所有方法参数以及其他的局部声明变量。对于类方法(比如静态方法)来说,所有方法参数下标都是从0开始,然而,对于实例方法来说这个0是留给this的。对于对象,局部变量区中永远只有指向堆的引用。

2.2.3操作数栈(Operand stack)

方法实际运行的工作空间。每个方法都在操作数栈和局部变量数组之间交换数据,并且压入或者弹出其他方法返回的结果。

2.2.3动态链接(帧数据)

每个栈帧都包含了运行时常量池的引用。这个引用指向了这个栈帧正在执行的方法所在的类的常量池,它对动态链接提供了支持。

局部变量区和操作数栈的大小依照具体方法在编译时就已经确定。调用方法时会从方法区中找到对应类的类型信息,从中得到具体方法的局部变量区和操作数栈的大小,依此分配栈帧内存,压入Java栈。

栈基本概念结束了,可以结合之前总结OOM异常,

 JVM说明书(规范)允许栈要么是一个固定大小,要么动态扩展来满足计算的要求。如果JVM栈是一个固定的大小,当栈被创建的时候每一个栈大小可以自由设置。 在动态扩展情况下,可以控制最大最小内存。 在VM Spec中对这个区域规定了2种异常状况(以下两种异常与JVM的栈机制有关):

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
如果VM栈可以动态扩展,在初始化新线程时没有足够内存创建栈则抛出OutOfMemoryError异常。

栈容量只由-Xss参数设定

2.3本地方法栈(Native Method Stacks)
通俗说就是供用非Java语言实现的本地方法的堆栈 并不是所有的JVM都支持本地方法。不过那些支持的通常会创建出每个线程的本地方法 栈。如果提供本地方法栈,每个线程创建时必须分配一个本地方法栈。是不是可以理解为java调用了C代码,就创建一个C栈?待确认哈。

2.4堆(heap)
J VM有一个在所有线程内共享的堆。 堆在虚拟机启动的时候创建, 堆是给所有类的实例和数组分配内存的运行时数据区。 数组和对象可能永远不会存储在栈上,因为一个栈帧并不是设计为在创建后会随时改变大小。栈帧仅仅保存引用,这个引用指向对象或者数组在堆中的位置。与局部变量表(每个栈帧里)中的基本数据类型和引用不同,对象总是被存储在堆里,所以他们在方法结束后不会被移除,仅仅在垃圾收集的时候才会被移除。堆中储存的对象通过一个自动存储管理系统(垃圾回收器)进行回收。 对象从不明确的被分配(JVM从不指明对象的释放)。
插一句: 堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。
为了提高GC效率,从jd k1.2开始将堆内存做分代(Generation)处理。

通常他们的工作流程如下:

新对象和数组被分配在年轻代。

年轻代会发生Minor GC。 对象如果仍然存活,将会从eden区移到survivor区。
Major GC 通常会导致应用线程暂停,它会在2个区中移动对象,如果对象依然存活,将会从年轻代移到老年代。
当每次老年代进行垃圾收集的时候,会触发持久代带也进行一次收集。同样,在发生full gc的时候他们2个也会被收集一次。

2.4.1年轻代

 HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法。

年轻代有关参数。

1)-XX:NewSize和-XX:MaxNewSize

用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio

用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

3)-XX:+PrintTenuringDistribution

这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

2.4.2 老年代

存放那些在历经了Eden区和Survivor区的多次GC后仍然存活下来的对象。

这里有个对象分配的优化,可以结合OOM异常的demo来看。

对于小对象的分配,会优先在线程私有的 TLAB (Thread Local Allocation Buffer)中分配(因为在堆上分配内存需要锁定整个堆,而在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配,以提高效率),TLAB中创建的对象,不存在锁甚至是CAS的开销。TLAB占用的空间在Eden Generation。

当对象比较大,TLAB的空间不足以放下,而JVM又认为当前线程占用的TLAB剩余空间还足够时,就会直接在Eden Generation上分配,此时是存在并发竞争的,所以会有CAS的开销,但也还好。当对象大到Eden Generation放不下时,JVM只能尝试去Old Generation分配,这种情况需要尽可能避免,因为一旦在Old Generation分配,这个对象就只能被Old Generation的GC或是FullGC回收了。

堆有关参数:-Xms -Xmx -Xmn 其中:old=Xmx-Xmn

2.5方法区(Method Area)

也被称为非堆区域(在HotSpot JVM的实现当中)

JVM的方法区是所有线程共享的,方法区类似于传统语言编译代码时的存储区域或类似于操作系统进程的文本段。他存储内容包括:每一个类的结构,如运行时常量池,字段和方法的数据;方法和构造器的代码,如用于类,实例和接口初始化的特殊方法。这个方法区在JVM启动的时候被创建,一般情况下JVM不会选择对方法区进行垃圾回收或者压缩.在HotSpot JVM里,方法区被称为永久区或者永久代(PermGen)。

所有线程都共享同样的方法区,所以访问方法区的数据和动态链接的过程都是线程安全的。如果两个线程尝试访问一个类的字段或者方法而这个类还没有加载,这个类就一定会首先被加载而且仅仅加载一次,这2个线程也一定要等到加载完后才会继续执行。

2.5.4 运行时常量池(Runtime Constant Pool)

运行时常量池是类和接口运行时的常量池表,它在字节码文件里。它包含几类常量。 在编译时期识别的数值常量,在运行区识别的方法或引用字段。运行区常量池类似于传统语言的字符表,但它比传统字符表所存储的范围更广。每一个运行区常量池从方法区分配内存。当类和接口被JVM创建时相应的常量池也被创建。换句话说:当一个方法或者变量被引用时,JVM通过运行时常量区来查找方法或者变量在内存里的实际地址。

几种在常量池内存储的数据类型包括:

数量值、字符串值、类引用、字段引用、方法引用

它可以通过-XX:PermSize及-XX:MaxPermSize来进行调节。可以结合之前OOM异常学习笔记来看。

运行区常量池包括以下异常:

  • 当类和接口创建时,如果运行区常量池所需内存不足,则抛出OutOfMemoryError。

拓展知识点:

1.从jdk8开始。持久代已经被元空间(Metadata )取代。

它是本地堆内存中的一部分
它可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来进行调整
当到达XX:MetaspaceSize所指定的阈值后会开始进行清理该区域
如果本地空间的内存用尽了会收到java.lang.OutOfMemoryError: Metadata space的错误信息。
和持久代相关的JVM参数-XX:PermSize及-XX:MaxPermSize将会被忽略掉。

2. 代码缓存——在JVM里,Java字节码被解释运行,但是它没有直接运行本地代码快。为了提高性能,Oracle Hotspot VM会寻找字节码的”热点”区域,它指频繁被执行的代码,然后编译成本地代码。这些本地代码会被保存在堆外内存的代码缓存区。Hotspot用这种方式,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。编译后的代码就是本地代码(硬件相关的),它是由JIT(Just In Time)编译器生成的,这个编译器是HotSpot JVM所特有的。

*********************************总结********************************************
JVM真是博大精深啊,本次学习笔记属于OOM引发的JVM内存背景梳理。
与之相关的有GC,参数优化,并发与内存模型,甚至还有String这样神奇的对象,一篇写不完,分开整理。就到这里吧

参考并发编程网:
http://ifeve.com/jvm-runtime-data/
http://ifeve.com/jvm-yong-generation/
http://ifeve.com/jvm-permgen-where-art-thou/
http://ifeve.com/jvm-internals/
http://blog.hesey.net/2011/04/introduction-to-java-virtual-machine.html


Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐