JVM简记2-堆

JVM虚拟机-堆

仅做学习内容的简单记录

Java堆被所有线程共享,在Java虚拟机启动时创建。是虚拟机管理最大的一块内存。

Java堆是垃圾回收的主要区域,而且主要采用分代回收算法。堆进一步划分主要是为了更好的回收内存或更快的分配内存。

堆的内存分配其实是通过【垃圾收集器】去实现。也就是说垃圾收集器不仅负责堆的垃圾回收,还负责堆中对象需要的内存分配。

垃圾回收针对不同情况的对象,回收策略(回收算法)是不同的的。而通过内存的划分,可以将不同的算法
在不同的区域中进行使用。

​ 比如说年轻代使用了复制算法。

​ 年轻代中的对象,生命周期很短,基本上是很快就死了,也就是被GC了。
​ 老年代中的对象,都是一些老顽固,都是多次回收的对象或者大对象才存到老年代中

在这里插入图片描述

1.jdk不同版本内存模型

JDK 1.7----------

在这里插入图片描述

Young 年轻区(代)
Young区被划分为三部分,Eden区和两个大小相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,仍然存活于Survivor的对象将被移动到Tenured区间。

Tenured 年老区
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。

Perm 永久区
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。

Virtual区:
最大内存和初始内存的差值,就是Virtual区。

JDK 1.8---------

在这里插入图片描述

在jdk1.8中变化最⼤的Perm区,⽤Metaspace(元数据空间)进行了替换。
需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在。

JDK 1.9(G1)---------

在这里插入图片描述

2.对象内存分配

2.1 对象内存的分配原则

  1. 优先在 Eden 分配,如果 Eden 空间不足虚拟机则会进行一次 MinorGC
  2. 【大对象】直接进入老年代,【大对象】一般指的是【很大的字符串或数组】,默认是0.
  3. 【长期存活的对象】也会进入老年代,每个对象都有一个【age】,当age到达设定的年龄的时候就会进入老年代,默认是15岁。

2.2 对象内存的分配方式

内存分配的方法有两种:指针碰撞(Bump the Pointer)和空闲列表(Free List)

分配方法说明收集器
指针碰撞内存地址是连续的(年轻代)Serial和ParNew收集器
空闲列表内存地址不连续(年老代)CMS收集器和Mark-Sweep收集器

2.3 对象内存分配的安全问题

在分配内存的同时,存在线程安全的问题,即虚拟机给A线程分配内存过程中,指针未修改,B线程可能同时使用了同样一块内存。

在JVM中有两种解决办法:

  1. CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  2. TLAB,本地线程分配缓冲(Thread Local Allocation Buffer即TLAB): 为每一个线程预先分配一块内存

2.4 对象内存分配担保

在JVM的内存分配时,存在这样的内存分配担保机制。就是当在新生代无法分配内存的时候,把新生代的对象转移到老年代,然后把新对象放入腾空后的新生代。

把Java堆大小设置为20MB,不可扩展。
其中10M分配给新生代,另外10M分配给老年代,然后做个例子

串行垃圾收集器内存担保

JVM参数配置:

-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
public class Test {

    private static final int _1MB = 1024 * 1024;

