目录
一、Jdk源码解析过程
- Jdk(java开发工具包)-》jre(java运行时环境)-》jvm(java虚拟机)
二、java虚拟机运行时数据区
1、Java虚拟机的五大分区
首先来看一个图:
Java虚拟机的五大分区为:
- 方法区
- 堆
- 虚拟机栈
- 本地方法栈
- 程序计数器
下面来一一介绍
程序计数器(PC寄存器):
可以理解为当前线程所执行的字节码的指示器。
是线程私有的,每条jvm线程都有自己的程序计数器,各条线程互不影响,独立存储,是“线程私有”内存。
Java虚拟机栈
线程私有的,每个jvm线程都有自己的java虚拟机栈,与线程同时创建,生命周期和线程相同。
虚拟机描述的是java方法执行的内存模型:
每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈:
和虚拟机栈作用相似,虚拟机栈为虚拟机执行的java方法服务,本地方法栈为虚拟机用到的Native方法服务。
线程私有的
HotSpot直接把java虚拟机栈和本地方法栈合二为一。
Java堆
是虚拟机内存中最大的一部分
用来存储对象实例,所有对象技术组都要在这里分配内存
线程共享
虚拟机创建的时候创建
由于这块区域是线程共享的,里面存的数据不能随线程消亡而删除,所以这里的存储的对象实例以及数组要在这里被自动管理,也就是垃圾回收(GC)(Garbage Collector)。
方法区
方法区也是一块被各个线程共享的区域。
存储被虚拟机加载的类信息,常量,静态变量。
在虚拟机启动的时候被创建
运行时常量池
是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
类似于一个缓存区,避免了在Java堆和Native堆中来回复制数据。
三、OutOfMemory异常实践(OOM)
1、Java堆溢出
当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
内存泄露:是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
2、虚拟机栈和本地方法栈溢出
(1)单线程:
①如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
②如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
(2)如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程了。如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。
3、方法区和运行时常量池溢出
运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”
4、本机直接内存溢出
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后文件很小,而程序中有直接或简介使用了NIO,那就可以考虑一下是不是这方面的原因。
四、垃圾回收
由于程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,栈帧随方法进行入栈出栈操作。所以的这些区域的内存分配和回收具有确定性,不考虑垃圾回收问题。内存会随线程的结束而自动回收。
堆和方法区不一样,我们只有在程序运行期间才知道会创建哪些对象,这部分的内存分配和回收是动态的,垃圾回收关注的是这部分内存。
(1)堆的回收
- 死掉的对象所占内存需要回收
- 判断对象是否死掉
1)引用计数法
- 每当有一个地方需要使用这个对象时,计数器加一,当引用失效的时候,计数器减一,任何时刻,计数器为0的对象都不会再被使用。
- Java中没有使用引用计数法,因为很难解决对象之间循环引用的问题。
2)可达性分析算法或者根搜索算法
- 即判定对象是否存活,即从“GC Roots”对象作为起始点,从这些节点向下搜索,搜索走过的路径叫做“引用链”,当一个对象到GC Roots 没有任何引用链,证明此对象是不可用的。
- 在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
3)对象的引用
- JDK1.2之前,认为如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用,这种太过狭隘。
- 在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。
- 强引用:类似于“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:软引用用来描述一些还有用,但并非必需的对象。表示在java堆里面没有数据,但是在栈和方法区中有数据,比如:Object object= null;
- 弱引用:弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。所以弱引用的对象无论内存是否足够,都会被回收。例如:局部变量,返回值,参数。
- 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。比如:反射获取的对象,注解。
(2)方法区的回收
- 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
- 很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
- 无用的类是指:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
(3)垃圾回收算法
- 标记-清除算法(Mark-Sweep)
- 首先标记出所有需要清除的对象,标记完了统一回收被标记的对象。
- 主要缺点:
- 效率问题:要遍历两次
- 空间问题:产生大量不连续的内存碎片,使得程序在需要分配比较大的对象的时候无法找到连续的内存而不得不提前触发一次垃圾回收。
- 复制算法
- 为了解决效率问题
- 将内存分为大小相同的两块,每次只使用其中的的一块内存,当这一块内存使用完了,将这块内存上有用的对象复制到另一块上,然后把这块内存所有的数据都清空。
- 优点:实现简单,运行高效
- 缺点:内存缩小为原来的一半,代价高。
-
用来回收新生代,伊甸园区和两个幸存者区是8:1:1的关系。
- 标记-整理算法
- 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 老年代使用标记整理算法。
- 分代收集算法
- 一般是把Java堆分为新生代和老