学习JVM-运行时数据区
之前的关于OOM异常的学习笔记,强迫自己整理了异常及对应的参数。本片学习笔记梳理下相关背景知识。一背景:作为java码农,对于常见的编码,编译,执行比较熟悉了。更加关注框架跟业务实现,但是回头想想,当我们执行java命令后究竟发生了什么,就是我们通过JVM与机器交互,Java通过使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java具备了一次编写,多处运行的特性。JVM主要由类加载器子系统
好了,现在看看笔记整理的重点。
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异常。
通常他们的工作流程如下:
新对象和数组被分配在年轻代。
年轻代会发生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将会被忽略掉。
更多推荐
所有评论(0)