JVM内存管理学习

一.内存区域
1.线程私有内存:
(1)程序计数器
(2)虚拟机栈

存放着编译器可知的各种基本数据类型, 对象引用(可能是一个引用指针或者一个句柄)和 returnAddress(指向一条字节码指令的地址).

(3)本地方法栈
2.线程共享内存
(1)堆(粗略分为年轻代, 老年代, 年轻代可细分为Eden, From Survivor, To Survivor)
(2)方法区(包括运行时常量池(字符串常量池))

存储虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码等数据. 运行时常量池:用于存放编译器生成的各种字面量和符号引用

(3)直接内存(不是运行时数据区, 也不是规定的内存区域)类似NIO只用的Buffer区域

二.内存创建, 布局, 访问举例(Hot Spot 中对象的分配, 普通对象不指数组Class对象)
(1)对象创建, 包括克隆, 反序列化(首先检查在方法区中能否定位到这个类的符号引用, 并检查类有没有加载解析初始化)
A.内存空间划分:Serial, ParNew等Compact类型的收集器使用指针碰撞方式分配, 而CMS类型的收集器使用Mark-Seep算法进行分配(这关乎内存空间分配是使用指针碰撞的方法还是使用空闲列表的方法)
补充: 分配过程存在线程竞争问题可以有两种方法解决:
a.采用CAS配上失败重试保证更新的原子性
b.每个线程现在堆中分配出本地线程分配缓冲(Thread Local Allocation Buffer TLAB), 分配时候现在TLAB中分配, TLAB用完重新分配需要同步锁定

B.内存初始化 : 内存空间初始化为零

C.对象设置 : 设置属于哪个类, 如何找到类的元数据信息, 对象的哈希码, 对象GC分年代等信息(这些信息存放在对象头里面)

(2)对象的内存布局
A. 对象头 :
a.存储对象自身运行时数据如:哈希码, GC分代年龄, 锁状态标志, 线程持有的锁, 偏向线程ID, 偏向时间戳(这部分数据被称为Mark Word)
b.类型指针(指向类元数据的指针)

B.实例数据(父类, 子类中字段的数据)

C.对齐填充(不是必然存在的, 仅仅起到占位符的作用)

(3)对象的访问定位(根据具体虚拟机的实现一般有两种方式)
A.句柄
内存中创建了句柄池(reference存储的就是对象的句柄地址), 句柄中包含了对象实例数据(在堆中)还有对象类型数据(在方法区中), 优点就是reference中存放的是稳定的句柄地址 , 对象移动时候只需要修改句柄指向的地址就可以了

B.直接指针
如果使用直接指针必须访问, 那么必须在堆对象中考虑如何设置对象类型数据的相关信息(如在对象中有指针直接指向方法区中的对象类型数据), reference中存储的是堆中的对象实例地址, 优点是访问速度更快了

三.实战 : OutMemoryError
目的 : 一.验证各个区域的存储内容, 二.根据出错信息判断哪个区域出现OOM
(1)堆
虚拟机启动设置堆参数:
VM Args : -Xms20m -Xmx20m -XX:+heapDumpOnOutOfMemoryError
-Xms 堆的最小值
-Xmx 堆的最大值(最大值等于最小值意味着不可扩展)
-XX:+HeapDumpOnOutOfMemoryError可以在虚拟机出现内存溢出异常时Dump出当前的堆转储快照以便分析
解决异常 :
通过内存映像分析工具(Eclipse MEMORY Analyzer)重点确认内存中对象是否必要的, 分清楚内存泄露还是内存溢出
如果内存泄露, 进一步通过工具查看泄露对象到 GC Roots的应用链
否则是内存溢出, 那么应该查看对参数-Xms -Xmx是否还可以调大, 分析哪些对象的生命周期过长

(2)虚拟机栈和本地方法栈溢出
-Xoss设置本地方法栈大小(但是对HotSpot无效)
栈容量只用-Xss参数设置
虚拟栈和本地方法栈有两种异常 :
A.线程请求的栈深度大于虚拟机所允许的最大深度, 抛出StackOverflowError异常
B.虚拟机在扩展栈的时候无法申请到足够的内存空间, 抛出OutOfMemoryError异常

VM Vrgs: -Xss128k
a.使用-Xss参数减少栈内存的容量, 抛出StackOverflow异常
b.定义大量的本地变量, 增大本地变量表的长度, 抛出StackOverflow异常

(3)方法区和运行时常量池溢出
VM Args: -XX:PermSize =10M -XX:MaxPermSize = 10M
设置方法区的大小为10M, 方法区的最大大小为10M

(4)本机直接内存溢出
VM vrgs: -XX:MaxDirtDirectMemorySize指定, 如果不指定 则默认与堆最大值一样
内存溢出之后的明显特征是 Dump文件很小, 而是程序中直接或间接使用NIO

四, 垃圾回收器(这里只针对堆, 方法区, 线程相关的内存区域只需要线程结束时释放即可, 不需要什么回收算法)
1.对象标记算法
A.引用计数算法
如果A, B实例互相引用, 而实际上A, B已经没用, 那么使用引用计数无法回收该类对象

B.可达性分析算法(主流实现)
通过对象到GC Root的可达性分析该对象是否可回收
java中可作为GC Root的对象包括以下几种
**a.虚拟机栈中引用的对象
b.方法区中静态属性引用的对象
c.方法区中常量引用的对象
d.本地方法栈JNI(一般说Native引用)的对象**
由于现实的原因, 需要根据当前内存 所剩的状况判断一个可有可无的对象是否存活, JDK1.2以后扩充了引用的概念
a.强引用(Strong Reference), 指在代码中普遍存在, 只要存在垃圾收集器就不会回收
b.软引用(Soft Reference), 指一些有用但非必需的对象, 将在发生内存溢出之前将其列为回收范围进行第二次回收, 如果回收不够才会抛出 内存溢出异常
c.弱引用(Weak Reference), 指一些非必需对象, 只能生存到下一次垃圾收集之前(而无论内存是否足够)
d.虚引用(Phantom Reference)幽灵引用幻影引用, 根本无法通过虚引用来操作一个对象, 设置该类引用的唯一目的是在进行垃圾收集的时候能收到一个系统通知

