JVM内存分配笔记

3 篇文章 0 订阅

 

一、JVM运行过程

1)编写.java文件
2)JVM(虚拟机)将.java文件编译成.class文件
3)类加载器加载.class文件
4)加载完毕,交由JVM执行引擎(Execution Engine)和字节码解释器执行
在执行过程中,JVM会用一部分空间来存储程序执行期间需要用到的数据和相关信息。
这一段空间分为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。

二、Runtime Data Area(运行时数据区)包含那几个部分?

根据《Java虚拟机规范(Java SE 7版)》 的规定,通常包括以下几个部分:
1)程序计数器(Program Counter Register)----线程私有
2)JVM栈(JVM Stack)----线程私有
3)本地方法栈(Native Method Stack)----线程私有
4)堆(Heap)----线程共享
5)方法区(Method Area)----线程共享

三、运行时数据区的每部分到底存储了哪些数据?

1)程序计数器(Program Counter Register)----线程私有
一块较小的内存空间,可以当做是当前线程所执行的字节码文件的行号指示器,用来指示执行哪一条指令。
JVM多线程是通过获取CPU时间片的方式运行的,确保每次线程中断都能回到原来的位置,各个线程之间的计数器互不影响。
如果线程正在执行的是一个非Native方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
字节码解释器的工作就是通过改变这个计数器的值,来选取下一条需要执行的字节码的指令,分支、 循环、 跳转、 异常处理、 线程恢复等基础功能都需要依赖这个计数器来完成。
不会出现OutOfMemoryError。

2)JVM栈(JVM Stack)----线程私有
JVM栈是Java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,用于存储:
a.局部变量表:用来存储方法中的局部变量,包括方法中声明的非静态变量以及函数形参。
对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。
局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
long和double占用2个局部变量空间(Slot),其它只占用1个局部变量空间(Slot)。
b.操作数栈
c.动态链接
d.方法出口
e.等信息
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),
如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3)本地方法栈(Native Method Stack)----线程私有
本地方法栈和虚拟机栈所发挥的作用是非常相似的。
两者区别:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 
甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

4)堆(Heap)----线程共享
Java虚拟机所管理的内存中最大的一块。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。 从内存回收的角度来看,由于现在收集器基
本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有
Eden空间、 From Survivor空间、 To Survivor空间等。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
Java7堆:新生代(Eden ,S0(From) ,S1(To)),老年代,永久区
Java8堆:新生代(Eden ,S0(From) ,S1(To)),老年代,元空间(metaspace)
新生代和老年代的划分,GC的收集次数。
S0和S1是两块大小相等,可以互换的内存。主要用于垃圾收集算法。两块区域只使用一块。在一次GC后就会将对象放入S1或S0。
每执行一次GC就会增加1。

5)方法区(Method Area)----线程共享
在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,
在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
很多人都更愿意把方法区称为“永久代”(Permanent Generation)。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

6)直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分, 也不是Java虚拟机规范中定义的内存区域。
但是这部分内存也被频繁地使用, 而且也可能导致OutOfMemoryError异常出现。
在JDK 1.4中新加入了NIO( New Input/Output) 类, 引入了一种基于通道( Channel) 与缓
冲区( Buffer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储
在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著
提高性能, 因为避免了在Java堆和Native堆中来回复制数据。
显然, 本机直接内存的分配不会受到Java堆大小的限制, 但是, 既然是内存, 肯定还是
会受到本机总内存( 包括RAM以及SWAP区或者分页文件) 大小以及处理器寻址空间的限
制。 服务器管理员在配置虚拟机参数时, 会根据实际内存设置-Xmx等参数信息, 但经常忽略
直接内存, 使得各个内存区域总和大于物理内存限制( 包括物理的和操作系统级的限制) ,
从而导致动态扩展时出现OutOfMemoryError异常。

 四、使用new新建一个对象,例如:User u=new User();

1.新建对象的过程
1)首先检查指令的参数是否能在常量池(方法区)中定位到一个类的符号引用,并且检查这个符号引用代表的类(User)是否已被加载、解析和初始化过。
如果没有,就必须先执行相应的类加载过程。
2)然后为新生对象(u)分配内存,分配内存的大小在类加载完成后就可确定。两种分配方式,由Java堆是否规整决定。是否规整,由垃圾收集器是否有压缩内存功能决定。
    a.指针碰撞:内存和空闲的内存有明显的分界。在空的堆中直接复制出一块内存,并将指针指向内存。
    b.空闲列表:内存和空闲的内存相互交错。寻找空闲的内存新建对象。
    弊端:在并发量大的时候造成线程不安全,可以使用同步ThreadLoca(Thread Local Allocation Buffer)TLAB
3)内存分配完成后,内存空间初始化为零值(不包括对象头)。如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。可以通过对象访问属性的0值(int)。
4)虚拟机设置好对象,设置对象属于哪个类、怎样找到元数据(metadata)信息、对象的HashCode、GC分代年龄等信息。这些信息都存放在对象头(object header)。
5)此时新的对象已经产生,所有的数据都是初始值或者0值。然后执行<init>方法,将对象进行初始化。
2.对象在内存中的布局
分为3块区域:对象头(object header)、实例数据(instance data)和对齐填充(padding)。
1)对象头(object header),包括两部分信息
    a.对象自身的运行时数据:如哈希码、GC分代年龄、偏向线程ID、偏向时间戳、锁状态标志、线程持有的锁。
    b.类型指针:确定对象是哪一个类的实例(并不是所有的虚拟机有类型指针)。
    c.如果对象是一个Java数组,还有一块记录数组的长度的内存。
2)实例数据:有效信息,定义的各种类型的字段的内容,从哪个父类继承下来的。虚拟机默认分配参数策略和字段在源码中的定义的顺序影响。
    HotSpot虚拟机默认的分配策略为longs/doubles、 ints、 shorts/chars、bytes/booleans、 oops(Ordinary Object Pointers),从
    分配策略中可以看出,相同宽度的字段总是被分配到一起。 在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。 如果
    CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。
3)对齐填充:没有实际意义,起到占位符的作用。
3.两种访问对象的方式(取决于虚拟机如何去实现)

1)使用句柄访问:这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾
收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

2)使用直接指针访问:使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因
此这类开销积少成多后也是一项非常可观的执行成本。 就本书讨论的主要虚拟机Sun HotSpot而言,它是使用第二种方式进行对象访问的,但从整个
软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值