虚拟机内存区域与内存溢出
虚拟机内存区域与内存溢出
运行时数据区域
程序计数器
定义:是一小块内存空间,它可以是当前线程所执行的字节码的行号指示器。
如何理解计数器存储在“线程私有”的内存?
Java虚拟机的多线程是通过线程轮流切换,分配处理器执行的时间的方式来实现的,所以在任意的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程内的指令。因此为了线程切换后能够恢复到正确的位置,每一条线程都需要一个独立的程序计数器来保证程序的执行,各个线程之间的计数器互不影响,独立存储,这部分内存称为“线程私有”的内存。
“线程私有”的内存是唯一一个在《Java虚拟机规范中》没有规定任何恶OutOfMemoryError情况的区域
Java虚拟机栈
定义:
每个线程运行时所需要的内存称为虚拟机栈
每个栈由多个栈帧组成,对应每次方法调用所需要的内存,每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
每一个线程中只有一个活动栈帧,对用着当前执行的方法
它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
问题:
1.垃圾回收是否涉及栈内存?
不涉及,栈内存中方法调用结束后栈帧会自动弹出栈内存不需要进行垃圾回收
2.栈内存分配越大越好吗?
不是的,在官网中给出了说明默认配置
物理内存的大小是固定的,如果栈内存过大会影响线程的数量
3.方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出
栈帧过多导致栈内存溢出(比如方法没有正确的结束递归…)
栈帧过大导致栈内存溢出
本地方法栈
本地方法栈与虚拟机栈锁发挥的作用是十分相似的,其区别只是虚拟机栈为虚拟机提供Java方法(也就是字节码服务),而本地方法栈为虚拟机使用到的是本地(Native)方法服务
需要注意的是:
这些本地方法(Native Method)的实现由非Java代码实现,
与虚拟机栈一样,本地方法栈也是线程私有的 ,也会在栈深度溢出或者栈扩展失败时抛出Stack Overflow Error和OutOfMemoryError异常
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,java虚拟机将会抛出一个Stack Overflow Error异常
堆
定义
Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,此区域存在的唯一目的就是存放对象实例,Java世界里“几乎” 所有的对象实例都在这里分配内存。
如何理解几乎而不是绝对?
由于即时编译技术的发展,尤其是逃逸分析技术的日益强大,栈上分配,标量替换优化手段已经导致了一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上渐渐显得不在那么绝对
特点
它是线程共享的,所有堆中的对象都需要考虑线程安全问题
在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永久代(Permanent Generation)
JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
来设置。
堆内存溢出
定义:Java堆用于储存对象实例,所以我们只要不停的创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制处理掉这些对象,那么随着对象的不断增多,总容量触及最大堆的容量限制后就会产生内存溢出异常
堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程 id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
方法区
定义
与Java堆一样,是各个线程共享的内存区域,它用来存储被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它有一个别名“非堆”(non-heap),目的是与Java堆区分开来。所以方法区是独立堆的内存空间
为什么在jdk6时会将方法区和永久代混淆?
因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分点设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理堆内存一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但现在看来,这样实现方法区并不是一个好主意,这样设计让Java应用更容易遇到内存溢出的问题,而且有极少数的方法(例如:String.intern())会因为永久代的原因而导致不同的虚拟机下有不同的表现
jdk7
到jdk7的HotSpot,已经把原本放在永久代的字符串常量,静态变量等移除
jdk8
到了jdk8,废弃了永久代的概念,转而与JRockit,J9一样在本地内存实现的元空间来代替,把jdk7中永久代中还剩余的内用(主要是类型信息)全部转移到元空间
运行时常量池
常量池和运行时常量池的区别
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息。
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
运行时常量池具有 动态性 :
Java语言并不要求常量只有编译期才能产出,也就是说非预置在class文件常量池的内容也可以进入运行时常量池中, 运行期间也可以将新的常量放入池中。 这一特性用的较多的就是String类的intern()方法(这个方法在不同的jdk版本下也有不同)
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虚拟机调用
直接内存的使用:
常见于 NIO 操作时,用于数据缓冲区
分配回收成本较高,但读写性能高
不受 JVM 内存回收管理
普通的IO操作
分配和回收原理
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦
ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!