C.判断对象生死的标准
可达性分析中不可达只是判了缓刑, 真正判定死亡只扫需要两次标记过程:
a.需要判断该对象是否必要执行finalize(), 或者是否已经执行过finallize()
b.如果有必要执行finalize()则将其放进一个F-Queue队列之中, 所以最后重生机会在于,之后GC会对队列中的对象进行第二次小规模标记, 要挽救自己只能被重新引用连接到应用链上去

2.回收方法区
回收大量是来自堆中的年轻代的回收(但不代表方法区中不需要垃圾回收机制)
收集主要包括两部分的内容:
(1)废弃的常量, 与堆中的对象回收相似, 当没有其他引用引用了了该常量, 或者其他地方也没有引用该字面量, 则会在下一次内存回收的时候必要的时进行回收
(2)无用的类 , 需要满足三个条件:
a.该类的实例都已被回收
b.加载该类的ClassLoader已经被回收了
c.该类的java.lang.Class对象没有在任何地方被引用, 无法通过反射访问该类
满足以上条件只是可以回收不是必定回收(而是HotSpot提供了参数进行控制)

3.垃圾收集算法
(1)标记-清除算法
不足: 标记跟清除的过程效率都不高, 产生大量不连续的内存碎片

(2)复制算法
优点:实现简单, 运行高效
不足:代价是内存的使用被缩为原来的一半, 如果在对象存活率较高的时候复制的效率低
改进: 商用的时候将内存分配成一个Eden, 两个Survivor区域, 比例为80%, 10%, 10%

(3)标记-整理算法
老年代比较适合, 同样的标记过程之后不进行复制, 而是将内存向一端移动, 直接清理端边界以外的内存

(4)分代收集算法
当前的商用虚拟机采用的, 根据对象存活周期将内存划分为, 一般将堆划分为新生代, 老年代, 根据各个年代的特点采用适当的收集算法
新生代采用复制算法, 老年代采用标记-清理或者标记-整理算法

4.HotSpot的实现
(1)枚举根节点, 从GC Root查找引用链, 期间会发生Stop The Word(即停止所有的Java 线程), 使用OopMap数据结构协助HotSpot快速且准确的完成了GC Root枚举

(2)关于安全点, 安全区的一些描述

(3)垃圾收集器
a.Serial收集器, 单线程并且会出现Stop The World 的现象, 客户端模式下默认新生代收集器

b.ParNew收集器, 是Serial的多线程版本, Server模式下的新生代收集器

c.Parallel Scavenge收集器并行多线程收集器(吞吐量优先收集器), 新生代收集器, 使用复制算法, 目标是达到一个可控制的吞吐量, 主要适合后台运算而不需要太多交互任务(并且有自适应调节策略 )

d.Serial Old收集器, 单线程收集器, 使用标记-整理算法, 也是适用Client模式下虚拟机使用

e.Parallel Old 是Parallel Scavenge收集器的老年代版本, 使用多线程和标记-整理算法

f. CMS收集器, 是一种获取最短暂停顿时间目标收集器(适用于互联网站或者B/S体统的服务端), 使用标记-清除算法, 标记过程为:
初始标记(Stop The World), 标记GC Roots能直接关联到的对象速度很快
并发标记, GC Roots Tracing过程
重新标记(Stop The World), 修正并发标记期间用户程序
并发清除
优点 :
并发收集, 低停顿

g.G1收集器, 当今收集器技术发展最前沿成果之一, 面向服务器端的垃圾收集器 , 替换掉CMS收集器
特点:
(1)并行与并发, 缩短Stop The World停顿的时间
(2)分代收集, 不需要配合其他收集器就能独立管理整个GC堆
(3)空间整合, 从整体上看是属于标记-整理算法, 从局部(两个Region之间)是基于复制算法
(4)可预测的停顿
如果不维护Remembered Set的操作, 步骤如下
初始标记
并发标记
最终标记
筛选回收

(4)理解GC日志

(5)内存分配与回收策略
测试环境为Client环境下的虚拟机运行(使用Serial/Serial Old)
a.对象优先在Eden分配, 没有足够空间是发起一次Minor GC, 如果Survivor区不够分配直接将Eden的对象进入担保的老年区
b.大对象直接进入老年代
c.长期存活的对象将进入老年代
d.动态对象年龄判定

(6)空间分配担保
在发生Minor GC之前虚拟机会检查老年代最大可能的连续空间是否大于新生代所有对象总空间, 如果不是要查看虚拟机是否运行担保失败, 最终如果不进行冒险,则要改进为Full GC(目的是让老年代腾出更多的空间), 如果选择冒险却担保失败那也得进行Full GC

补充 : Minor GC与Full GC分别在什么时候发生
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。对于一个拥有终结方法的对象,在垃圾收集器释放对象前必须执行终结方法。但是当垃圾收集器第二次收集这个对象时便不会再次调用终结方法。
(1)Minor GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区,然后整理Survivor的两个区。这种方式的GC是对新生代的Eden区进行,不会影响到老年代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。
(2)Full GC
对整个堆进行整理,包括Young(年轻代)、Tenured(老年代)和Perm(持久代)。Full GC因为需要对整个对进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
**• 老年代(Tenured)被写满
• 持久代(Perm)被写满
• System.gc()被显示调用**

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值