开坑,深入理解jvm虚拟机系列

1.java内存模型

1.1 程序计数器

内容

程序计数器记录当前线程所执行的字节码的行号指示器。字节码解释器工作时,通过改变计数器的值来读取下一条需要执行的字节码指令。如果java不存在多线程,那么也是不需要程序计数器的,因为程序是可以自己往下执行的。但是由于java支持多线程的特性,所以当线程挂起再恢复时,需要知道当前的字节码执行位置,恢复后继续执行。正是因为这样的用途,所以程序计数器是线程私有的,生命周期随着线程的创建而创建,随着线程的消亡而消亡。

特性

  • 如果当前线程执行的java方法,那么程序计数器指向的是下正在执行的虚拟机的字节码地址,如果正在执行的是native方法,由于不存在字节码,所以计数器的值为空(Undefined)。 问题:那么当执行到native方法时如果程序需要挂起怎么办?

  • 程序计数器是java虚拟机规范中唯一一个不会oom的区域。

拓展

  • .字节码,是jvm为了实现一次编译多平台运行而实现的技术。对于任何语言来讲,需要把变成语言翻译成机器语言机器才能执行,但是对于不同的平台,指令集不同,所以对于相同的代码,也无法跨平台直接使用,需要经过重新编译。jvm加入了中间成,java字节码。先让代码翻译为字节码,再翻译为机器码。而java内置了不同品台的翻译方法,所以可以做到一份字节码可以翻译为不同的机器码。

  • .java文件->.class,文件,是通过java编译器完成的。

  • 当执行native方法的时候,jvm是管理不到native方法执行时的内存的。内存、多线程都由c、c++自己决定。扩展阅读:操作系统如何实现线程切换

  • java 字节码解释

1.2java 虚拟机栈

概览

虚拟机栈,这个名字是以为了有别于操作系统所使用的栈。jvm在执行的时候有自己的栈结构。栈的基本组成单位是栈帧。虚拟机栈是县城私有的,并且栈帧的入栈出栈对应着方法的执行过程。栈帧会存储这样几个结构:局部变量表、操作数栈、动态连接、方法出口等信息。

1.局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间(数组结构),用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。联想到之前,使用@Data的时候,参数列表超长过了255.就是说这个局部变量表的大小超过了限制,而且会到编译无法通过,原因是因为实在编译时就确定好了局部变量表。换句话说,局部变量表是在class文件中的。

局部变量表的数据结构,由变量槽(Variable Slot)组成。一个变量槽占据32位的空间。能够存储绝大部分的数据类型:包括基本数据类型:int,float,boolean,byte,char,short,引用数据类型:reference。对于long,double这样的长数据类型需要占据64位存储,那么相应的会占据两个slot。所以耽搁方法的最大参数数量,不一定是255,根据具体参数的类型具体计算。在127-255之间。

其中reference为引用类型,存储的是堆中的对象地址,或者是方法区中的地址。

在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用。(在方法中可以通过关键字this来访问到这个隐含的参数)。

其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot。

局部变量表所需的容量大小是在编译期确定下来,并在方法的Code属性的Maximum local variables 数据项中,在方法运行期间是不会改变局部变量表的大小。

几种方法:构造方法,实例方法。

对于构造方法,如果当前帧由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数执照参数表的顺序继续排列。

对于实例方法,当前栈帧由实例方法创建,那么局部变量表将从index 的0处开始,顺序排列

补充说明

1. 在栈帧中,与性能评估关系最为密切的部分就是局部变量表。 在方法执行时,虚拟机使用局部变量表完成方法的传递。

2. 局部变量表中的变量也是重要的垃圾回收区域,只要被局部变量表中直接或间接引用的对象都不会被回收。划重点!!!

疑问:

lambda表达式是否会创建一个栈帧呢?

2.动态连接引用

动态连接是一种方法的引用类型。

具体的为什么叫动态连接,有什么作用:

  • 1.java在编译时,把java文件转换为class文件时,class文件中所有的变量和方法引用都作为符号引用保存在class文件的常量池里

  • 2.描述一个方法调用另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

3.方法出口

当一个方法被执行时,只有两个办法可以推出当前方法的执行:

  • 正常情况:遇到retrun字段

  • 异常情况:遇到异常,并且未catch住

退出之后都需要知道当前程序的执行位置,而执行位置在jvm中是由程序计数器来记录。所以栈帧创建时,会记录当前的pc值。

1.3 本地方法栈

java虚拟机规范对本地方法栈没有任何要求。Hotpot直接把本地方法栈和虚拟机栈合二为一了。

本地方法栈同样也是和方法的执行的生命周期挂钩。

1.4 java 堆

堆是所有java对象实例存储的区域,相应的也是垃圾回收器进行回收的区域。

