1.运行时数据区(内存模型)
线程不安全: 方法区和堆
线程安全 :虚拟机栈,本地方法栈,程序计数器
程序计数器:较小的内存空间,是当前线程执行的字节码行号知识器,改变计数器的值来选取下一条需要执行的字节码指令(分支、循环、跳转)
虚拟机栈:java方法执行的内存模型,每个方法执行都会创建一个栈帧,里面有局部变量表(八中基本数据类型和对象引用),操作数栈(运行结果),动态链接和方法出口等,方法开始和结束就是栈帧入栈和出栈的过程。
本地方法栈:与虚拟机栈类似,为虚拟机使用到的本地Native方法(C语言)服务。
Java堆:内存最大的一块,在虚拟机启动时创建,几乎所有的对象实例都在这里分配内存 (随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。)
方法区:存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据
常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用
直接内存:JDK1.4后加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过有一个存储在java堆中的对象,作为这块内存的引用进行操作
这里需要说明一下对象访问的方式,主要包括2种句柄访问和直接指针访问:
1. 句柄访问主要是Java堆中划分一块句柄池,虚拟机栈中存放句柄池中的地址,句柄池中包括对象的实例数据和对象类型的数据的地址
2.直接指针访问,就是虚拟机栈直接指向Java堆中的对象类型指针和对象的实例数据,然后对象类型指针在指向方法区中对象类型的实例数据
Student s = new Student();在内存中做了哪些事情?
• 加载Student.class文件进内存
• 在栈内存为s开辟空间
• 在堆内存为学生对象开辟空间
• 对学生对象的成员变量进行初始化
• 通过构造方法对学生对象的成员变量赋值
• 学生对象初始化完毕,把对象地址赋值给s变量
2.内存溢出
①java堆溢出----OutofMemoryError异常
S1=new ArrayList();
While(true){
S1.add(new Person());
}
结果:虚拟机将当前的堆导出,并保存到XXX.dump文件下。使用MAT工具可以分析
②栈溢出(本地/虚拟机)
private static int stackLength=1;
Public void stackLeak(){
stackLength ++;
stackLeak();-------不停的递归调用这个函数,线程请求栈的深度大于虚拟机栈所允许的深度,栈就会溢出
}
③方法区和常量池溢出
String.intern()是一个native方法,如果字符串常量池中已经包含有一个等于此String字符串,则返回代表池中这个String对象。使用list保持着常量池引用,避免FullGC回收常量池行为
Int i=0;
While(true){
List.add(String.ValueOf(i++).intern());------自动装箱,拆箱intValue()
}
3.判断对象死亡?
①引用计数法:添加引用计数器,每当有一个引用,计数器值加一;当引用失效,计数器值减一,为0时就不能被使用
*不能解决循环引用的例子
②可达性分析:节点向下搜索,走过路径称为引用链(GC ROOT)
·虚拟机栈引用的对象(栈帧中的本地变量表)
·方法区中类静态属性引用的对象
·方法区中常量引用的对象
·本地方法栈中的引用的对象
宣告死亡:有两次标记过程,在finalize()中可以重新关联对象
四大引用:
强引用:Object o=new Object();
软引用:有用但非必须,列进回收范围进行二次回收
弱引用:被关联的对象只能生存到下一次垃圾回收之前
虚引用:不影响也不能使用(目的,回收会提示)
4.JVM垃圾处理方法(标记清除、复制、标记整理)
• 标记-清除算法 ①标记阶段:先通过根节点,标记所有从根节点开始的对象,未被标记的为垃圾对象 ②清除阶段:清除所有未被标记的对象
---缺点:产生大量不连续的内存片段,空间资源浪费
• 标记-整理 ①标记阶段:先通过根节点,标记所有从根节点开始的可达对象,为被标记的为垃圾对象 ② 整理阶段:将所有的存活对象压缩到内存的一段,之后清理边界所有的空间
• 复制算法 ①将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象。
Java堆中内存分配
新生代与老年代其中新生代分为eden:from:to=8:1:1 采用复制算法
Eden和from区第一次进行minorgc(复制算法)后存活对象到to,清空Eden和from区域,然后from,to交换。内存不够进行担保分配、多次gc存活年龄很大、大对象进入到老年代。老年代快满时进行fullGC(标记清除/整理)
5.垃圾收集器
①CMS:基于”标记-清除”算法实现,并发标记,并发清除,速度非常快---缺点:产生大量的空间碎片
②G1:基于”标记-整理”算法,并行与并发,分代收集,空间整理,吞吐率高
③Serial:单线程收集器,在垃圾收集时,必须暂停其他工作线程,直到结束
优点:简单而高效(相对单线程收集器)、没有线程交互的开销,专心垃圾回收,获得效率
6.JVM类加载
加载-->验证-->准备-->解析-->初始化-->使用--->卸载
通过全限定名来加载生成class文件对象到内存中,然后进行验证这个class文件,包括文件格式、元数据、字节码检验。准备是对这个对象分配内存,解析是将符号引用转化为直接引用。初始化就是开始执行构造器的代码
初始化中注意①new关键字实例化对象、读取或设置一个类的静态字段(除被final修饰、已在编译期放入常量池的静态字段),以及调用一个类的静态方法的时候②对类进行反射调用时③先初始化父类,在初始化子类④虚拟机启动用户指定执行的主类
双亲委派概念 :如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
优点:保证程序稳定运行,一个java.lang.Object无论哪一个类加载器加载这个类,最终都委托给顶端的启动类加载器,保证Object类在程序的各种类加载器环境中都是同样一个类,相反没有双亲委派模型,系统会出现多个不同的Object类,混乱。
• 加载器
启动(Bootstrap)类加载器(c++虚拟机自身)
扩展(Extension)类加载器
应用程序(Appliction)类加载器
自定义(User)类加载器
• 如果加载同一个类,该使用哪一个类? ○ 父类的
7.JVM性能调优
一些运行参数:
-Xmx:堆内存最大限制
-Xms:起始内存大小
-XXNewSize:新生代大小
-XXNewRatio:新生代与老年代比例
-XX:SurvivorRatio:eden:from:to比例
通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照分析。一般通过内存映像分析工具(如Eclipse Memory Analyzer、EclipseMAT)对Dump出来的进行分析,先查看是内存泄漏还是内存溢出
内存泄漏:可以通过工具查看泄漏对象到GCRoot的引用链,就能追踪到为什么垃圾收集器无法回收;内存溢出:调整虚拟机堆参数(Xmx,Xms)
排错:用jmap内存映像工具)看内存情况,jstack(堆栈跟踪工具)看某个新java进程内线程堆栈信息
JVM线程死锁,你该如何判断是因为什么?如果用VisualVM,dump线程信息出来,会有哪些信息
• 常常需要在隔两分钟后再次收集一次thread dump,如果得到的输出相同,仍然是大量thread都在等待给同一个地址上锁,那么肯定是死锁了。
----------参考文献《深入理解java虚拟机》