一、运行时数据区域
java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,包括以下几个区域:
我们先举个栗子,方便回头再来看这个
MyClass.java
public class MyClass {
public void m1(){
System.out.println("你真帅...");
}
}
Test.java
public class Test {
public static void main(String[] args) {
MyClass mc=new MyClass();
mc.m1();
}
}
1、程序计数器:
程序计数器是线程私有,存放下一条需要执行的字节码指令,分支,循环,跳转,异常处理线程恢复等基础功能都需要依赖这个计数器来完成,每条线程都有一个独立的程序计数器,各个程序计数器之间互不影响。
2、java虚拟机栈本地方法栈
java虚拟机栈是线程私有,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈出栈到出栈的过程。局部变量表存放了编译期可知的各种基本数据类型、对象引用。
3、java堆
java堆是虚拟机里最大的一块内存,线程共享,用于存放创建对象实例,java堆是垃圾收集的主要区域,也被称作GC堆;java堆也可以细分为:新生代和老年代;再细致可以为:Eden空间、From Survivor空间、To Survivor空间等。
4、方法区
方法区是线程共享,用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它又被称为非堆。人们也把方法区称为“永久代”,该区域的内存回收目标只要是针对常量池的回收和对类型的卸载。
5、运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
6、直接内存
二、对象的创建过程
1、详细创建过程
2、对象的内存布局
对象在内存中存储的布局可以分为3块区域:
1、对象头:对象头可以分为两部分:第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志,线程持有的锁、偏向线程ID、偏向时间戳等;第二部分是类型指针,即对象指向它的类数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那在对象头中还必须有一块用于记录数组长度的数据。
2、实例数据:实例数据是对象的真正存储的有效信息,各种字段内容。或许是从父类中继承而来,或是子类中定义
3、对其填充:这个并不是必须存在的,没有什么特殊含义,起占位符作用。
3、对象的访问定位
对象访问定位说的就是如何拿到你在堆中创建的对象,栈是通过栈上的reference数据来操作堆上的具体对象的,目前通过两种方式来访问对象:
1、句柄:使用句柄,java堆会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与数据各自的具体地址。使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据 ,而reference本身不需要改变
2、直接指针:如果直接使用指针访问,那么reference中存储的就是对象地址。使用直接指针访问的最大好处就是速度快,节省指针定位的时间开销。对于SunHotSpot,使用的是直接指针。
三、OutOfMemoryError异常
1、java堆溢出
在避免垃圾回收机制清除对象的情况下,在java堆不断地创建对象就会产生OOM(out of memory)内存溢出异常
内存溢出:内存溢出是一切正常的情况下,堆的空间不够用,造成内存溢出
内存泄漏:该回收的对象,垃圾收集器无法对它们自动回收
2、虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常
如果虚拟机在扩展栈时无法申请到足够的内存空间,则将抛出OutOfMemoryError异常
3、方法区和运行时常量池溢出
方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。通过运行时产生大量的额类去填满方法区,直到溢出,产生OOM异常
4、本地直接内存溢出
三、垃圾收集器
1、判断对象的生命状态:
1、引用计数算法:给对象添加一个引用计数器,如果对这个对象有引用的时候,计数器加1,引用失效时,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。
2、可达性分析法:通过一系列称为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。可作为GC Roots的对象包括以下几种:
- 虚拟机栈中引用的对象
- 方法区中类静属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般是Native方法)引用的对象
2、垃圾收集算法:
(下列算法根据《深入理解java虚拟机》中语言描述和配图来理解,这里不予描述)
- 标记-清除算法:
- 复制算法:
- 标记-整理算法:
- 分代收集算法:
3、Hotspot的算法实现:
- 枚举根节点:
- 安全点:
- 安全区域:
4、垃圾收集器:
1、serial收集器:
工作在新生代,Serial收集器是一个单线程的收集器,它使用的是一条收集线程完成垃圾收集工作,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。到目前为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。Serial收集器由于没有线程交互,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
2、Parnew收集器
工作在新生代,ParNew收集器其实就是Serial收集器的多线程版本,是一款并发收集器,实现了让垃圾收集线程与用户线程同时工作。
3、Parnew Scavenge收集器
工作在新生代,使用的是复制算法的收集器,优势并行的多线程收集器,Parnew Scavenge收集器的目的则是达到一个可控制的吞吐领,也被称为“吞吐量优先”收集器。Parnew Scavenge提供了一些可控制吞吐量的参数,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。自适应调节策略也是Parnew Scavenge与ParNew收集器的一个重要区别。
4、Serial old收集器
工作在老年代,是Serial的老年代版本,是一个单线程收集器,使用‘标记-整理’算法,这个收集器的意义也是在于给Client模式下的虚拟机使用。有两个用途:一种是与Parnew Scavenge收集器搭配使用,另外一种是作为CMS收集器的后备预案。
5、Parallel old收集器
工作在老年代,Parallel old是Parallel Scavenge收集器的老年代版本,使用“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parller Old收集器。
6、CMS收集器
工作在新生代,是一种以获得最短回收停顿时间为目标的收集器。CMS是基于“标记-清除”算法实现的,它的运作整个过程:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
CMS优点体现在:并发收集、低停顿。
缺点:CMS收集器对CPU资源非常敏感、CMS收集器无法处理浮动垃圾、基于“标记-清除”算法实现的收集器在收集结束后会产生大量空间碎片。
7、G1收集器:工作在新生代与老年代,是一款面向服务应用的垃圾收集器,使用“标记-整理”算法实现,它具有以下的特点:
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
G1收集器之所以能够建立可预测的停顿时间模型,是因为它可以有计划的避免在整个java堆中进行安全区域的垃圾收集。
G1收集器在运作分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收