JVM-part-运行时数据区

运行时数据区组成部分:

  1. 程序计数器(PC寄存器 Program Counter Register)
  2. Java虚拟机栈(Java Virtual Machine Stacks)
  3. 本地方法栈(Native Method Stack)
  4. 堆(Heap)
  5. 方法区(Method Area)

其中存在线程共享和线程不共享的区域,如下:
线程共享:堆、栈
线程不共享:每一个线程都有的,程序计数器、本地方法栈

详细解释:

1、程序计数器:

他的作用是记录每一个线程当前正在执行的字节码的内存地址,便于在线程恢复的时候恢复执行**,避免了重复执行

2、Java虚拟机栈:

它是通过数组实现的一个栈结构(栈结构的特点是先进后出),每一个线程都存在自己的Java虚拟机栈,Java虚拟机栈中的基本元素为栈帧(Stack Frame),这里的栈帧其实就是我们调用的方法,调用一个方法,就会在Java虚拟机栈中压入一个栈帧,作为当前栈帧,也就是正在执行的方法;当一个方法结束的时候,就会将对应的栈帧进行弹出,将Java虚拟机栈中的下一个栈帧作为当前栈帧

  • 栈帧的组成:
    • 局部变量表(Local Variable Table):元素索引从0开始,它是栈帧的重要组成部分之一,同时对栈帧的大小也具有决定性的作用,通常来说,栈帧中的局部变量表越大,栈帧的大小就会越大,局部变量表的作用是存储调用方法中的局部变量和方法参数,需要注意的是局部变量表的大小在编译的时候就确定了,通过javap -v Xxx.class可以进行查看

    • 操作数栈(Operand Stack):也是栈帧的重要组成部分之一,底层通过数组实现,他的作用是存储当前方法过程中的操作数,举一个例子:例如


      通过jclasslib查看

可以通过-Xss进行指定Java虚拟机栈的大小

  • 方法返回地址(Return Address):它的作用是保存了被调方法执行完成之后需要执行的代码位置(这个位置当然在调用者中),恢复调用者的代码执行
  • 动态链接(指向运行时常量池的方法引用)(Dynamic Linking):他的作用是指向运行时常量池的方法引用,也就是将符号引用解析为直接引用,举一个例子,在此之前说明一下,在Java文件编译为Class文件的时候,会将所有的变量和方法引用转化为符号引用,存放到常量池中,比如,如果需要描述一个方法调用了另一个方法,就需要使用常量池中的表示指向方法的符号引用,动态链接就是解析这些符号引用,将其转化为方法的直接引用

说到动态链接的话,就会涉及到几个调用指令,如下:

  • 普通调用指令:
    • invokestatic:调用static方法,是确定的方法,在编译期就能确定,所以是非虚方法。
    • invokesopecial:调用方法(构造器)、私有方法和通过super调用的父类方法也是确定的,在编译期间就能确定,为非虚方法
    • invokevirtual:这里需要注意,除了通过final修饰的方法为非虚方法外,其他的都为虚方法(不知掉具体的方法),其具体实现也是在运行时确定的。
    • invokeinterface:调用接口,也是虚方法,其具体实现也是在运行时确定的。
  • 动态调用指令:
    • invokedynamic:在java7出来之后添加的新指令,它允许开发人员编写更加灵活的代码,通常伴随lamble表达式出现(类型通过变量来确定)与其他调用指令不同,invokedynamic 指令的调用目标在运行时才会确定,因此它是一种更加灵活和动态的调用方式。

    • 其他信息:通常包括当前方法的状态、异常处理信息、其他信息

3、本地方法栈:

它和Java虚拟机栈的结构差不多,但是调用的方法为native修饰的本地方法,通常是通过C或者C++编写的 (支持本地方法的调用)

4、堆:

堆是运行时数据区的重要组成部分,它是由年轻代、老年代组成(可能有的帖子中还存在元数据,但是我考虑到元数据是在方法区中的,所以就没有将它放进来哈)。

  • Young/New generation 年轻代:在年轻代中又由三部分组成:
    • Eden(伊甸园区):伊甸园区通常是新对象创建并存储的地方,因为据统计有80%的对象都存在朝生夕死的特征,能使垃圾回收变得更高效,注意哈,它能触发YGC(Young GC或者叫Minor GC)
    • Survivor0(幸存者1区):它是年轻代的组成部分之一,它主要是存储经过一次或者多次垃圾回收之后任然存在的对象,注意哈,Surivor幸存者区不能触发YGC
    • Survivor1(幸存者2区):同上

幸存者区,也叫 from、to,如何判断哪一个幸存者是from或者to,这个很容易,为空的一个就是to,不为空的就是from

当一个对象来的时候,存放的流程介绍(感觉没说明白,可看图参考):
首先当来一个对象的时候,如果伊甸园区是空的或者能够存放当前的对象的时候(伊甸园区的空闲大小比当前对象的大小大),就会将该对象存入伊甸园区,如果伊甸园区无法进行存储,或者已经满了,就会触发YGC(MinorGC)来进行垃圾回收,注意哈,不管是什么GC,都会将用户线程进行暂停(Stop The World,stw),因为MinorGC的效率很高,所以MinorGC对我们的性能往往没有什么影响,那么,在MinorGC中会做些什么呢,首先会通过可达性分析算法判断在Eden中哪些对象是垃圾,如果是垃圾就会直接进行清除,如果不是垃圾就会将其放入为空幸存者区(to),然后将另一个幸存者区的对象也放入to区,并且会在每一个对象的对象头的age属性中加1,当这个age等于15的时候(默认为15,可自定义)就会将这个对象从幸存者区晋升到老年代,当MinorGC完成之后,这时的Eden区和一个幸存者区都是空的,就又能存储新对象了


  • Tenure/Old generation 老年代:老年代中通常存放的是大对象(连续内存地址,如数组)或者存活时间较长的对象,当老年代满的时候,会先触发一次Minor GC,在执行Major GC(针对老年代的垃圾回收),这里要注意下,有的帖子会将这里的Major GC称为Full GC,尽管他们两个都会回收老年代的垃圾,但是,Full GC是针对与整个堆的清理和方法区的清理,而Major GC从效果上只会将年轻代和老年代进行清理,所以还是不同的哈,接着说,如果当老年代需要存储对象时,触发了Major GC之后任然不能存储的时候,就会出现OOM