传统的垃圾回收器都是基于分代理论建设的,所以传统的堆会被分成:新生代,老年代,永久代。随着G1的问世,堆空间的管理方式发生了变化。java虚拟机规范规定:堆中的空间可以是物理不连续,但必须是逻辑上连续的。

堆是所有线程共享的一块空间,但是为了提升各个线程分配空间的效率,堆中可以分配出多个线程私有的分配缓冲区(Thread Loacl Allocation Buffer,TLAB)-待补充。

对于线程缓冲区的意义:

1.为什么需要线程缓冲区?

  • 细分堆的好处是,多个线程可以同时分配对象,而不会互相干扰。此外,通过从同一存储器区域分配同一线程使用的对象,可以提高高速缓存命中率。―托马斯·安德森

2.Thread Local有什么用

  • 存在调用链很长的情况,需要进行上下文传递参数,此时只需要相应的get以下就好了。比如cookie登录信息。、

3.TLAB如何分配在何时分配?

TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

TLAB默认是eden区的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

4.TLAB和JAVA的ThreadLocal是不同的东西。实现方法和使用方法完全不同。

可以理解为TLAB是jvm层面的内存优化,而ThreadLocal确是做了一些应用层面的设计来达到TLAB的效果。

3.java中的ThreadLocal 是如何实现的

Tread类中存在变量:ThreadLocalMap,是一个Map结构。但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

ThreadLocalMap的实现方式:去知乎搜的图:

 

1.5 方法区

方法区与堆相同,都属于一块所有线程共享的区域。

主要储存:类型信息,常量,静态变量,即时编译器编译后的代码缓存。

在java虚拟机规范中,方法区被描述成堆的一个逻辑部分,但是它却拥有一个别名叫非堆。

在java8以前,方法区和永久代在实际管理上是等价的。实际上以前的逻辑管理机制只是把堆中的空间管理拓展到了方法区,并刚好用永久代的区域来存储方法区的内容。所以可以认为在java8之前是等价。在JDK 8中完全废弃永久代的概念,将方法区用另一块区域:元空间替代。把类型信息存放在元空间中。

1.6运行时常量池

运行时常量池是方法区的一部分。Class文件除了存放类的版本,字段,方法,接口描述等信息外,还有常量池表(Constant Pool Table)。用于存放编译期间生成的各种字面变量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

java虚拟机规范对于Class有着严格的格式要求,每一个字节用于存储哪种数据必须符合规范才能被jvm加载,执行;但是对于运行时常量池,或者常量池表。不同的虚拟机的视线可以不同。Class文件的常量池具备动态特性,可以在运行时进行添加。比如String类型的intern()方法。运行时常量池作为方法区的一部分同样也会发生oom异常。

1.7 直接内存

直接内存(Driect Memory)并不是虚拟机运行时数据区的一部分。也不是java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且可能导致OOM。所以放到一起。

JDK1.4中引入了new Input/output类型,一种基于channel和buffer的io方式。可以直接使用native函数直接分配堆外内存。 - 待补充

由于直接内存不属于堆内内存,但是当其无法分配空间时,也会oom报错。所以在配置jvm参数时,需要计算堆内内存和堆外内存的总内存不超过整台机器的最大内存。

扩展:对于input/output类型,是java高效解决IO的一种方式。

由于直接内存不是jvm中的内存,所以开辟内存需要直接调用navive类库,与操作系统直接交互。申请内存成本较高。但是读写效率大大提升。

传统的使用java读写磁盘的操作方式:

java本身不具备读写磁盘的能力,需要借助native库方法,调用操作系统的函数进行磁盘读写。

  • 调用时,cpu由用户态切换到内核态。切换成功后,就可以由CPU的函数,去真正读取磁盘文件的内容,在内核状态时,读取内容后,他会在操作系统内存中划出一块儿缓冲区,这块缓冲区成为系统缓冲区。先分批次的把磁盘中的内容,读取到系统缓冲区中。

  • 由于jvm无法读取堆外内存,所以需要再在jvm中分出一块内存。比如new btye[1024]。然后需要将系统缓冲区的内容移动到jvm的堆内存区域中。需要先从内核态切换回用户态,然后从系统缓冲区复制数据到jvm堆中。

需要开辟量两块内存,系统内存和jvm内存。还需要来回的将CPU在用户态与内核态之间切换,所以开销是非常大的。

采用byteBuffer的方式:

那么如何优化上一个过程呢?直接让jvm可以读取系统缓冲区的内容,不但能够解决jvm内存限制,直接使用机器内存,还能减少cpu切换和内存拷贝的时间。

有了直接内存driect memory之后。jvm可以直接读取这一部分内存进行磁盘读写。至于一些细节,比如说jvm如何能读取到系统内存,就需要之后再详细探究了。

小结:看完了jvm的运行时数据区域。jvm对几大数据区域分别的实现方式做了限制,经过几个JDK版本的迭代HOTPOT的实现方式也和之前有很多不同。知识需要不断的更新和换代。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值