JVM内存结构阐述
Posted 进击地小白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM内存结构阐述相关的知识,希望对你有一定的参考价值。
内存结构
程序计数器
-
作用,是记住下一条jvm指令的执行地址
-
是线程私有的
-
在线程上下文切换的过程中需要记录到下一条要执行的指令的地址,等到线程再次被调度到执行的时候,还是根据该线程的程序计数器,来找到下一条要执行的指令的地址
-
每个线程都有自己独有的程序计数器
-
唯一一个内存不会溢出的
-
随着线程创建而创建,随着线程销毁而销毁
栈
栈可以说是虚拟机栈中的局部变量表
局部变量表中存放了编译期可知的各种基本数据类型,对象引用(不等于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
-
线程运行需要的内存空间
-
栈帧(参数,局部变量,返回地址):每个方法运行时需要的内存
-
一个栈由多个栈帧组成
-
栈先入后出
栈的演示
主方法调用method1,method1调用method2
method2栈帧在栈的顶部
method1在栈的中间
主方法在栈的底部
局部变量,参数在method2栈帧内占用内存
方法结束完后一步步从顶至下弹出,占用的内存也会被释放掉
问题辨析
-
垃圾回收是否涉及栈内存:不需要,栈内存是一次次方法调用产生的栈帧内存,而每一次方法调用后都会被弹出栈,自动被回收掉,不需要垃圾回收来涉及栈内存
-
栈内存分配越大越好吗:栈内存过大会导致线程数变少,物理内存大小是有限的,假设物理内存为500M,如果栈内存为250M,能运行的线程就只有俩个
-
方法内的局部变量是否线程安全:局部变量是线程私有的,不会受到其他线程干扰,是线程安全的。但是给局部变量加上static修饰,就会有线程安全问题了!如果方法内局部变量没有逃离方法的作用范围,它就是线程安全的。如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
三个方法 m1线程安全,m2,m3线程安全需要考虑
m2中StringBuilder作为参数(逃离了方法的作用范围)可能被别的线程访问到,需要改成Stringbuffer才能保证线程安全
m3中把sb返回(逃离了方法的作用范围)可能导致被别的线程访问到
栈内存溢出
-
栈帧过多导致栈内存溢出(stackOverFlowError)(递归没有退出条件)
-
栈帧过大导致栈内存溢出
线程运行诊断
-
用top(linux命令)定位哪个进程对cpu占用过高
-
ps H -eo pid,%cpu | grep 进程id
(用ps命令进一步定位是哪个线程引起的cpu占用过高)
-
jstack进程id (根据线程id找到有问题的线程)
本地方法栈
java中有时候不能与操作系统直接交互,需要本地方法接口(c,c++编写的)与操作系统更底层的api来实现交互
堆
堆:通过new关键字,创建对象都会使用堆内存,是java虚拟机所管理的内存中最大的一块,此内存区域唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,堆是垃圾收集器管理的主要区域
-
它是线程共享的,堆中对象都要考虑线程安全
-
有垃圾回收机制
堆内存溢出(java.lang.OutofMemoryError:java heap space)
堆内存诊断(idea Terminal中输入命令)
-
jps:查看当前系统有哪些java进程
-
jmap -heap (进程id) 检查java堆内存占用
-
jconsole 图形化界面监视和管理控制台
-
jvisualvm 可视化虚拟机
方法区
随着虚拟机启动时创建
方法区与堆一样是各个java虚拟机线程共享的一块区域
它存储了跟类的结构相关的一些信息
类的成员变量,常量,静态变量,方法数据,以及成员方法,构造器方法,特殊方法的代码部分等数据,虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来
永久代:hotspot虚拟机1.8以前对方法区的称呼
方法区内存溢出:
-
1.8以前导致永久代溢出
-
1.8以后会导致元空间溢出
场景:
-
spring
-
mybatis
-
动态代理
在运行时生成类导致内存溢出
运行时常量池
常量池:就是一张表,虚拟机指令可以根据这张表找到要执行的类名,方法名,参数类型,字面量等信息
运行时常量池:运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放,并把里面的符号变为真实地址
JDK1.8 StringTable(字符串常量池(运行时常量池的一部分))是存在堆中的
jdk1.6即以下版本 StringTable是在永久代中
最重要一点,StringTable中存储的并不是String类型的对象,储存的而是指向String对象的索引,真实对象还是储存在堆中。
s1+s2是变量,在运行中可能引用的值被修改,结果不能确定,所以必须在运行期间动态的用StringBuilder进行字符串拼接,而s5是常量在编译期就已经能确定好,不需要StringBuilder方式拼接
字符串是延迟称为对象的,即执行到哪一行才会在字符串常量池中放入那一行
了解字符串常量池StringTable案例
String.intern()是一个Native方法,它的作用:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个对象的String对象,否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用
这里一开始将x放入了字符串常量池,然后new了一个s放在了堆中
s.intern将s放入串池,但是串池中已经有了“ab” 则不会放入串池,只会返回串池中的对象 所以s2 == x(true) 因为s2是串池中返回的对象 与常量池中的ab相等,s==x为false 常量池已经有了“ab”所以没把s放入串池中,还是存在堆中 (jdk1.8).
s2==x(true)
s== x(true) 因为s.intern()方法将s放入了字符串常量池(串池中没有“ab”)
如果是jdk1.6是将s拷贝,结果又会不同了,这里就不进行详细阐述了
x1 == x2 为false 因为常量池已有cd x2.intern()没有将x2放入常量池成功,x2.intern()的返回对象才会与 x1 相同
StringTable调优
调整 -XX:StringTableSize=桶个数
考虑将字符串对象是否入池
如果字符串很多 可考虑入池
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存频繁地被使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里进行讲解
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channle)与缓冲区(Buffer)的的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆中和Native堆中来回复制数据
显然,本机直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存以及处理器寻址空间的限制,服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx等参数信息,但经常忽略直接内存,使得各个内存区域综合大于物理内存限制,从而导致动态扩展时OutOfMemoryError异常。
-
常见于NIO操作,用于数据缓冲区
-
分配回收成本高,但读写性能高
-
不受jvm内存回收管理
分配和回收原理:
-
使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
-
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放内存
以上是关于JVM内存结构阐述的主要内容,如果未能解决你的问题,请参考以下文章