相较于之前看的Java虚拟机规范(JVM内存区域),这本书多了实践内容,感觉看着更带感呢(主要是趁着打折买了新书,喜新厌旧的我).
在我上一篇关于内存区域的博客中对内存区域的讲解与这一章的内容都大同小异 (里面有段话,挺好玩的,一些资料称Java堆为GC堆,作者说幸好没翻译为垃圾堆.俺又想到之前网传的一句啥子话,不要在垃圾堆里找对象,对象又都在GC堆里啊,难怪找不到对象(开玩笑的)) .
相较于规范,本书多了一些细节吧,俺记录一下.
1.永久代: HotSpot虚拟机团队将垃圾收集器的分代设计拓展到了方法区,使得垃圾收集器可以像管理堆一样管理方法区的内存 (主要针对常量池的回收以及对类型的卸载),以省去为方法区专门编写管理代码的方式,可是这种设计并不太好,更容易出现内存溢出问题.到了Java8就全部移至使用直接内存的元空间了.
2.运行时常量池的动态性: 运行时期也可以将新常量放入池中(如String::intern()方法)
**3.直接内存:**它不是运行时数据区的一部分,也不是规范中定义的内存区域,比如NIO类(基于通道与缓冲区的I/O流)可直接分配堆外内存,以提升性能,受本机内存以及处理器寻址空间的限制,设置各区域动态内存拓展上限时别忘了考虑直接内存的大小.
那么按照这些更新,再画一下运行时数据区域:
主要分为两种分配方式:
而在多线程的情景下就需要考虑到内存分配的安全性了,毕竟对象的创建是一个很频繁的事情,这里也有两种方式来保证线程安全:
对象信息分为: 对象头,实例数据,对其填充 .
如果是TALB的方式可直接将内存区域提前初始化为0值,如果不是则将对象除了对象头初始化为对应的0值.
初始化必要信息(对象头).对象头分为两类信息;
static class test{
public synchronized void test(){
}
}
public static void main(String[] args) throws InterruptedException {
Object obj = new test();
print(ClassLayout.parseInstance(obj)
.toPrintable());//1。偏向锁
System.out.println(obj.hashCode());
print(ClassLayout.parseInstance(obj).
toPrintable());//2.无锁
new Thread(()->{
synchronized (obj){
System.out.println("thread1");
print(ClassLayout.parseInstance(obj).
toPrintable());//3.轻量级锁
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(300);
synchronized (obj){
System.out.println("main");
print(ClassLayout.parseInstance(obj).
toPrintable());//3.轻量级锁
}
//查看对象内部信息
print(ClassLayout.parseInstance(obj).
toPrintable());//4.无锁
new Thread(()->{
synchronized (obj){
System.out.println("thread1");
print(ClassLayout.parseInstance(obj).
toPrintable());//5.重量级
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
synchronized (obj){
System.out.println("thread1");
print(ClassLayout.parseInstance(obj).
toPrintable());//6,重量级锁
}
// //查看对象外部信息
// print(GraphLayout.parseInstance(obj).toPrintable());
//
// //获取对象总大小
// print("size : " + GraphLayout.parseInstance(obj).totalSize());
}
}
1.偏向锁
2.无锁
3.轻量级锁
4.轻量级锁
5.重量级锁
6.重量级锁
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码,对象分代年龄 | 01 | 无锁 |
指向锁记录的指针 | 00 | 轻量级锁 |
指向重量级锁的指针 | 10 | 重量级锁 |
空 | 11 | GC |
偏向ID,偏向时间戳,分代年龄 | 01 | 可偏向 |
HotSpot的默认分配顺序是按照宽度大小顺序分配,同宽度的一起存放,父类中定义的变量再按照这个基础规律,先于子类,也可以设置+XX:CompactFields参数,让子类窄数据放于父类变量的间隙.
系统要求起始地址必须是8字节的整数倍,所以若需要则通过对齐填充实现.
栈上reference数据来操作堆上具体对象.
有两种访问方式:
设置堆最大内存和最小内存都为20m(即不能扩展),当堆内存空间溢出时输出堆的内存快照:
//-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
主要思路就是疯狂的实例化对象,直到塞满堆.
排查思路是使用内存映像分析工具,分析是内存泄漏(含无用对象)还是内存溢出(增大堆空间(注意与机器内存比对))
设置栈内存为128k:
-Xss128k
栈有两种异常,
1.当没有空间以分配栈帧时,会报栈溢出异常.
2.当多线程的场景下无法分配栈空间时会报内存溢出异常.(俺是64位机器复现不了,还把电脑搞挂了23333)
解决思路: 栈深度来说一般是够用的,如果内存溢出,如果无法换64位机器或者减少线程数,那就减小每个栈所占内存.
-XX:PermSize=10M//不过8以后会报异常,没有这个空间了
-XX:MaxMetaspaceSize=10M //设置元空间最大为10M,默认为-1,即不受限制
-XX:MetaspaceSize=10M//初始空间
方法区内存溢出,可能常量池溢出,也阔能方法区溢出
常量池的话,用于存储编译时期产生的字面量和符号引用的,所以可以疯狂产生字面量(书中例子就是字符串常量池,不过,Java6以后的JVM都不会引发这个异常,因为字符串常量池被移到堆中了)
方法区的话,存储类信息,实际场景中当出现很多动态加载的类时,就要格外小心方法区内存溢出.
-XX:MaxDirectMemorySize=10M//指定直接内存为10M
直接操作本地内存的,比如NIO之类,如果产生的Heap Dump(Java进程所使用的内存情况在某一时间的一次快照比较小),就有可能是这个问题了。
为啥1,2锁状态的转换,为啥调用了hashcode方法就由偏向锁变为了无锁状态,