JVM之内存详解
对象内存分配
JVM中对象创建流程
User user = new User();
1. new指令
2. 常量池检查:检查这个指令是否能在常量池中定位到一个类的符号引用
3. 此时判断是否加载过该类User如果没有或者检查不通过则触发类的加载
4. 分配内存空间 1.指针碰撞 2.分配器加锁(空闲列表)
指针碰撞 分配器加锁(空闲列表)接下来详讲
6. 内存空间初始化为零值
7. 必要信息设置(不是set方法而是JVM内部需要做的 比如该类是否有资格进入老年代 对象头相关的信息)
8. init方法调用
对象内存分配方式
内存分配方式和垃圾收集器息息相关!
指针碰撞
当有新的对象需要分配内存时(新生代),分界指示器会移动以保证内存的连续性,由于新生代的对象的大小在JVM分配内存空间时就是固定的因此有助于内存分配的优化。同时存在一个问题?此时会有线程安全问题吗? 如果两个线程都需要这个指针移动 JVM是怎么解决这个问题的呢?
- 内存地址是连续的
- 适用于新生代
- 当有新的对象需要分配内存的时候只需要将指针(分界指示器)转向还未使用到的内存,指针指向多少取决于该对象的大小,而该对象的大小是根据元数据来定义的,此时JVM是可以计算出来的
空闲列表
- 内存地址是不连续的
- 适用于老年代
- 由于老年代的对象比较大也比较老
以至于占用空间不能连续当该对象被回收时,该对象会被回收,但是该内存不会被回收
对比
内存分配安全问题
问题概述
当两个线程同时去一块内存时会出现内存碰撞问题
解决方式
CAS:乐观锁(采用自旋失败重试的方式保证操作的原子性)
TLAB:本地线程分配缓存(为每一个线程预先分配一块内存)
流程
首先适用CAS进行TLAB分配。
当对象大于TLAB中的剩余内存或者TLAB的内存已经用尽时再i采用上述的CAS进行内存分配
对象内存分配以及GC详细流程-非常重要
- 第一次gc将Eden区中不可达(需要回收)的对象进行清楚,将可达(不可回收)的对象复制到s0,此时整个新生代只有s0区域有数据
- 第二次gc将Eden区和s0区域中不可达的对象进行清除,将可达的对象复制到s1区,此时只有s1区域有数据
- 第三次gc同理,但是有可能Eden区域+s1区域的可达对象复制到s0的时候发现s0的区域不够存下这么多的数据,此时这些对象会进行老年代晋升,则申请晋升老年代?
- 如果申请成功进行老年代分配对象内存
- 如果申请失败则进行FGC-full gc(整个堆进行GC)
- 如果FGC完还不能够存放得下
- 发生著名的OOM!
进入老年代的条件-非常重要
- 存活年龄过大:默认gc15次会进入老年代(-XX:MaxTenuringThreshold)
- 动态年龄判断:YGC之后发现s0或者s1中的一批对象总大小大于这块s区域内存大小的50%(这个参数可以指定-XX:TargetSurvivorRatio),那么此时大于等于这批对象中年龄最大的对象可以直接进入老年代。这么做的目的是希望把那些有可能是长期存活的对象尽早放入老年代。
- 大对象直接进入老年代 1.前提:Serial或ParNew垃圾收集器 2.字符串或者数组-XX:PretenureSizeThreshold 一般设置成1M
- YGC(Minor GC)后存活对象太多无法放入s0或者s1区
- 空间担保机制:YGC前判断老年代可用空间是否已经小于整个新生代,如果小于则判断之前每次YGC进入老年代对象的平均大小,如果连续两次成功则会冒险尝试进行一次FGC,否则进行YGC。
空间担保机制补充:
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
进入老年代的条件测试
//大家可以准备这样一段代码进行测试
//参数
//-Xms60m -Xmx60m -XX:+PrintGCDetails -XX:+UseParallelGC jdk1.8默认的垃圾回收器
//-Xms60m -Xmx60m -XX:+PrintGCDetails -XX:+UseParNewGC
public class TestJVM {
private byte[] bytes = new byte[1024*1024*10];
public static void main(String[] args) {
TestJVM testJVM = new TestJVM();
System.out.println("完毕");
}
}
测试结果如下:
======================================================================
测试1:使用parnew垃圾回收器 -XX:+UseParNewGC
20M新生代 15M没有进入老年代 16M进入了老年代
10M新生代 7M没有进入老年代 8M进入了老年代
总结:对象到达了新生代的80%就会进入老年代
============================================================
**测试2:使用jdk8默认的回收器 -XX:+UseParallelGC
20M新生代 14M没有进入老年代 15M进入了老年代
10M新生代 6M没有进入老年代 7M进入了老年代
总结:对象到达了新生代的70%就会进入老年代
====================================================================
测试3:测试3:使用-XX:PretenureSizeThreshold=2m -XX:+UseParNewGC 使得2M的对象会直接进入老年代**
================================================================
总结:内存分配方式随着垃圾回收器的变化而变化
空间机制担保测试:
public class TestJVM {
private static final int _1MB = 1024*1024;
public static void main(String[] args) {
// byte[] bytes = new byte[1024*1024*3];
// System.out.println("完毕");
byte[] allocation1, allocation2, allocation3,
allocation4;
allocation1 = new byte[2 * _1MB];//2M
allocation2 = new byte[2 * _1MB];//2M
allocation3 = new byte[2 * _1MB];//2M
allocation4 = new byte[8 * _1MB];//4M
System.out.println("完毕");
}
}
参数:
//-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
//-XX:SurvivorRatio=8 -XX:+UseSerialGC
//堆空间20M
//年轻代10M
//老年代10M
//打印GC
//Eden:S0:S1=8M:1M:1M
结论:
1. 当Eden区存储不下新分配的4M的对象时会触发一次minorGC(YGC)
2. GC之后还存活的对象正常逻辑应该进入幸存区(S0或者s1)
3. 但此时幸存区的空间不足够 存放新对象因此触发了内存担保机制
4. 将Eden区GC之后还存活的对象放入老年代也就是3个2M的对象,后来新的4M的对象放入到Eden区
ps:如果此时第四个进来的是一个8M的对象 此时不会触发担保 此时判断该对象已经达到了新生代的80% 此时这个8m的对象会直接进入 老年代
对象内存布局
对象内部组成结构
1.对象头
a.markword
b.类型指针
i:作用:通过反射找到Class。一个类的class类对象的内存地址
ii:开启指针压缩4字节 未开启8字节 jdk.1.6之后默认开启
c.数组长度(如果对象是数组则记录数组的长度)
2. 数据区域(具体存储实例变量)
3. 对其填充(满足8个字节 不够补充)
Markword
概述
1.64位的Markwod占8字节 2.java中并发编程都是在对象头实现的 想要搞懂synchronizied原理 必然要懂对象头中的Markword
这里不详细展开讲Markword 在并发编程章节会详细讲 只需要混个脸熟 知道几种锁态都是记录在Markword中的即可
直接内存(堆外内存)
ps:IO操作在整个系统层面的流程:
java.io->用户内存(java进程、redis、mysql…)->内核内存/直接内存(cpu以及系统需要占用的内存)->磁盘驱动->写回java进程
直接内存也叫堆外内存。并不是JVM数据区的一部分。属于内核内存。
这也是为什么IO在直接内存会如此之快的原因,netty、nginx等作为高性能框架多处使用了对外内存
jdk1.4之后提供了NIO。 引入DirectByteBuffer对象对这块内存进行操作。
直接内存和堆内存比较
- 直接内存申请空间消耗更多的性能
- 直接内存IO读写要比堆内存性能要好
- 直接内存不受限于java堆内存的大小。受到物理机内存大小限制
- 配置JVM参数不要忽略这个参数 以防止直接内存OOM - List item
程序计数器
- 字节码指示器(存储下一次执行的字节码指令的行号)
- 不会触发OOM
- 线程私有的
- 内存占用很小几乎可以忽略不计