文章目录
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 对象内存的分配原则
- 优先在 Eden 分配,如果 Eden 空间不足虚拟机则会进行一次 MinorGC
- 【大对象】直接进入老年代,【大对象】一般指的是【很大的字符串或数组】,默认是0.
- 【长期存活的对象】也会进入老年代,每个对象都有一个【age】,当age到达设定的年龄的时候就会进入老年代,默认是15岁。
2.2 对象内存的分配方式
内存分配的方法有两种:指针碰撞(Bump the Pointer)和空闲列表(Free List)
分配方法 | 说明 | 收集器 |
---|---|---|
指针碰撞 | 内存地址是连续的(年轻代) | Serial和ParNew收集器 |
空闲列表 | 内存地址不连续(年老代) | CMS收集器和Mark-Sweep收集器 |
2.3 对象内存分配的安全问题
在分配内存的同时,存在线程安全的问题,即虚拟机给A线程分配内存过程中,指针未修改,B线程可能同时使用了同样一块内存。
在JVM中有两种解决办法:
- CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- 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结果如推测一毛一样。哈哈哈
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.数组的内存布局
一维数组:
二维数组:
仅做学习记录与交流,如有错漏敬请指出