1. 操作系统与应用层面的内存管理
内存管理:内存分配+垃圾回收
动态分配和回收内存;复用已回收的内存
每个对象存储在内存中一段连续的空间中,包括header和fields。
对象可以包含对其他对象的引用,则存储它所指向的对象的内存地址
为新对象分配内存的基本操作:
- 在内存里创建一个新对象
- 将其与某个引用关联起来
- 初始化其内部各域
对象模型:
- 对象在heap堆中分配内存
- 对象引用:指向其他对象在堆中的起始地址
- 非基本数据类型的变量等价于对象引用
2. 内存管理的三种模式
静态内存分配:在编译阶段就已经确定好了内存分配
动态内存分配:在运行时动态分配内存,建立新的内存对象
基于堆和栈的内存管理都是动态分配
2.1 静态
Fortran
在将程序load进内存的时候或开始执行时,确定所有对象的分配,运行时无法改变 。
不支持递归,不支持动态创建的可变长的复杂数据类型
2.2 动态,基于栈
Pascal C
存储方法调用以及方法执行中的局部数据
- 当方法被调用,它的栈帧被放到call stack的最上面
- 栈帧保存着方法的状态,包括哪一行代码在执行和所有局部变量的值
- 栈顶的方法是正在运行的方法
无法支持复杂数据类型
2.3 动态,基于堆
堆:内存的一部分,分为多个小块,每块包含一个对象,或者未被占用
代码中的一个变量ref可在不同时间被指向到不同的内存对象上,无法在编译阶段确定。内存对象也可以进一步指向其他对象。
自由模式的内存管理,动态分配,可管理复杂的动态数据结构
3. Java内存模型
3.1 栈
JVM中运行的每个线程都有自己的线程栈(thread stack)
线程栈中的信息包括该线程在运行到当前点之前都调用过哪些方法,和运行的每个方法的所有局部变量。
一个线程只能访问自己的线程栈;被某线程创造的局部变量只对创造它的线程可见。
所有局部的基本数据类型都在栈上创建。
一个线程可以将原始变量的副本传递给另一个线程,但它不能共享原始局部变量本身。
3.2 堆
堆包含了Java应用中创建的所有对象,无论是哪个线程创建的。
包括基本数据类型的包装类(Byte,Integer,Long…)
作为局部变量的对象和作为其他对象成员变量的对象都在堆上存储。
堆上创建的对象可被所有线程共享引用。
如果线程可以访问对象,就可以访问对象内的成员变量。
如果两个线程调用同一个对象上的某个方法,它们分别保留该方法的局部变量的拷贝。
3.3 JVM的内存结构
https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap
1.栈:向方法中传参数;返回方法的结果;存储表达式计算出来的立即数;存储局部变量
2.堆:存对象,数组
3.本地方法栈:JVM用来管理本地方法(C语言编写)
4.PC:代码行号指示器,用于指示,跳转下一条需要执行的命令。(指向的是Java指令)
5.Method Area:用于存储被VM加载的类信息、常量、静态变量等
HotSpot JVM中用Permanent Area (Perm)实现该区域,并作为heap的一部分
Java 8之后改名为Metaspace (使用native memory?本机内存(虚拟机外部))
4. 垃圾回收
4.1 三种模式下的内存回收
在静态内存分配模式下,无需进行内存回收:所有都是已确定的
在栈上进行内存空间回收:按block(某个方法)整体进行
在堆上进行内存空间回收:复杂,无法提前预知某个object是否已经变得无用。
4.2 可达与不可达对象
活对象:从root可达的对象
死对象:从root不可达
从root对象开始进行有向图的搜索,将图分为root可达部分和root不可达部分,后者将被进行内存回收。
4.3 垃圾回收的定义
垃圾回收器根据对象的“活性”(从root的可达性)来决定是否回收该对象的内存。
“死”的对象就是需要回收的“垃圾”。
垃圾回收GC:识别“垃圾”对象,把其占用的内存加以回收。
GC代价的衡量标准:
- 执行时间
- 延迟时间(幽灵时间)
- 所占用的内存/对程序所使用内存的影响
…
5. 垃圾回收的基本算法
5.1 Reference counting 引用计数
基本思想:为每个object存储一个计数RC,当有其他reference指向它时,RC++;当其他reference与其断开时,RC–;如果RC==0,则回收它 (及其所有指向的object)。
优点:简单;计算代价分散;“幽灵时间”短
对count的修改是在每行代码执行后进行,而非集中进行
缺点:不全面(容易漏掉循环引用的对象);并发支持较弱;占用额外内存空间等
5.2 Mark-Sweep 标记-清除
标记:为每个object设定状态位(live/dead)并记录
清除:将标记为dead的对象进行清理
缺点:需要停止程序执行来Mark和Sweep,导致幽灵时间过长,也影响程序本身的性能;内存碎片化;时间复杂度高(O(heap))
5.3 Mark-Compact 标记-整理
标记不被清除的对象;把有标记的对象移到堆的最后区域,把前面的对象都回收。
优点:避免碎片化;剩余内存的相对位置保持不变;
缺点:时间消耗太长,影响程序本身
5.4 Fragmentation and Copying 复制
将live对象全部复制到另一个区域。
将堆分为两个区域:Fromspace和Tospace
在Tospace中为对象分配内存,当Tospace满时,交换两个区域的职能,挑出Fromspace中所有的live对象,复制到Tospace,更新引用,使用Tospace中的对象作为工作队列。
6. JVM中的垃圾收集
JVM自动回收不再使用的对象。
使用参数“-verbose:gc”在控制台或日志文件中输出JVM进行GC的全过程。
Java GC将堆分为不同的区域,各区域采用不同的GC策略,以提高GC的效率。
JVM有三个主要的区域: young generation, old generation, permanent generation
Java应用程序中对象分配内存首先分配到young generation space,如果该对象经历了几次GC还存活,则移到old generation space。
GC策略:
young generation:Copy
old generation:Mark-Sweep或Mark-Compact
只有当某个区域不能再为对象分配内存时(满),才启动GC
针对young generation,使用minor GC进行垃圾收集。Minor GC所需时间较短,如果历经多次minor GC仍存活下来,将其copy到old generation。如果old generation满了,则启动full GC,Minor GC和full GC独立进行,减小代价 。
eden区域满的时候,触发minor GC.
被引用的对象移动到S0,清除eden时未被引用的对象被清除。
下一次minor GC,eden和S0中存活的对象移动到S1,eden和S0都被清空。
下一次minor GC,重复相同过程。
当一个对象在S0和S1中反复移动达到一定次数,它将被移动到old generation
7. JVM中的垃圾收集的调优
7.1 调整VM Heap Size
堆的大小决定着VM将会以何种频度进行GC、每次GC的时间多长。这两个指标具体取值多少为“优”,需要针对特定应用进行分析。较大的heap会导致较少发生GC,但每次GC时间很长,如果根据程序需要来设置heap大小,则需要频繁GC,但每次GC的时间较短。
-Xms比-Xmx小时,young generation和old generation的总大小可可以随时间变化(在这两个参数之间变化)。heap尺寸变化时需要full GC。
- -XX: NewSize=<n>[g|m|k]:young generation区域的初始大小和最小大小。<n>是大小,[g|m|k]是单位。
- -XX: MaxNewSize=<n>[g|m|k]:young generation区域的最大大小
- -Xmn<n>[g|m|k]:young generation区域的初始、最小及最大大小
- -XX:NewRatio=<n>:young generation与old generation区域大小的比例是1:n
- -XX:SurvivorRatio=<n>:eden和survivor区域大小的比例是n:1
- -XX:MinHeapFreeRatio=<minimum> (default = 40%)
- -XX:MaxHeapFreeRatio=<maximum> (default = 70%)
…
7.2 调节GC方式
1.-XX:+UseSerialGC:用单线程处理垃圾回收工作。
2.-XX:+UseParallelGC -XX:ParallelGCThreads=n:minor gc多线程,full gc单线程
3.-XX:+UseParallelOldGC:minor gc,full gc都是多线程
4.-XX:+UseConcMarkSweepGC -XX:ParallelCMSThreads=n
5.–XX:+UseG1GC
7.3 输出/记录GC信息
-verbose:gc
可将GC信息记录到log中,以便于后续分析
7.4 手动请求GC
System.gc()