前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java底层-运行时数据区

Java底层-运行时数据区

作者头像
每天学Java
发布2020-07-07 10:27:58
6080
发布2020-07-07 10:27:58
举报
文章被收录于专栏:每天学Java每天学Java

前面的文章中,我们了解了Javac编译器、并模拟Javac命令实现了一个MyJavac命令,然后以HotSpot为例,了解了JVM的结构、类加载器以及类加载过程, 所以这一篇文章接着类加载子系统开始对运行时数据区相关概念进行学习。

在前面关于HotSpot组成中提到,运行时数据区就类似一个工厂,是Java程序运行所在的内存区域,这个区域被JVM所管理,按照虚拟机规范的规定将其划分为:方法区、堆、程序计数器,虚拟机栈、本地方法栈五个部分(在HotSpot虚拟机中虚拟机栈和本地方法栈功能上已经合并) 其中方法区和堆在JVM实例创建的时候就开始创建且分配好内存,我们在启动程序过程中可以通过一些参数设置,比如通过-Xms、-Xmx设置堆大小, JDK7中通过-XX:PermSize、-XX:MaxPermSize设置方法区大小(永久代),JDK8通过-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N设置方法区大小(元空间), 而程序计数器,虚拟机栈,本地方法栈是在线程创建时进行分配,所以方法区、堆是线程共享的,而程序计数器、虚拟机栈、本地方法栈是和线程绑定的,是线程私有的。

注:在HotSpot中虚拟机栈和本地方法栈功能上已经合并,所以下面统称为「虚拟机栈」。

下面我们具体来看每一块区域:

方法区

在前面我们谈论类加载器的时候,我们说过类加载的目的是将 Class 文件加载到JVM的方法区中,然后在内存中实例化一个java.lang.Class的对象, 关于实例化的Class的对象存储位置有一些需要注意的地方,《深入理解Java虚拟机 第2版》书中提过实例化的java.lang.Class是存储在方法区中, 但是网上认为这种说法已经过时了,多数人认为目前在JDK8中,实例化的java.lang.Class的对象极其静态变量是存储在堆中的, 参考文章:https://blog.csdn.net/Xu_JL1997/article/details/89433916, 这里我们以网上大佬的见解为主,那么方法区中存储哪些信息呢?

类全名、父类名称、实现的接口集合、类的各种字段信息、类的各种方法信息、类的修饰符都是在方法区的,静态变量转移到堆中。

此外我们还需要记住两点:

  • 方法区既然可以设置大小,那么说明他是存在OOM的可能,只不过在JDK8中,由于永久代的移除,元空间使用的是直接内存,所以方法区OOM可能性比较低。
  • 元空间替代永久代作为方法区的实现,虽然OOM的可能性降低,但方法区仍然是GC回收的区域(一般都伴随着Full GC进行内存释放)

在Java虚拟机运行时数据区中,堆内存是各类区域中内存中最大的一块,关于堆的面试题有很多,比如JVM的堆内存是如何进行划分的、 垃圾回收对于不同的划分区域又是采用哪些算法进行回收、新生代,老年代大小比例,如何保证并发安全访问堆数据等等, 下面简单的说一下,后面在GC回收系统中重点说明。

堆内存的创建伴随着虚拟机的启动而创建。所有对象实例的创建都是在堆内存中。在Java虚拟机规范中明确的描述了:所有对象实例以及数组都要在堆上分配内存空间(随着HotSpot虚拟机中JIT的成熟,对象并不一定在堆上分配,栈上分配也是有可能的)。这片区域中的数据在类加载过程中每一个加载类对应的Class对象极其静态成员变量都存放在这里,当我们实例化一个对象的时候, 其实例对象本身也是存在堆中的,因为堆内存大小在创建时已经确定,所以该区域也存在OOM的可能,。

垃圾回收的主要区域也是发生在堆内存中,通常采用分代回收的算法,新生代 ( Young ) 与老年代 ( Old ) 的比例的值通常为 1:2,但是 我们可以修改设置新生代比例参数,而且比例通常需要具体JDK版本下进行确定。由于堆内存被所有Java线程锁共享的,所以它不是线程安全的区域, 保证安全的并发访问就需要采用一些手段,比如:final,访问权限私有,volatile保证可见性,加锁/CAS等等。

程序计数器、虚拟机栈

程序计数器和虚拟机栈是线程私有的,当我们启动Java程序的时候,执行引擎驱动会找到主类的main函数,为其创建一个main线程, 然后为其分配私有的程序计数器和虚拟机栈。

线程的程序计数器的作用很简单:存放执行指令,因为其存储数据仅仅就是下一个需要待执行的命令的地址,所以它是运行时数据区中唯一一个不会发生OOM的地方, 那么程序计数器有什么意义呢?举个简单的例子Java语言是支持多线程的,线程的切换之后, 那么当前线程可能会进行等待,那么当前线程再次获取到CPU资源的时候,如何从切换前的地方开始执行程序呢?这就需要通过程序计数器来 完成了,因为它记录来当前线程的执行情况,线程切换之后仍能在正确的位置执行。

虚拟机栈实际上是栈的数据结构,它的操作只有压栈出栈两种,每当我们执行一个方法的时候,就为这个方法创建对应的栈帧,并放到栈中(PUSH), 当方法执行完就出栈(POP),栈帧中存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息,这也是为什么我们说对象的引用是存在栈中的原因。那么虚拟机栈的大小是如何规定的呢?虚拟机规范中允许栈的大小是动态的或者是固定不变的,当我们确定栈的大小时,可以通过-Xss参数进行设置, 如果当线程请求容量超过-Xss设置大小时,就会栈溢出,如果栈是动态的,那么当内存不足时,就出出现OOM。

这里我们就以下面的代码为例说一下方法入栈的过程: 当我们执行main函数的时候,会将main方法压入线程的虚拟机栈中,然后执行main函数,首先会实例化Test, 然后调用print方法,此时print方法入栈,接着执行print方法,print方法中调用print2方法,所以会将print2方法压入虚拟机栈,当 print2方法执行完之后,因为其程序执行完成所以出栈,接着是print出栈,最后main方法也出栈,程序执行完毕。

代码语言:javascript
复制
    public static void main(String[] args) {
        Test test = new Test();
        test.print();
    }

    private void print() {
        print2();
    }

    private void print2() {
        System.out.println("123");
    }

这里我们应该能想到递归函数为容易造成栈溢出,就是因为递归层次如果过深,那么不断的有栈帧入栈,导致栈溢出, 但是如果是支持尾递归优化的编译器、解释器,那么我们使用尾递归方式进行递归,方法会被当作一个栈帧处理,所以能防止栈溢出。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 每天学Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 方法区
  • 程序计数器、虚拟机栈
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档