    public static void main(String args[]) {
        testHandlePromotion();
    }
    public static void testHandlePromotion(){
        byte[] allocation1,allocation2,allocation3,allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[5 * _1MB];
    }
}
#执行结果如下:
[GC (Allocation Failure) [DefNew: 7160K->938K(9216K), 0.0062572 secs] 7160K->5034K(19456K), 0.0068157 secs] [Times: user=0.02 sys=0.02, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 8429K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  91% used [0x00000000fec00000, 0x00000000ff350950, 0x00000000ff400000)
  from space 1024K,  91% used [0x00000000ff500000, 0x00000000ff5eaad8, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 3498K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 382K, capacity 388K, committed 512K, reserved 1048576K

根据结果第一行得知发生了一次gc使新生代从7160k变为938k,再看[]后面代表年轻代gc前后的变化,gc后年轻代还占5034k。

但是看heap信息发现并不想预期的那样:

原本预期: 前三个正常放入eden区再allocation4执行的时候由于eden区和s0区空间都不够触发内存担保机制,进行一次minorGC把此时年轻代里的顽固对象送到年老代去,再将allocation4放到eden区去。

实际结果: 经计算进入年老代的是4M,推测是在执行allocation3的new的时候就触发了内存担保,进行了minorGC,两个共4M送入老年代,后面两个2M+5M刚好是eden区的使用,from里是JVM基础加载的对象。

为证实对实际结果的推测进行单行debug:

在第一行打断点,然后通过jmap -heap PID 获取堆信息如下

在这里插入图片描述

看到allocation1还没new的时候eden区就有了3.42M的占用,这个应该是JVM初始的时候加载的吧,这下就明白了,前两个对象2M+2M+3.42M=7.42M,于是eden区只剩下不到1M的空间了,所以当allocation3在new的时候触发了内存担保,debug结果如推测一毛一样。哈哈哈

img

1、当Eden区存储不下新分配的对象时,会触发minorGC。
2、GC之后,还存活的对象,按照正常逻辑,需要存⼊到Survivor区(幸存区)。
3、当无法存入到幸存区时,此时会触发担保机制(要区分的是客户端担保机制还是服务端担保机制—区别于垃圾收集器)
4、将发生客户端内存担保时,需要将Eden区GC之后还存活的对象放入老年代。后面的新对象或者数组放入Eden区。

并行垃圾回收器担保机制

在进行并行垃圾回收器担保机制测试的时候我根据我这边jvm那个初始3.42M的情况做了下调整,调整内容如下:

//JVM参数准备了串行和并行两套
//串行:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
//并行:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseParallelGC
public static void testHandlePromotion(){
        byte[] allocation1,allocation2,allocation3,allocation4;
        allocation1 = new byte[1 * _1MB];
        allocation2 = new byte[1 * _1MB];
        allocation3 = new byte[1 * _1MB];
        allocation4 = new byte[4 * _1MB];
}

分别在不同的jvm参数下启动结果如下:

#串行结果:
[GC (Allocation Failure) [DefNew: 6136K->938K(9216K), 0.0030823 secs] 6136K->4010K(19456K), 0.0032111 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 5201K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0299a0, 0x00000000ff400000)
  from space 1024K,  91% used [0x00000000ff500000, 0x00000000ff5eab18, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 3072K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  30% used [0x00000000ff600000, 0x00000000ff900030, 0x00000000ff900200, 0x0000000100000000)
 Metaspace       used 3481K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 380K, capacity 388K, committed 512K, reserved 1048576K
  
--------------------------------------------------------------------------------------------

#并行结果:
Heap
 PSYoungGen      total 9216K, used 6301K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 76% used [0x00000000ff600000,0x00000000ffc27418,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 4096K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff000010,0x00000000ff600000)
 Metaspace       used 3454K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

于是可以看到,串行下年老代used 3M 并行下年老代 used 4M,这说明:

串行下: 是在allocation4过来的时候触发内存担保和minorGC将新生代的三个共3M送到老年代,allocation4放到eden区

并行下: 在allocation4过来的时候触发内存担保机制,不同的是,并行下直接将allocation4的4M送到老年代去了,gc都没触发

接下来把allocation4的参数变化一下 allocation4 = new byte[4 * _1MB -100]; 再执行并行结果如下:

[GC (Allocation Failure) [PSYoungGen: 6136K->1016K(9216K)] 6136K->4175K(19456K), 0.0020121 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 5278K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 52% used [0x00000000ff600000,0x00000000ffa29940,0x00000000ffe00000)
  from space 1024K, 99% used [0x00000000ffe00000,0x00000000ffefe010,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 3159K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 30% used [0x00000000fec00000,0x00000000fef15ca0,0x00000000ff600000)
 Metaspace       used 3433K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

首先就看到了这次触发了GC并且老年代里是正好的3M,发现当我们使用Server模式下的ParallelGC收集器组合(Parallel Scavenge+Serial Old的组合)下,担保机制的实现和之前的Client模式下(SerialGC收集器组合)有所变化。
在GC前还会进行一次判断,如果要分配的内存 ≥ 1 2 \frac{1}{2} 21Eden区大小,那么会直接把要分配的内存放到老年代中。否则才会进入担保机制。

3.对象的创建与访问

3.1 对象的内存布局

对象在内存中存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。

在这里插入图片描述

1、对象头

对象头包括两部分信息:
一部分是用于存储对象自身的运行数据,如 哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳 等。
另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。当对象是一个java数组的时候,那么对象头还必须有一块用于记录数组长度的数据,因此虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小。

2、对齐填充

这部分并不是必须要存在的,没有特别的含义,在jvm中对象的大小必须是8字节的整数倍,而对象头也是8字节的倍数,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.2 对象访问方式

方式优点
句柄稳定,对象被移动只要修改句柄中的地址
直接指针访问速度快,节省了一次指针定位的开销

在这里插入图片描述

在这里插入图片描述

4.数组的内存布局

一维数组:

在这里插入图片描述

二维数组:

在这里插入图片描述


仅做学习记录与交流,如有错漏敬请指出

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值