JVM 进一步了解(1)
什么是JVM
定义:
Java Vitrtual Machine(JVM)
,java运行时环境
好处
- 一次coding,到处运行
- 可以自动管理内存
- 实现方法的多态
劣势
- 相较于编译型语言来说,性能差
- 相较于python,js等解释型语言来说,语法较为固定,没有高度自由。当然这种固化的格式也是java适合于大型的项目的原因之一,使其具有很好的代码质量控制能力;
- 虽然可以自动管理内存,但大型项目的诸多问题都是发生在内存回收阶段。目前在大部分特殊业务场景中,GC部分都需要进行针对性优化。
JVM的构成
我们先从JVM
的内存结构开始分析,然后分析他的execution engine
中的GC
部分,最后分析其Class Loader
部分。
JVM的内存结构
- PC register
- JVM stack
- Heap
- Method Area
- Native Method Stacks
PC register(程序计数器)
程序计数器,是指向下一条执行命令的指针。它是线程私有的,也就是说一个thread
独享一个pc register
。在物理上,pc
就是一个寄存器。
Tips:pc register
是java中唯一一个不需要考虑越界问题的结构。
JVM Language Stack
这和操作系统中运行一个程序的stack
很像,也是一个方法作为一个frame
。顾名思义,如果在方法内调用另一个方法,就push
一个frame
;方法结束调用,则返回并pop
这个frame
。其中需要注意的是,stack
大小有限制。特别是在做递归的时候,没有控制好下界,导致方法一直无法回归,那么就会出现stack overflow
的问题。
同样的单个frame
过大也会导致stack
不够大出现越界问题。
需要注意的是stack
同pc
一样也是线程私有的.也就是说方法内的local variables
,不会被其他线程访问到。(除了new
的对象,其余基本类型以及这些对象的引用,都是作为local variables
保存在本地栈帧中)
Heap
我们在创建一个对象的时候,会使用一个new
操作,他会调用对应类的构造函数,将其中的local variable
存放在heap
中,并且我们会在方法frame
中有一个引用来指向heap
中对象对应的位置。也就是说一般而言,new
对象的时候,内存中会在heap中创建一个真的object
,并且在JVM stack
中方法frame
里面有一个对象的引用。
一般而言Heap
内存是由JVM
自己控制的,这部分设计GC
的功能,将在另一篇文章讲解。Heap
不是线程私有的,由多个线程来共享一个heap
。其中,特别是在多线程开发中,对object
资源的占有,释放,以及为了占有、释放而发明的锁结构,是非常有趣的话题。这部分也会在接下来的blog中提及。
Method Area
方法区,方法区和heap
一样是由多个线程共享的区域,其主要存放类信息
,常量
,静态变量
以及java即使编译的临时code缓存
。其中有一部分为runtime constant pool
(运行时常量池),类文件中除了有类的版本,字段,方法,接口等描述文件外,还会有一个constant pool
来保存一些编译期产生的字面量和符号引用,这部分将在类加载后被存储到方法区内的运行时常量池中。
方法区在JKD8(HotSpot)以前,常常会被认作是永久代。或者人们将永久代和方法区混为一谈,其实这两者在概念上都不是等价的。仅仅是因为当时hotspot
团队将gc收集的设计拓展到方法区,换句话说是使用永久代来实现方法区而已。
直接内存
直接内存不属于java
虚拟机内存结构的一部分,但是这部分内存在现代的代码设计中会被频繁的使用。同样地,也会导致outofmemory
的问题。
直接内存是指java
虚拟机调用系统函数导致的堆外内存,例如nio
。java虚拟机
无需进行二次拷贝,可直接引用堆外内存的数据。java
虚拟机性能得到极大的提升,这是直接内存的一大优点。同样地,direct memory
也会有内存溢出的问题。
对象的create过程
对象create
操作需要check
对应的类是否已经加载到method area
中。如果还没有,则先执行类加载操作(具体类加载的过程将在以后的深入blog
中讲解)。在加载好类对象之后,需要在heap
中申请内存,此时申请的内存大小由类对象提供,然后对obejct header
进行一些必要的配置,之后执行其中的构造函数。其中我们先分析其中的heap
申请内存操作和分析对象在内存中的布局。
heap分配内存
一般而言,在一个具有足够大连续内存空间的heap
上,分配内存是很容易的。只需要heap
上的指针移动object大小
的值,将其中的空间分配给对象即可。但是总会遇到一些意外的情况,如空间碎片化,没有足够大的连续内存分配,并且jvm
并不提供内存整理功能时。这种情况直接抛出out of memory error
即可。但是由于多线程并发申请内存,使一个线程申请heap
内存时,heap
中的指针还没有就位,另一个线程又申请内存,导致前一个线程内存分配失败的情况。目前有两种方法来解决这个问题。
- 方法一,是分配内存操作进行同步处理,使其具有原子性,也就是说(类似串行化处理的方式),通过
CAS
和失败重试的方法来保证其原子性; - 方法二,通过给每一个线程在
heap
中预分配一块供其使用的空间来避免多个线程空间分配时的抢占情况,称为本地线程分配缓冲(Thread Local Allocation Buffer即TLAB)。
object在内存中的layout
在具体说明布局之前,先考虑这样一个问题,在一个32bit的机器上,一个int值占有多大的空间?一个Integer对象占有多大的空间?
答案可能出乎你的预料,一个int占有4个byte的栈空间,而一个简单的Integer
对象居然要占用4个byte大小的空间。如图:
一般而言,一个对象的内存layout主要包括三个部分:header
,instance data
,padding
。而Header部分一般又包含三个部分,一个指向类对象的指针,一个lock
于一个flag
word。
//todo
Reference
(Memory Layout of Objects in Java)[https://www.baeldung.com/java-memory-layout]
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!