摘要:本文主要讲解JVM的内存结构和内存分配,首先是逻辑上的内存模型,分为三大块:方法区、堆内存以及栈内存,然后是内存分配策略,对象的创建/布局/访问,堆/栈的区别,JVM指令重排以及内存屏蔽的知识点,最后是对java程序内存溢出,内存泄露的讨论。hotspot虚拟机是java程序运行的平台,掌握JVM对于项目bug的解决大有裨益。
逻辑上主要有三大块:方法区(常量池是方法区的一部分)、堆内存,栈内存
1)方法区
2)堆内存
3)栈内存(先进后出)
虚拟机栈:
栈帧的数据 | 详情 |
---|---|
1、局部变量表 | 局部变量:方法参数和方法内部定义的变量,局部变量表所需的内存空间在编译期间完成分配;编译期可知的8大基本数据类型 对象or数组引用(作为参数) 返回地址类型 |
2、操作数栈 | 先入后出,初始为空,在方法的执行过程中加入数据:算术运算、方法参数、方法返回值 |
3、动态链接 | 每个栈帧都包含一个指针:指向运行时常量池中–所属方法的符号引用,在运行期间将符号引用转化为直接引用称为动态链接 |
4、方法返回地址 | 正常完成出口:是执行引擎遇到一个return的字节码指令,恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,然后调用程序计数器执行后一条指令;异常完成出口:方法执行过程中遇到了异常,异常没有在方法体内得到处理,返回地址由异常处理器确定 |
本地方法栈:
4)程序计数器
概念:
1、线程私有,一块较小的内存空间
2、当前线程所执行的字节码的行号指示器
3、每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响
作用:程序计数器指向线程下一步执行的位置
为了线程切换后能恢复到正确的执行位置 (线程时程序运行最小的执行单位)
基础数据类型直接在栈空间分配;
方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;
引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;
方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;
局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC 回收;
方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放;
字符串常量在DATA 区域分配,this 在堆空间分配;
数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小!
1)虚拟机性能检测工具 与jvm调优相结合 个推 (结合JVM启动参数常见配置,jstat等命令)
定位项目问题时,知识,经验是关键基础,数据是依据,工具是运用知识处理数据的手段。数据包括:运行日志/GC日志/线程快照//堆转储快照等
jps:虚拟机进程状态工具
jstat:虚拟机统计信息监控工具
jstat是用于监视虚拟机各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程的类加载、内存、垃圾回收、JIT编译等运行数据
选项:-gcutil 最重要的参数是GC时间(YGC和FGC)次数和收集时间(YGCT和FGCT)
jinfo:java配置信息工具
实时查看和调整虚拟机各项参数
jmap:java内存映像工具
用于生成堆转储快照heapdump或dump,还可以查询finalize执行队列、java堆和永久代的详细信息
jhat:虚拟机堆转储快照分析工具,让用户可以在浏览器上查看分析结果
使用VisualVM或专业的分析dump文件的eclipse memoryAnalyzer工具更强大
jstack:java堆栈跟踪工具
用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待
2)jdk的可视化工具(Jconsole和visualVM)
Jconsole:java监视与管理控制台,在jdk目录下可以看见
内存监控:内存标签页相当于可视化的jstat命令,用于监控受收集器管理的虚拟机内存的变化趋势;
线程监控:线程标签页相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析;
visualVM:多合一故障处理工具,可以做到
显示虚拟机进程以及进程的配置、环境信息(jps/jinfo)
监视应用程序的cpu、gc、堆、方法区以及线程的信息(jstat/jstack)
dump以及分析堆转储快照(jmap/jhat)
程序运行性能分析,找出被调用最多、运行时间最长的方法
3)递归时,jvm栈里面有一个栈帧还是n个栈帧?
栈帧的作用:用于存储局部变量表、操作数栈、动态连接(指向常量池)、方法返回地址等信息,递归时会创建n个栈帧,当递归层数过多时,会导致虚拟机出现stackoverflowError的错误
降低gc频率,如果活着的对象全部进入老年代,老年代很快被填满,Full GC的频率大大增加
解决碎片问题
Eden空间快满时young GC,频率得以降低
缺点:两个Survivor,10%的空间浪费、复制对象开销
内存释放机制:
1、如果对象在新生代gc之后任然存活,暂时进入幸存区;以后每过一次gc,对象年龄 +1,直到某个设定的值15或直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中;
2、Eden: From Survivor : To Survivor空间大小设成8:1:1,对象总是在Eden区出生,From Survivor保存当前的幸存对象,To Survivor为空。一次gc发生后:
JAVA内存模型
规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行。在 java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。 ***
1)什么是重排序?
2)as-if-serial语义
3)内存访问重排序与内存可见性
4)内存访问重排序与Java内存模型
原则 | 特点 |
---|---|
1、程序次序法则 | 单线程内,书写在前面的操作happen-before后面的操作 |
2、解锁先于锁定 | 同一个锁的unlock操作happen-before此锁的lock操作 |
3、volatile变量法则 | 对volatile变量的写入操作先于每一个后续对同一个变量的读写操作。 |
4、传递性原则 | 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作 |
5、线程start方法优先 | 同一个线程的start方法happen-before此线程的其它方法 |
6、线程中断 | 对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。 |
7、线程终结 | 线程中的所有操作都happen-before线程的终止检测。(通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行) |
8、对象创建 | 先初始化,后finalize;一个对象的初始化完成先于他的finalize方法调用。 |
5)内存屏障 Memory Barrier
内存屏障的类型 | 特点 |
---|---|
1、LoadLoad屏障 | 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕 |
2、StoreStore屏障 | 对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 |
3、LoadStore屏障 | 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 |
4、StoreLoad屏障 | 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。(在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。) |
x.finalField = v; StoreStore; sharedRef = x;
从以下几个方面阐述:
1)申请方式:
2)申请后系统的响应
3)申请大小的限制
4)申请效率的比较:
5)heap和stack中的存储内容
6)数据结构层面的区别
补充:
栈有一个很重要的特殊性,就是存在栈中的数据可以共享
字面值的引用与类对象的引用不同,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变
String str = “abc” 的内部工作
1、先定义一个名为str的对 String类的对象引用变量:String str
2、在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址
3、将str指向对象o的地址
结论: 我们在使用诸如 String str = “abc”;的格式定义类,对象可能并没有被创建!唯一可以肯定的是,指向 String类的引用被创建了
1)对象的创建
检查new指令的参数是否能在常量池中定位到一个类的符号引用,按如下步骤执行类加载:
规划可用空间: 使用指针碰撞或空闲列表方法,选择哪种分配方式由java堆是否规整决定,在使用Serial、ParNew等带标记-整理过程的收集器时,系统采用指针碰撞法,在使用CMS这种基于标记-清除算法的收集器时采用空闲列表。
并发情况下线程安全的解决方法:CAS配上失败重试保证更新操作的原子性,第二种:本地线程分配缓冲TLAB,内存分配完后,jvm将分配的内存空间初始化为零,以保证不赋初始值就可直接使用。
2)对象的内存布局(对象头/示例数据,对齐填充)
对象头信息(对对象进行必要的设置)
1、Mark Word 6个: hash码、GC年龄、锁状态、持有的锁、偏向锁线程ID、偏向锁时间戳
2、类型指针:判断对象属于哪个类的实例,指向所属类的指针
实例数据 存储真正有效数据(对象按程序员的意愿进行实例化)
1、字段的分配策略:相同宽度的字段总是被分配到一起,便于之后取数据;
2、父类定义的变量会出现在子类前面
对齐填充 (占位符的作用,非必须)
经过上述步骤:从虚拟机的角度,一个新的对象已经产生
3)对象的访问定位:通过栈上的引用数据来操作堆中具体对象
1)java堆空间
当对象创建太多超出了最大堆容量限制,且GCroots到对象之间路径可达,就会发生内存溢出异常(循环或递归中大量的new对象)
一般的异常信息:java.lang.OutOfMemoryError:Java heap space
Java堆溢出解决方案
通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析 ,先分清是内存泄漏还是内存溢出。内存溢出:实例过多、数组过大、大量的new对象;内存泄露:没被引用的对象(垃圾)过多
若是内存泄漏: 通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象与GC Roots相关联路径。
若是内存溢出:(对象确实还活着) 检查虚拟机的参数(-Xmx最大堆与-Xms最小堆)的设置是否适当,代码上检查是否某些对象生命周期过长。
2)虚拟机栈和本地方法栈溢出原因:(hotspot不区分两者,用-xss表示)
递归太深、死循环导致栈帧创建过多(递归或无限递归) stackoverflow异常
线程请求的栈深度大于虚拟机允许的最大深度,抛出StackOverflowError异常
在扩展栈无法申请到足够的内存(函数体内的数组过大) 则抛出OutOfMemoryError异常
Java堆溢出解决方案(减少内存的手段):多线程下,栈的大小越大,可分配的线程数就越少,通过减少最大堆和最大栈容量换取更多的线程
3)方法区和运行时常量池溢出
方法区作用:存储已被JVM加载的类信息、常量池、静态变量等。编译器编译后的代码,线程共享
溢出原因:CGlib字节码增强和动态语言填满了方法区(静态变量大、类加载过多)
异常信息:java.lang.OutOfMemoryError:PermGen space (jdk1.6)
解决方法:-XX:PermSize和-XX:MaxPermSize 设置方法区的大小 (jdk1.6)
4)本机直接内存溢出(-XX来指定)
java.lang.OutOfMemoryError抛异常时没有向操作系统申请分配资源,直接内存导致的内存溢出在dump文件中看不到明显异常
-XX:MaxDirectMemorySize指定,默认和Java堆最大值相同
项目中引起内存溢出的原因有很多种,常见的有以下几种:
内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
举例:在后台管理商品时,要把商品的信息导入到EasyUI控件中,刚开始没有设置mysql的分页,数据量太大,从而导致内存溢出 ***
集合类中有对对象的强引用,使用完后未清空,使得JVM不能回收;
代码中存在死循环或循环产生过多重复的对象实体;
使用的第三方软件中的BUG;****
启动参数内存值设定的过小;
解决方案:
第一步,修改JVM启动参数(-Xms,-Xmx),直接增加内存。
第二步,检查错误日志,查看“OOM”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出,因此对于数据库查询尽量采用分页的方式查询。检查代码中是否有死循环或递归调用,检查是否有大循环重复产生新对象实体,检查List、MAP等集合对象是否有使用完后,未清除的问题
第四步,使用内存查看工具 (如Memory Analyzer)动态查看内存使用情况。
1)概念:一个对象已经不需要再使用,本该被回收,另外一个正在使用的对象持有它的引用从而导致它不能被回收。停留在堆内存中,这就产生了内存泄漏
2)例如:对象连接资源未关闭造成的内存泄漏,集合容器中的内存泄露,出栈时,栈中对象不会被当作垃圾回收,通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
3)如何避免:写代码时:保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期
例子1:静态集合类 使用set,vector,hashmap等集合类时,当这些类被定义为静态时,由于他们的生命周期和应用程序一样长,这时候就可能发生内存泄漏。
class static test{
private static vector v = new vector(10);
public void init(){
for object obj = new object();
v.add(obj);
obj = null;
}
}
例子2:关于匿名内部类,非静态内部类会造成内存泄露的隐患。
Thread 是一个匿名内部类。
public void run() {
while (true) {
SystemClock.sleep(1000);
}
例子3:
监听器listener 物理连接 如数据库连接和网络连接 除非显式地关闭了连接,否则不会自动被GC回收。
单例模式:对象初始化后在整个jvm生命周期中存在,持有的外部对象的引用,那么这个外部对象就不能被回收,导致内存泄漏。
有些人你永远不用爱,有哪个谈爱发了财;有些手机你永远不必等,您拨的用户比你更郁闷