业精于勤,荒于嬉;行成于思,毁于随。
内容:
介绍了虚拟机中的内存是如何划分的,哪部分区域,什么样的代码可能导致内存溢出
运行时数据区域
程序计数器
定义:是一小块内存空间,它可以是当前线程所执行的字节码的行号指示器。
如何理解计数器存储在“线程私有”的内存?
Java虚拟机的多线程是通过线程轮流切换,分配处理器执行的时间的方式来实现的,所以在任意的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程内的指令。因此为了线程切换后能够恢复到正确的位置,每一条线程都需要一个独立的程序计数器来保证程序的执行,各个线程之间的计数器互不影响,独立存储,这部分内存称为“线程私有”的内存。
“线程私有”的内存是唯一一个在《Java虚拟机规范中》没有规定任何恶OutOfMemoryError情况的区域
Java虚拟机栈
定义:
每个线程运行时所需要的内存称为虚拟机栈
每个栈由多个栈帧组成,对应每次方法调用所需要的内存
每一个线程中只有一个活动栈帧,对用着当前执行的方法
问题:
不涉及,栈内存中方法调用结束后栈帧会自动弹出栈内存不需要进行垃圾回收
2.栈内存分配越大越好吗?
不是的,在官网中给出了说明默认配置
物理内存的大小是固定的,如果栈内存过大会影响线程的数量
3.方法内的局部变量是否线程安全?
栈内存溢出
![](https://img-blog.csdnimg.cn/90a9e39349dc4d52a535b5ba7dc206bf.png)
![](https://img-blog.csdnimg.cn/8eb51cace5ea4329b163d1fd67c4c4f9.png)
线程运行诊断
本地方法栈
本地方法栈与虚拟机栈锁发挥的作用是十分相似的,其区别只是虚拟机栈为虚拟机提供Java方法(也就是字节码服务),而本地方法栈为虚拟机使用到的是本地(Native)方法服务
需要注意的是:
这些本地方法(Native Method)的实现由非Java代码实现,
与虚拟机栈一样,本地方法栈也是线程私有的 ,也会在栈深度溢出或者栈扩展失败时抛出Stack Overflow Error和OutOfMemoryError异常
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,java虚拟机将会抛出一个Stack Overflow Error异常
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,java虚拟机将会抛出一个Stack Overflow Error异常
Java堆
定义
Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此区域存在的唯一目的就是寸法对象实例,Java世界里“几乎” 所有的对象实例都在这里分配内存。
如何理解几乎而不是绝对?
由于即时编译技术的房展,尤其是逃逸分析技术的日益强大,栈上分配,标量替换优化手段已经导致了一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上渐渐显得不在那么绝对
特点
它是线程共享的,所有堆中的对象都需要考虑线程安全问题
堆内存中垃圾回收机制
堆内存溢出
定义:Java堆用于储存对象实例,所以我们只要不停的创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制处理掉这些对象,那么随着对象的不断增多,总容量触及最大堆的容量限制后就会产生内存溢出异常
堆内存诊断
方法区
定义
与Java堆一样,是各个线程共享的内存区域,它用来存储被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它有一个别名“非堆”(non-heap),目的是与Java堆区分开来。所以方法区是独立堆的内存空间
方法区的在jdk6,jdk7,jdk8中的演变细节
需要明确的是只有在HotSpot虚拟机中才有永久代的概念
为什么在jdk6时会将方法区和永久代混淆?
因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分点设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够想像管理堆内存一样管理这部分内存,,省去专门为方法区编写内存管理代码的工作。但现在看来,这样实现方法区并不是一个好主意,这样设计让Java应用更容易遇到内存溢出的问题,而且有极少数的方法(例如:String.intern())会因为永久代的原因而导致不同的虚拟机下有不同的表现
jdk7
到jdk7的HotSpot,已经把原本放在永久代的字符串常量,静态变量等移除
jdk8
到了jdk8,废弃了永久代的概念,转而与JRockit,J9一样在本地内存实现的元空间来代替,把jdk7中永久代中还剩余的内用(主要是类型信息)全部转移到元空间
方法区内存溢出
OOM:PermGen和OOM:Metaspace
在jdk6版本运行时常量池溢出,在OOM异常后面会有着PermGen标志着永久代的溢出
在jdk7中继续使用-XX:MaxPermSize或者jdk8以及以上版本使用-XX:MathMetaspaceSize参数也不会出现jdk6中的溢出异常
运行时常量池
常量池和运行时常量池的区别
StringTable(串池)
位置:
在jdk6中StringTable在常量池中,而jdk7将StringTable放入到了堆内存当中,因为永久代的回收效率很低,只有在full gc时才会触发
特性:
1.常量池中的字符串仅是符号,第一次用到时才变为对象2.利用串池的机制,来避免重复创建字符串对象3.字符串变量拼接的原理是 StringBuilder ( 1.8 )4.字符串常量拼接的原理是编译期优化5.可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一 份, 放入串池, 会把串池中的对象返回
StringTable垃圾回收
垃圾回收只会在内存不足的时候触发
StringTable性能调优
StringTable的底层是hash表,也就是数组加链表的实现方式,其性能是根据hash表桶的个数(也就是数组的长度)来决定,如果数据桶个数多,那么元素分布比较分散,那么发生哈希碰撞的概率会更小,查找效率会更高,反之则更低
方式一:通过调整参数-XX:StringTableSize=桶个数
方式二:考虑将字符串对象是否入池
测试:
//StringTable{"a","b","ab"} hashtable结构无法扩容
String s1="a";
String s2="b";
String s3="ab";
String s5="a"+"b";
s1.intern();
String s4=s1+s2;//new StringBuilder().append("a").append("b").toString();new String("ab"); s1 s2为变量在运行期间进行拼接
System.out.println(s3==s4);
System.out.println(s3==s5);//javac在编译期的优化,结果在编译期间已经确定为ab 在串池中标找ab
直接内存
定义
直接内存(Driect Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的区域内存,但是这部分内存可以直接被jvm虚拟机调用
直接内存的使用:
![](https://img-blog.csdnimg.cn/0c0be2b5cd8e49bdbb979c5904a97fab.png)
![](https://img-blog.csdnimg.cn/96f7075f38fb46ed956e18260be981f7.png)
分配和回收原理
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存