JVM概念汇总
JVM
垃圾收集
它基本上通过暂停它周围的世界来操作,标记所有根对象(由运行线程直接引用的对象),并遵循它们的引用,标记它沿途看到的每个对象
Java基于分代假设-实现了一种称为分代垃圾收集器的东西,该假设表明创建的大多数对象被快速丢弃,而未快速收集的对象可能会存在一段时间
分代描述
-
Young Generation -这是对象的开始。它有两个子代
-
Eden Space -对象从这里开始。大多数物体都是在Eden Space中创造和销毁的。
- 在这里,GC执行Minor GCs,这是优化的垃圾收集。执行Minor GC时,对仍然需要的对象的任何引用都将迁移到其中一个survivors空间(S0或S1)。
-
Survivor Space (S0 and S1)(from and to)-幸存Eden Space的对象最终来到这里。
-
其中有两个,在任何给定时间只有一个正在使用(除非我们有严重的内存泄漏)。
-
一个被指定为空,另一个被指定为活动,与每个GC循环交替。
-
-
为什么会有Young Generation?
-
分代的唯一理由就是优化GC性能。
-
如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。
-
-
为什么要有Survivor区?
-
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。
-
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
-
解决思路
-
增加老年代空间
-
更多存活对象才能填满老年代。降低Full GC频率
-
随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长
-
-
减少老年代空间
-
Full GC所需时间减少
-
老年代很快被存活对象填满,Full GC频率增加
-
-
-
-
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
-
为什么一个Survivor区不行?
-
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化
-
内存碎片化
-
碎片化带来的风险是极大的,严重影响Java程序的性能。
-
堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存,就会发现没有足够的内存给予分配。
-
-
刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1。
-
这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生
-
最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。
-
-
-
-
年轻代中的GC
-
新生代大小(PSYoungGen total 9216K)= eden大小(eden space 8192K)+ 1个survivor大小(from space 1024K)
- eden : survivor = 8 : 1
-
复制算法
-
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法.
-
复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。
-
复制算法不会产生内存碎片。
-
步骤
-
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
-
紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
-
年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
-
经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。
-
不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
-
-
-
-
-
Tenured Generation -也被称为老年代(图2中的旧空间),这个空间容纳存活较长的对象,使用寿命更长(如果它们活得足够长,则从Survivor空间移过来)。填充此空间时,GC会执行完整GC,这会在性能方面降低成本。如果此空间无限制地增长,则JVM将抛出OutOfMemoryError - Java堆空间。
-
PerGen (永久代)
Permanent Generation-
作为与终身代密切相关的第三代,永久代是特殊的,因为它保存虚拟机所需的数据,以描述在Java语言级别上没有等价的对象。例如,描述类和方法的对象存储在永久代中。
-
java8移除
-
标记清除算法 (Mark-Sweep)
-
过程
-
- 标记出来所有需要回收的对象
-
- 标记完成后统一回收所有被标记的对象
-
-
性能
-
标记,清除效率低
-
空间利用率低(清除后有空间碎片,导致有大的对象时,需要先回收一次内存
-
-
算法细节
-
Partial GC:并不收集整个GC堆的模式
-
Young GC:只收集young gen的GC
-
Minor GC
-
从年轻空间(包括Eden和幸存者空间)收集垃圾称为Minor GC。这个定义既清晰又统一。但是在处理小的垃圾收集事件时,你仍然需要注意一些有趣的信息:
-
Minor GC总是在JVM无法为新对象分配空间时触发,例如Eden已经满了。因此,分配率越高,执行Minor GC的频率就越高。
-
每当池被填满时,它的整个内容都会被复制,并且指针可以再次从零开始跟踪空闲内存。所以代替了传统的标记,清扫和紧凑,清洁Eden spaces 和 Survivor spaces进行标记和复制代替。因此,在Eden spaces 和 Survivor spaces内实际上不会发生分裂。写指针总是驻留在使用的池的顶部。
-
在Minor GC事件期间,终身生成实际上会被忽略。从成熟代到年轻代的引用被认为是事实上的GC根。在标记阶段,从年轻代到终身代的引用被简单地忽略。
与人们普遍认为的相反,所有较小的GCs都会触发“停止世界”暂停,从而停止应用程序线程。对于大多数应用程序,暂停的长度在延迟方面可以忽略不计。如果Eden中的大多数对象都被认为是垃圾,并且永远不会复制到Survivor/Old spaces,那么就会出现这种情况。如果相反,并且大多数新生对象都不适合GC,那么小的GC暂停将花费更多的时间。
因此,使用Minor GC时,情况相当清楚——每个Minor GC清除年轻代。
-
-
-
-
Old GC:只收集old Gen / Tenured Gen 的GC。只有CMS的concurrent collection是这个模式
-
Major GC
-
首先,许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。
-
指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里
就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10
倍以上。 -
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
-
Major GC is cleaning the Tenured space.
-
-
CMS
- 适合对响应时间的重要性需求 大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation(终生代)的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。
-
-
Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
-
G1(Garbadge First Collector)
-
可以解决CMS中Concurrent Mode Failed问题,尽量缩短处理超大堆的停顿,在G1进行垃圾回收的时候完成内存压缩,降低内存碎片的生成。
-
G1在堆内存比较大的时候表现出比较高吞吐量和短暂的停顿时间,而且已成为Java 9的默认收集器。未来替代CMS只是时间的问题。
-
原理
-
Region
-
G1将内存划分成了多个大小相等的Region(默认是512K),Region逻辑上连续,物理内存地址不连续。
-
同时每个Region被标记成E、S、O、H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。
-
H (Humongous)
-
H表示Humongous。从字面上就可以理解表示大的对象(下面简称H对象)。
-
当分配的对象大于等于Region大小的一半的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝。通过如果发现堆内存容不下H对象的时候,会触发一次GC操作。
-
-
-
-
-
-
-
Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
-
- system.gc();
-
- 旧生代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
- 旧生代空间不足
-
- Permanet Generation空间满
Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出错误信息:java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
- Permanet Generation空间满
-
- CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
应对措施为:增大survivor space、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
- CMS GC时出现promotion failed和concurrent mode failure
-
- 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
- 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
-
- 对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。
可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
- 对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。
- Full GC is cleaning the entire Heap – both Young and Tenured spaces.
-
-
-
gc弃用一些组合
-
DefNew + CMS
-
ParNew + SerialOld
-
Incremental CMS
-
对应的命令行选项会产生警告信息并且建议你不要使用这样的组合。这些命令行选项会在将来的某个主要版本中移除。
-
命令行选项-Xinggc被弃用
-
命令行选项-XX:CMSIncrementalMode被弃用。注意,这个命令行选项会影响所有的CMSIncremental选项。
-
命令行选项-XX:+UseParNewGC被弃用。除非你同时使用选项-XX:+UseConcMarkSweepGC。
-
命令行选项-XX:-UseParNewGC只有在和-XX:+UseConcMarkSweepGC选项一起使用时被弃用。
-
想要获得更多的信息,请查看 http://openjdk.java.net/jeps/173
-
-
数据区域
直接内存
(Direct Memory)
-
JDK1.4-NIO-基于Channel和缓冲区的I/O方式
-
使用Native函数库直接分配堆外内存,通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
- 在一些场景中提升性能,避免了在java堆中和Native堆中来回复制数据
-
-
异常
-
outOfMemory
- 配置时忽略直接内存,使得各个内存区域总和大于物理内存限制(物理的、操作系统级的限制),从而导致动态扩展时报错
-
-
不是运行时数据区的一部分
-
本机直接内存的分配不会受到java堆块大小的限制,但是收到本机总内存(RAM、SWAP或者分页文件)大小及处理器寻址空间的的限制
运行时数据区
-
程序计数器
-
线程私有
-
记录当前线程所执行的字节码的行号。
-
此 内存 区域 是 唯一 一个 在 Java 虚拟 机 规范 中 没有 规定 任何 OutOfMemoryError 情况 的 区域。
-
每个线程对应一个计数器
-
独立存储,互不影响
- 这类内存成为“线程私有”
-
-
概念模型(不同虚拟机会有更高效的做法)中的做法:
-
字节 码 解释器 工作 时 就是 通过 改变 这个 计数器 的 值 来 选取 下一 条 需要 执行 的 字节 码 指令, 分支、 循环、 跳 转、 异常 处理、 线程 恢复 等 基础 功能 都 需要 依赖 这个 计数器 来 完成。
-
一个确定的时刻,一个处理器(内核)只会处理一个线程。所以,计数器保管了线程中字节码的行号,下次调用的时候能够快速定位(恢复线程)
-
-
正在执行的是Native方法,计数器值为空(Undefined)
- 怎么恢复?
-
-
Java虚拟机栈
(Java Virtual Machine Stacks)-
线程私有
-
生命周期与程序计数器一致
-
每个方法的开始到结束对应
- 一个栈帧(在虚拟机栈中)从入栈到出栈
-
-
Java方法内存执行模型:
-
每个方法再执行的同时都会创建一个栈帧(Stack Frame)
-
栈帧
-
存储内容
-
局部变量表(俗称的“栈”)
-
存放
-
对象引用(reference) (不等同于对象本身)
-
有可能是
-
对象起始地址的引用指针
-
与此对象相关的位置
-
returnAddress 类型( 指向 了 一条 字节 码 指令 的 地址)
-
代表对象的句柄
-
-
-
编译期间可知的基本数据类型
boolean, byte, char, short, int, float, long, double- 64位长度的long, double占用2个局部变量空间(Slot)
-
-
局部变量表所需的内存空间在编 译期间完成分配
- 当进入一个方法时,这个方法需 要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
-
-
动态链接
-
方法出口
-
操作数栈
-
-
栈帧是方法运行时的基础数据结构
-
-
-
-
为虚拟机执行java字节码服务
-
异常
-
StackOverflowError
-
线程请求的深度大于虚拟机所允许的深度,则抛出异常
- 使用- Xss 参数 减少 栈 内存 容量。
-
多线程
-
不能减少线程数的前提
- 通过减少最大堆内存和减少栈容量(减少大量的本地变量,本地变量表长度减少)来换取更多线程
-
每个线程分配的栈容量越大,建立的线程数就越少,建立时就容易耗尽内存
-
建立非常多的线程
- 为每个线程分配的栈空间内存越大越容易内存溢出
-
-
单线程下
- 栈帧过大or虚拟机栈过小
-
-
OutOfMemoryError
-
如果虚拟机栈可以动态扩展(当前大部分的java虚拟机都可动态扩展,只不过虚拟机规范中也允许固定长度的虚拟机栈),扩展没有申请到足够的内存,则抛出异常
-
32位windows限制位2G
-
系统限制内存-Xmx(最大堆容量)-MaxPermSize(最大方法区容量)-程序计数器消耗的内存(很小,可忽略)
- (虚拟机进程本身耗费不计算在内)剩下的内存由虚拟机栈和本地方法栈“瓜分”
-
-
-
-
栈深度
-
栈的高度
- 虚拟机默认设置下,合适:1000~2000
-
栈帧越多,高度越高
-
栈帧越大,高度越小
-
-
-
方法区
(Method Area)-
线程共享
-
Non-Heap
-
存储
-
已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
-
类信息
-
常量
-
静态变量
-
即时编译器后的代码
-
etc
-
-
永久代 PerGen(java8已取消)
-
常量的回收,类的卸载
-
JDK1.7中Hotspot,已经把原本放在永久代中的字符串常量池移出
-
对象类型数据
-
-
运行时常量池 (runtime constant pool)
-
异常
-
outOfMemory
- 当无法申请到内存时会抛出
-
-
常量池(runtime constant table),存放编译时期生成的各种字面量和符号引用,也在运行时常量池中保存
-
一般来说
-
翻译出来的直接引用
-
保存class文件中描述的符号引用
-
-
Java 语言并不要求常量一定要只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中
- String.intern()
-
-
异常
-
outOfMemoryError
- 方法区无法满足内存分配需要
-
-
-
java堆
(java heap)-
所有线程共享
-
虚拟机中管理内存最大的一块
-
唯一目的存放对象
- 几乎所有的对象都在这里存放
-
垃圾收集器管理的主要区域
- “GC堆”
-
内存回收角度来看
(基本采用“分代收集算法”)-
新生代
-
旧生代
-
-
内存分配角度来看
- 多个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB)
-
Java堆可以存储在不连续的物理空间中,只要逻辑是连续的即可
-
-Xmx和-Xms控制
-
大多数虚拟机都是按照可扩展来实现的
-
-
进一步的划分是为了更好的分配内存或回收内存
-
虚拟机规范描述:所有的对象及数组都要在堆上分配
-
但是,随着…
所有对象并不是绝对分配在堆上了-
JIT编译器的发展
-
栈上分配
-
逃逸分析技术逐渐成熟
-
标量替换优化技术逐渐成熟
-
-
-
在虚拟机启动时创建
-
异常
-
outOfMemoryError
-
如果在堆中没有内存完成实例分配,并且堆也无法再扩展的时,则抛出异常
- 只要 不断 地 创建 对象, 并且 保证 GC Roots 到 对象 之间 有可 达 路径 来 避免 垃圾 回收 机制 清除 这些 对象, 那么 在 对象 数量 到达 最 大堆 的 容量 限制 后 就会 产生 内存 溢出 异常
-
(内存溢出)进一步提示:Java heap space
-
内存 映像 分析 工具( 如 Eclipse Memory Analyzer) 对 Dump 出 来的 堆 转储 快照 进行 分析, 重点 是 确认 内存 中的 对象 是否 是 必要 的, 也就是 要 先 分清 楚 到底 是 出现 了 内存泄漏( Memory Leak) 还是 内存 溢出( Memory Overflow)
- 掌握 了 泄露 对象 的 类型 信息 及 GC Roots 引用 链 的 信息, 就可以 比较 准确 地 定位 出 泄露 代码 的 位置
-
不存在泄露
- 就是 内存 中的 对象 确实 都 还 必须 存活 着, 那就 应当 检查 虚拟 机 的 堆 参数(- Xmx 与- Xms), 与 机器 物理 内存 对比 看 是否 还可以 调 大, 从 代码 上 检查 是否 存在 某些 对象 生命 周期 过长、 持有 状态 时间 过长 的 情况, 尝试 减少 程序 运 行期 的 内存 消耗
-
-
-
堆 参数(- Xmx 与- Xms),一旦设定自动扩展就不可用
-
-
-
本地方法栈
-
为虚拟机制执行Native方法服务
-
没有限制,可由虚拟机自行设计
-
JAVA8
PermGen
(永久代)
-
此内存空间已完全删除
-
PermSize和MaxPermSize JVM参数将被忽略,如果在启动时出现警告,则会发出警告
-
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
一些其他数据已移至Java堆空间。 这意味着在将来的JDK 8升级之后,您可能会发现Java堆空间的增加
新生代:Eden+From Survivor+To Survivor
老年代:OldGen
永久代(方法区的实现) : PermGen
替换为Metaspace(本地内存中)
Metaspace 元空间
-
元空间的内存大小
-
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
-
理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数
-
-
常用配置参数
-
MetaspaceSize
- 初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数
-
MaxMetaspaceSize
- 限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)
-
MinMetaspaceFreeRatio
- 当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
-
MaxMetasaceFreeRatio
- 当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%
-
MaxMetaspaceExpansion
- Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。
-
MinMetaspaceExpansion
- Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)
-
对象
创建
(虚拟机角度,java角度,init还没有执行)
-
在常量池找寻类的符号引用
-
检查这个符号引用代表的类是否被加载和初试化过
-
没有
- 执行类的加载过程
-
有
-
新生对象分配内存
-
分配内存大小在类加载的时候就可以完全确定 (在java堆中分出确定大小的内存来)
-
分配方式
(java堆是否规整决定)-
内存是规整
-
“指针碰撞”(Bump the Pointer)
-
指针是使用的及未使用的分界
-
划分:指针向未使用的区域移动对象大小的距离
-
-
-
内存不是规整的
-
“空闲列表”(Free List)
-
已用未使用相互交错
-
虚拟机维护一个列表,记录那些是可用的
-
分配的时候,在列表中找一块足够大的内存给对象,并更新列表的记录
-
-
-
是否规整
- 采用的垃圾回收器是否有压缩功能
-
-
-
-
-
-
分配内存的指针在并发下并不是线程安全的
-
解决方案
-
分配内存空间的动作进行同步处理
- 失败重试的保证更新操作的原子性
-
分配内存的动作按照不同线程,划分在不同的空间中进行处理
-
每个java线程在java堆中预先分配一小块内存,这个内存称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
-
TLAB用完重新分配,才需要同步锁定
-
是否启用TLAB,参数:-XX:+/-UseTLAB
-
对象实例字段在不出初始化的时候就可以使用,初始化零值(不包过对象头)
-
-
-
设置
- 保存在对象头(Object header)
内存分布
-
对象头
(object header)-
存储自身的运行时数据
(Make word)-
如 哈 希 码( HashCode)、 GC 分 代 年龄、 锁 状态 标志、 线程 持 有的 锁、 偏向 线程 ID、 偏向 时间 戳 等
- Mark Word 被 设计 成 一个 非 固定 的 数据 结构 以便 在 极小 的 空间 内存 储 尽量 多的 信息, 它 会 根据 对象 的 状态 复 用 自己的 存储 空间。
-
-
类型指针
-
对象指向类元数据的指针
- 虚拟机通过这个指针确定对象所属类实例
-
并不是 所有 的 虚拟 机 实现 都 必须 在 对象 数据 上 保留 类型 指针, 换句话说, 查找 对象 的 元 数据 信息 并不 一定 要 经过 对象 本身,
-
-
如果 对象 是 一个 Java 数组, 那 在 对象 头中 还 必须 有 一块 用于 记录 数组 长度 的 数据,因为可以从对象的元数据判断对象大小,但从数据的元数据无法判断数组大小
-
-
实例数据
(instance data)-
也是 在 程序 代码 中 所 定义 的 各种 类型 的 字段 内容。 无论是 从父 类 继承 下来 的, 还是 在 子类 中 定义 的, 都 需要 记录 起来。
- 这 部分 的 存储 顺序 会受 到虚拟 机 分配 策略 参数( FieldsAllocationStyle) 和 字段 在 Java 源 码 中 定义 顺序 的 影响。 HotSpot 虚拟 机 默认 的 分配 策略 为 longs/ doubles、 ints、 shorts/ chars、 bytes/ booleans、 oops( Ordinary Object Pointers), 从 分配 策略 中 可以 看出, 相同 宽度 的 字段 总是 被 分配 到一起。 在 满足 这个 前提 条件 的 情况下, 在 父 类 中 定义 的 变量 会 出现 在 子类 之前。 如果 CompactFields 参 数值 为 true( 默认 为 true), 那么 子类 之中 较窄 的 变量 也可能 会 插入 到 父 类 变量 的 空隙 之中。
-
-
对齐填充
(padding)-
并不是必然存在
-
没有特别含义仅仅是占位符
- 由于 HotSpot VM 的 自动 内存 管理 系统 要求 对象 起始 地址 必须 是 8 字节 的 整 数倍, 换句话说, 就是 对象 的 大小 必须 是 8 字节 的 整 数倍。 而对 象头 部分 正好是 8 字节 的 倍数( 1 倍 或者 2 倍), 因此, 当 对象 实例 数据 部分 没有 对齐 时, 就 需要 通过 对齐 填充 来 补 全。
-
访问定位
-
通过栈上reference来访问堆上的对象数据
-
虚拟机只规定了reference是一个指向对象的引用
-
指针访问
-
Hot spot的实现方式
-
速度快 (节省了一次指针定位的时间开销)
-
Reference存储的是对象直接地址
- 到对象类型的指针
-
-
-
-
句柄访问
-
更稳定 (垃圾回收的时候改变的只是句柄中的实例指针,而reference本身不需要修改)
-
Reference存储的是对象的句柄地址
- 句柄地址包含:
1.(java堆.实例池)对象实例数据(地址) 2.(方法区)类型数据各自的具体地址信息(地址)
- 句柄地址包含:
-
Java堆划分出来内存做句柄池
-
-
-
基础概念
字节码
-
通常情况下它是已经经过编译,但与特定机器码无关。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。
-
字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码
符号引用
- 软链接
字面量
机器码
- 机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据
分析
使用Java VisualVM远程分析堆
- VisualVM是一种工具,它提供了一个可视化界面,用于查看有关基于Java技术的应用程序运行时的详细信息。
MemLeak
泄漏诊断
识别症状
-
通常,如果Java应用程序请求的存储空间超过运行时堆提供的存储空间,则可能是由于设计不佳导致的。例如,如果应用程序创建映像的多个副本或将文件加载到数组中,则当映像或文件非常大时,它将耗尽存储空间。这是正常的资源耗尽。该应用程序按设计工作(虽然这种设计显然是愚蠢的)
-
但是,如果应用程序在处理相同类型的数据时稳定地增加其内存利用率,则可能会发生内存泄漏。
-
此GC跟踪文件中的每个块(或节)按递增顺序编号。要理解这种跟踪,您应该查看连续的分配失败节,并查找随着时间的推移而减少的释放内存(字节和百分比),同时总内存(此处,19725304)正在增加。这些是内存耗尽的典型迹象
启用详细垃圾回收
-
verbosegc
-
断言确实存在内存泄漏的最快方法之一是启用详细垃圾回收。通常可以通过检查verbosegc输出中的模式来识别内存约束问题。
具体来说,-verbosegc参数允许您在每次垃圾收集(GC)过程开始时生成跟踪。也就是说,当内存被垃圾收集时,摘要报告会打印到标准错误,让您了解内存的管理方式。
-
启用分析
- 不同的JVM提供了生成跟踪文件以反映堆活动的不同方法,这些方法通常包括有关对象类型和大小的详细信息。这称为分析堆。
分析踪迹
- 跟踪可以有不同的格式,因为它们可以由不同的Java内存泄漏检测工具生成,但它们背后的想法总是相同的:在堆中找到不应该存在的对象块,并确定这些对象是否累积而不是释放。特别感兴趣的是每次在Java应用程序中触发某个事件时已知的临时对象。应该仅存少量,但存在许多对象实例,通常表示应用程序出现错误。