个人理解:
因为在使用 JAVA 创建一个类或者对象后, 难免会存在以后不使用的情况, 为了减少其继续再占用内存, 必须建立一套清理垃圾的机制, 但是怎么判断什么样的才算是不使用的垃圾呢, 这里面进行了判断并标记分类, 然后根据不同的标记再进行不同的处理. 不过世事无完美之说, 其也是存在弊端的 (开销通常很大, 而且它的运行具有不确定性), 为了避免, 我们还是在正常工作中, 养成一个好的编程习惯. 详情参照 https://www.cnblogs.com/jeffwongishandsome/p/talk-about-GC-and-how-to-use-GC-better.html(侵删) 和 https://www.cnblogs.com/zhguang/p/3257367.html(侵删)以及 https://www.cnblogs.com/xiaoxi/p/6486852.html(侵删). 其都解释的比较详细!
一, JAVA 内存区域:
在 Java 运行时的数据区里, 由 JVM(JAVA 虚拟机)管理的内存区域分为下图几个模块:
其中:
1, 程序计数器(Program Counter Register): 程序计数器是一个比较小的内存区域, 用于指示当前线程所执行的字节码执行到了第几行, 可以理解为是当前线程的行号指示器. 字节码解释器在工作时, 会通过改变这个计数器的值来取下一条语句指令.
每个程序计数器只用来记录一个线程的行号, 所以它是线程私有 (一个线程就有一个程序计数器) 的.
如果程序执行的是一个 Java 方法, 则计数器记录的是正在执行的虚拟机字节码指令地址; 如果正在执行的是一个本地 (native, 由 C 语言编写完成) 方法, 则计数器的值为 Undefined, 由于程序计数器只是记录当前指令地址, 所以不存在内存溢出的情况, 因此, 程序计数器也是所有 JVM 内存区域中唯一一个没有定义 OutOfMemoryError 的区域.
2, 虚拟机栈(JVM Stack): 一个线程的每个方法在执行的同时, 都会创建一个栈帧(Statck Frame), 栈帧中存储的有局部变量表, 操作站, 动态链接, 方法出口等, 当方法被调用时, 栈帧在 JVM 栈中入栈, 当方法执行完成时, 栈帧出栈.
局部变量表中存储着方法的相关局部变量, 包括各种基本数据类型, 对象的引用, 返回地址等. 在局部变量表中, 只有 long 和 double 类型会占用 2 个局部变量空间(Slot, 对于 32 位机器, 一个 Slot 就是 32 个 bit), 其它都是 1 个 Slot. 需要注意的是, 局部变量表是在编译时就已经确定好的, 方法运行所需要分配的空间在栈帧中是完全确定的, 在方法的生命周期内都不会改变.
虚拟机栈中定义了两种异常, 如果线程调用的栈深度大于虚拟机允许的最大深度, 则抛出 StatckOverFlowError(栈溢出); 不过多数 Java 虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的), 所以线程可以一直申请栈, 直到内存不足, 此时, 会抛出 OutOfMemoryError(内存溢出).
每个线程对应着一个虚拟机栈, 因此虚拟机栈也是线程私有的.
3, 本地方法栈(Native Method Statck): 本地方法栈在作用, 运行机制, 异常类型等方面都与虚拟机栈相同, 唯一的区别是: 虚拟机栈是执行 Java 方法的, 而本地方法栈是用来执行 native 方法的, 在很多虚拟机中(如 Sun 的 JDK 默认的 HotSpot 虚拟机), 会将本地方法栈与虚拟机栈放在一起使用.
本地方法栈也是线程私有的.
4, 堆区(Heap): 堆区是理解 Java GC 机制最重要的区域, 没有之一. 在 JVM 所管理的内存中, 堆区是最大的一块, 堆区也是 Java GC 机制所管理的主要内存区域, 堆区由所有线程共享, 在虚拟机启动时创建. 堆区的存在是为了存储对象实例, 原则上讲, 所有的对象都在堆区上分配内存(不过现代技术里, 也不是这么绝对的, 也有栈上直接分配的).
一般的, 根据 Java 虚拟机规范规定, 堆内存需要在逻辑上是连续的(在物理上不需要), 在实现时, 可以是固定大小的, 也可以是可扩展的, 目前主流的虚拟机都是可扩展的. 如果在执行垃圾回收之后, 仍没有足够的内存分配, 也不能再扩展, 将会抛出 OutOfMemoryError:Java heap space 异常.
关于堆区的内容还有很多, 将在下节 "Java 内存分配机制" 中详细介绍.
5, 方法区(Method Area): 在 Java 虚拟机规范中, 将方法区作为堆的一个逻辑部分来对待, 但事实上, 方法区并不是堆(Non-Heap); 另外, 不少人的博客中, 将 Java GC 的分代收集机制分为 3 个代: 青年代, 老年代, 永久代, 这些作者将方法区定义为 "永久代", 这是因为, 对于之前的 HotSpot Java 虚拟机的实现方式中, 将分代收集的思想扩展到了方法区, 并将方法区设计成了永久代. 不过, 除 HotSpot 之外的多数虚拟机, 并不将方法区当做永久代, HotSpot 本身, 也计划取消永久代. 本文中, 由于笔者主要使用 Oracle JDK6.0, 因此仍将使用永久代一词.
方法区是各个线程共享的区域, 用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息, 包括版本, field, 方法, 接口等信息),final 常量, 静态变量, 编译器即时编译的代码等.
方法区在物理上也不需要是连续的, 可以选择固定大小或可扩展大小, 并且方法区比堆还多了一个限制: 可以选择是否执行垃圾收集. 一般的, 方法区上执行的垃圾收集是很少的, 这也是方法区被称为永久代的原因之一(HotSpot), 但这也不代表着在方法区上完全没有垃圾收集, 其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载.
在方法区上进行垃圾收集, 条件苛刻而且相当困难, 效果也不令人满意, 所以一般不做太多考虑, 可以留作以后进一步深入研究时使用.
在方法区上定义了 OutOfMemoryError:PermGen space 异常, 在内存不足时抛出.
运行时常量池 (Runtime Constant Pool) 是方法区的一部分, 用于存储编译期就生成的字面常量, 符号引用, 翻译出来的直接引用 (符号引用就是编码是用字符串表示某个变量, 接口的位置, 直接引用就是根据符号引用翻译出来的地址, 将在类链接阶段完成翻译); 运行时常量池除了存储编译期常量外, 也可以存储在运行时间产生的常量(比如 String 类的 intern() 方法, 作用是 String 维护了一个常量池, 如果调用的字符 "abc" 已经在常量池中, 则返回池中的字符串地址, 否则, 新建一个常量加入池中, 并返回地址).
6, 直接内存 (Direct Memory): 直接内存并不是 JVM 管理的内存, 可以这样理解, 直接内存, 就是 JVM 以外的机器内存, 比如, 你有 4G 的内存, JVM 占用了 1G, 则其余的 3G 就是直接内存, JDK 中有一种基于通道(Channel) 和缓冲区 (Buffer) 的内存分配方式, 将由 C 语言实现的 native 函数库分配在直接内存中, 用存储在 JVM 堆中的 DirectByteBuffer 来引用. 由于直接内存收到本机器内存的限制, 所以也可能出现 OutOfMemoryError 的异常.
Java 对象的访问方式
一般来说, 一个 Java 的引用访问涉及到 3 个内存区域: JVM 栈, 堆, 方法区.
以最简单的本地变量引用: Object obj = new Object()为例:
1,Object obj 表示一个本地引用, 存储在 JVM 栈的本地变量表中, 表示一个 reference 类型数据;
2,new Object()作为实例对象数据存储在堆中;
3, 堆中还记录了 Object 类的类型信息 (接口, 方法, field, 对象类型等) 的地址, 这些地址所执行的数据存储在方法区中;
二, 需要 GC 的原因:
应用程序对资源操作, 通常简单分为以下几个步骤:
1, 为对应的资源分配内存
2, 初始化内存
3, 使用资源
4, 清理资源
5, 释放内存
应用程序对资源 (内存使用) 管理的方式, 常见的一般有如下几种:
1, 手动管理: C,C++
2, 计数管理: COM
3, 自动管理:.NET,Java,PHP,GO...
但是, 手动管理和计数管理的复杂性很容易产生以下典型问题:
1. 程序员忘记去释放内存
2. 应用程序访问已经释放的内存
产生的后果很严重, 常见的如内存泄露, 数据内容乱码, 而且大部分时候, 程序的行为会变得怪异而不可预测, 还有 Access Violation 等.
.NET,Java 等给出的解决方案, 就是通过自动垃圾回收机制 GC 进行内存管理. 这样, 问题 1 自然得到解决, 问题 2 也没有存在的基础.
总结: 无法自动化的内存管理方式极容易产生 bug, 影响系统稳定性, 尤其是线上多服务器的集群环境, 程序出现执行时 bug 必须定位到某台服务器然后 dump 内存再分析 bug 所在, 极其打击开发人员编程积极性, 而且源源不断的类似 bug 让人厌恶.
三, GC 如何工作:
GC 的工作流程主要分为如下几个步骤:
1, 标记(Mark)---GC 的根节点也即 GC Root
2, 计划(Plan)
3, 清理(Sweep)
4, 引用更新(Relocate)
5, 压缩(Compact)
在 Java 中, 可以当做 GC Root 的对象有以下几种:
1, 虚拟机 (JVM) 栈中的引用的对象
2, 方法区中的类静态属性引用的对象
3, 方法区中的常量引用的对象(主要指声明为 final 的常量值)
4, 本地方法栈中 JNI 的引用的对象
四, 什么时候发生 GC;
1, 当应用程序分配新的对象, GC 的代的预算大小已经达到阈值, 比如 GC 的第 0 代已满
2, 代码主动显式调用 System.GC.Collect()
3, 其他特殊情况, 比如, Windows 报告内存不足, CLR 卸载 AppDomain,CLR 关闭, 甚至某些极端情况下系统参数设置改变也可能导致 GC 回收
五, 建议:
由于 GC 的代价很大, 平时开发中注意一些良好的编程习惯有可能对 GC 有积极正面的影响, 否则有可能产生不良效果.
1, 尽量不要 new 很大的 object, 大对象 (>=85000Byte) 直接归为 G2 代, GC 回收算法从来不对大对象堆 (LOH) 进行内存压缩整理, 因为在堆中下移 85000 字节或更大的内存块会浪费太多 CPU 时间.
2, 不要频繁的 new 生命周期很短 object, 这样频繁垃圾回收频繁压缩有可能会导致很多内存碎片, 可以使用设计良好稳定运行的对象池 (ObjectPool) 技术来规避这种问题.
3, 使用更好的编程技巧, 比如更好的算法, 更优的数据结构, 更佳的解决策略等等.
来源: http://www.bubuko.com/infodetail-3076982.html