这里会有几个问题哈:

1.是不是所有在老年区的对象的age都是15?

解析:不是,因为当需要存储一个大对象的时候,如果Eden不能存储会首先执行一次Minor GC,判断Eden是否能存储这个对象,如果不能,就会考虑直接将这个对象放入老年区中,也是需要判断下是否能够存储的哈,如果不能存储就会出现OOM错误,还有一种就是当Survivor区中的to满了,Eden区还需要向to区复制对象的时候,会先触发一次Minor GC,之后在判断是否能存储,如果不能存储就会考虑将to区的对象和Eden区中需要写入to区的对象写入到老年代,但是也是需要考虑是否能存的下哈

2.我们都知道在运行时数据区中的堆和方法区是线程共享的,一个JVM只有一个,那么是不是在堆中的所有数据都是线程共享的?

解析:在堆中的Eden中还存在一种内存区域叫做TLAB(Thread Local Allocation Buffer),每个线程在堆中都有一份,默认的大小是Eden内存的1%,也可以存储对象,它的作用是在多线程并发环境下需要内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。

3.为什么要将堆中的数据进行分代,不能直接为一块吗?

解析:我们都知道,大多数的对象都是朝生夕死的,并且在每一次GC操作都是会将用户线程先暂停(STW),在进行GC操作的,这个暂停时间大小从小到大为:Minor GC < Major GC < Full GC (范围越大,时间越长),如果我们将堆内存设置为一块的话,就会频繁的调用GC,导致用户线程暂停的时间越长,就影响效率了

4.是不是所有的对象都是分配在堆中的?

解释:不是的,这里有个技术叫做逃逸技术,简单解释下就是这个对象实体会不会被其他线程使用到,如果会,就说明这个对象会进行逃逸,反之为不会逃逸,对于不会逃逸的对象,我们可以实施一种叫做栈上存储的技术,就是将这个对象存储到栈中,我们知道在Java虚拟机栈中的栈帧是当执行方法执行完成之后就会出栈,当我们将对象存到栈中,就不再需要GC进行回收,从而提高了效率。这里说到的逃逸对象,我们有多种优化方式,包括刚说的栈上存储、标量替换、同步消除,这里就不一一说明了,有兴趣可以搜搜看。

5、方法区

在JDK8之前,方法区也叫做永久代,在JDK8之后,方法区叫做元空间。

方法区在逻辑上属于堆的一部分,但是在HotSoprtJVM来说,方法区也叫做Non-Heap(非堆),并且在通过-X:Xms和-X:Xmx进行设置堆的大小的时候,并没有包含方法区的大小,所以方法区可以看作是一块独立于Java堆的内存空间。

方法区和堆一样,是线程共享的。在JVM创建的时候创建,可以自定义设置方法区的大小,方法区的大小决定了系统可以保存多少类,如果系统定义的类的总大小大于方法区的大小,就会导致方法区溢出,出现OutOfMemoryError:PermGen space或者OutOfMemoryError:Metaspace错误(取决于jdk的版本是1.7之前还是之后)

方法区的组成:

  • 类型信息:
    方法区中存储了每一个加载的类型(class,interface,enum,annatation)以下信息:
    • 这个类型的有效名称(包名+类名)
    • 这个类型的父类的有效名称(需要注意的是Object和interface没有父类)
    • 这个类型的修饰符(如public、final等)
    • 这个类型直接接口的一个有序列表
  • 域信息
    • JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
    • 域的相关信息包括:域名称、域类型、域修饰符(public、private等)
  • 方法信息
    方法区中存储了方法的以下信息:
    • 方法名称
    • 方法的返回值类型
    • 方法的参数数量及其类型(需要按照参数列表的顺序)
    • 方法的修饰符(如public、final、static等)
    • 方法的字节码
    • 异常表
  • JIT代码缓存:将即时编译器(JIT)编译之后的机器码进行保存,提高执行效率。
  • 静态变量:存放类的静态变量,这些变量在类加载的时候分配内存空间,在类卸载的时候将空间释放。
  • 运行时常量池:存储编译期间生成的各种字面量和符号引用

常量池和运行时常量池的区别:
1.常量池的存储位置在.class文件中,运行时常量池的存储位置在JVM的方法区中
2.常量池是在编译期间生成的,而运行时常量池是在类加载阶段生成的,基于编译期常量池创建
3.常量池的内容时静态的,在编译的时候就确定了,而运行时常量池的内容是动态的,允许在运行时添加新常量
4.常量池时用在.class文件在磁盘上的存储和传输,而运行时常量池的作用是在运行时动态连接和常量管理。

最后放几张图。
运行时数据区会错误的几个部分,即是否线程共享

Java运行时数据区的结构:

posted @ 2024-05-17 16:48  just1t  阅读(4)  评论(0编辑  收藏  举报