JVM内存组成与垃圾回收(浅显小白版)
JVM内存的组成
五个部分
1. 方法区(Method Area)
2. 堆(Heap)
3. 程序计数器PC
4. 虚拟机栈VM Stack
5. 本地方法栈 Native Method Stack
线程共享区
堆
- 是Java虚拟机所管理的内存中最大的一块,唯一的目的是存放在程序运行时所创建的对象实例,是垃圾回收的主要区域。
- 被所有线程共享。
方法区:
- 存储虚拟机加载的类的信息,常量,静态变量,方法的声明等数据。
- 同样被所有线程共享,可以被直接访问。
- 垃圾回收器很少对其进行垃圾回收,主要回收的是常量以及类型卸载的信息,静态变量不进行回收。注意如果定义的静态常量是引用类型,那么被引用的对象是有可能被回收的。
线程私有区
程序计数器 或 PC寄存器( PC register )
当前线程所执行的,字节码指令的行号指示器,例如分支、跳转、循环、异常处理、线程恢复都需要依赖程序计数器实现。
在Java多线程中,是通过线程轮流切换来分配时间片执行,为了线程切换后,能恢复到正确的位置,每个线程都有单独的程序计数器。
在程序涉及到分支、跳转的时候用计数器来记录行号。
虚拟机栈
- 主要为Java的方法服务。
- 当线程创建的时候,虚拟机栈会为线程分配一块内存的区域,在线程执行的过程中,调用的每一个方法都会创建一个栈帧(相当于当前方法的一个引用),在栈帧中存放局部变量、操作栈、动态链接、方法出口等。
- 每个方法从被调用到执行完,都意味着一个栈桢在虚拟机中入栈到出栈的过程。(可以形象地看成栈帧是方法在内存中的一个实例)
- 在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
本地方法栈
与虚拟机栈非常相似,但用途不同,本地方法栈为执行本地方法提供服务。
- Java中的方法有两种,java方法和本地方法。
java方法:
是由java语言编写,编译成字节码,存储在class文件中的。java方法是与平台无关的。
本地方法:
本地方法是由其他语言(如C、C++ 或其他汇编语言)编写,编译成和处理器相关的代码。本地方法保存在动态连接库中,格式是各个平台专用的,运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。
Java的垃圾回收(GC:Garbage Collection)
- 回收程序中不再使用的内存
- 在C语言与C++语言进行开发时,开发人员必须仔细管理好内存的分配与释放,而Java语言提供了垃圾回收器,自动检测对象的作用域;
- GC主要有三方面工作:
1) 为对象分配内存
2) 确保对象的引用
3) 对于没有被引用的对象进行回收 - GC回收的依据就是在内存中如果某个对象没有被其他任何对象或者变量引用的话,则可以被回收
- 屏蔽了开发人员对内存的控制,避免开发人员错误地操作内存导致程序崩溃
- 缺点:JVM必须对内存进行跟踪,释放没有用的对象,完成内存的释放后,还需要对JVM中的堆进行碎片处理,这些操作增加了JVM的负担,进而降低了程序的执行效率。
GC使用有向图的方式记录和管理堆内存中的所有对象
- 通过有向图可以识别哪些对象是可达的,哪些对象是不可达的,不可达则被视为垃圾。
- 底层垃圾回收的算法:主要有5种
- 1) 引用计数算法*
简易原理:在JVM堆中存储的每一个对象都一个被引用的计数器,当有一个变量引用这个对象的时候,这个计数器就会加一,当变量被释放,或者引用断开的时候,计数器减一,如果这个对象的计数器变为零,则代表这个对象可以被垃圾回收。
缺陷:若两个对象彼此互相引用,则称为循环引用,计数器算法无法解决。 - 2)跟踪回收算法
简易原理:根据JVM维护的对象引用图,从根节点开始遍历,标记所有遍历到的对象,遍历结束后未被标记的对象代表可以被回收的对象。 - 3)压缩回收算法*
简易原理:将JVM堆中活动的对象,放到一个集中区域,在堆的另外一端留出一大块空闲的区域,这就相当于对堆中的碎片进行处理。
缺陷:对性能的损失比较大(相当于打扫教室的垃圾前,先把桌子堆放到墙角。) - 4)复制回收算法*
简易原理:把堆分成两个相同大小的区域,在任何时刻,只有其中的一个区域被使用,直到这个区域被消耗完。此时垃圾回收器中断程序的运行,通过遍历的方式,把所有活动的对象复制到另外一个区域中。在复制过程中,这些对象紧密挨在一起,从而消除在内存中产生的碎片。当复制结束后,继续运行程序,直到此区域也被使用完,重复复制过程。
优点:在垃圾回收的同时,也完成了对象的重新布置与安排。因为在内存中对象都是紧密连接的,所以访问效率与寻址效率都非常高。
并且一次性解决了内存碎片的问题。
缺陷:对于指定大小的堆来说,需要两倍大小的内存空间。同时由于在内存调整的过程,需要中断当前程序的执行,进而降低了程序的执行效率, - 5) 按代回收算法*
简易原理:解决了复制回收算法的一些缺点。将堆分成两个或者多个子堆,每个子堆都视为一代,算法在执行过程中,优先收集年轻的对象,对于多次收集仍然存活的对象,移入较高级别的堆中。
优点:减少了稳定的,不常用的类的扫描次数,进而缩小了扫描的范围,提高了效率。
Java中内存泄漏的场景
内存泄漏:一个不再被使用的对象,仍在内存中占有空间
1)静态集合类
- static静态关键字的滥用
静态对象存储在方法区中,垃圾回收器几乎不会对方法区内的东西进行高频率的回收,static 的大集合,而方法区的空间比较小,很容易导致内存溢出,进而程序崩溃。
2)各种连接
- 数据库连接,网络连接、IO连接等等,忘记关闭连接。
由于连接被显示打开,而未被关闭,那么在JVM中这些对象一直是可达的,因此不会被回收。连接的长期堆砌有可能导致程序崩溃。
3)监J/听T/器Q
- 一个应用可能会使用到多个监J/听T/器Q,例如Java Web中的网络J监/T听/Q器Listener:监听指定的类或者对象产生的行为,从而做出对应的响应。由于J监/T听/Q器往往是全局存在的,如果对于J监/T听/Q器中所使用的对象或者变量没有进行有效的控制,很容易产生内存泄漏。
4)不合理的作用域
- 在软件开发中有一个基本的原则:作用域最小化。
如果变量可以声明在方法中,就不要把它拿到方法外;能用private不用public;这是Java开发人员的基本功,只有需要使用到某个变量的时候才去创建它。如果一个变量的定义范围大于其适用范围,很可能造成内存泄漏的情况。
5)未及时设置为null (较少发生)
- 另外如果使用时,没有把一个引用的对象及时设置为null的话也有可能会造成内存泄漏的产生。