「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
前言
在上一篇中,讲解了JVM对应的运行时数据区详解,继续上一篇的话题,本篇讲解JVM对应的对象分配过程完全解析
在开始之前,首先介绍一下HSDB工具使用
1、HSDB工具应用
如图所示
进入对应的JDK-Lib目录,然后输入java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
就会出现HSDB
窗体应用程序
然后运行对应的Demo代码
public class HSDBTest {
public HSDBTest() {
}
public static void main(String[] args) {
Teacher kerwin = new Teacher();
kerwin.setName("kerwin");
for(int i = 0; i < 15; ++i) {
System.gc();
}
Teacher jett = new Teacher();
jett.setName("jett");
StackTest test = new StackTest();
test.test1(1);
System.out.println("挂起....");
try {
Thread.sleep(10000000L);
} catch (InterruptedException var5) {
var5.printStackTrace();
}
}
}
复制代码
开启新的dos命令
如图所示
当运行成功后,在对应HSDB应用上输入对应的进程号就能看到对应进程的加载情况!
如图所示
如果说对应的HSDB一直出现加载情况,那么就得查看打开HSDB对应的dos命令页面上是否报错。
如果说报 UnsatisfiedLinkError异常
那么说明:JDK目录中缺失sawindbg.dll文件
如图所示
此时,就需要把自己其中\jre\bin目录下sawindbg.dll 粘贴到另一个\jre\bin 目录下,然后关闭HSDB,再次打开既ok
如图所示
在这里选择对应的main线程,Stack Memory
就能看到对应Stack详细信息!
如图所示
打开对应的Tools -heap parametes
就能看到对应的年轻代,老年代对应的起始点!
如图所示
从这两张图可知:年轻代里面包含Eden区,From区和To区,对应的内存地址块都在年轻代范围内!
OK!到这里,相信你对 年轻代和老年代里面具体划分有了一定的认知!!!
那么!年轻代和老年代它们之间是怎么运作的呢?为什么年轻代要分为Eden、From、To三个模块呢?
因此迎来了本篇重点:对象的分配过程,前面都是引子!
2、堆的核心结构解析
那么堆是什么呢?
2.1 堆概述:
- 一个JVM进程存在一个堆内存,堆是JVM内存管理的核心区域
- java 堆区在JVM启动是被创建,其空间大小也被确定,是JVM管理的 最大一块内存(堆内存大小可以调整)
- 本质上堆是一组在物理上不连续的内存空间,但是逻辑上是连续的 空间(参考上面HSDB分析的内存结构)
- 所有线程共享堆,但是堆内对于线程处理还是做了一个线程私有的 部分(TLAB)
那么堆的对象分配、管理又是怎么的呢?
2.2 堆的对象管理
- 在《JAVA虚拟机规范》中对Java堆的描述是:所有的对象示例以及数 组都应当在运行时分配在堆上
- 但是从实际使用角度来看,不是绝对,存在某些特殊情况下的对象产 生是不在堆上分配
- 这里请注意,规范上是绝对、实际上是相对
- 方法结束后,堆中的对象不会马上移除,需要通过GC执行垃圾回收后 才会回收
2.3 堆的内存细分
如图所示
- 堆区结构最外层分为:年轻代和老年代,比例为 1:2
- 年轻代里面又分为:Eden区和Survivo区,比例为8:2
- Survivo区,又分为From区和To区,比例为1:1
至于为什么要这样分配,这就和分代相互关联了!
那么!为什么要分代(年轻代和老年代)呢?
2.4 分代思想
- 不同对象的生命周期不一致,但是在具体使用过程中70%- 90的对象是临时对象
- 分代唯一的理由是优化GC性能。如果没有分代,那么所有对象在一块空间,GC想要回收扫描他就必须扫描所有的对象,分代之后,长期持有的对象可以挑出,短期持有的对象可以固定在一个位置进行回收,省掉很 大一部分空间利用
如图所示
- 那些临时对象就会放在年轻代里面,当对应临时对象,生命周期执行完毕时,将会触发临时对象的GC回收;
- 而老年代存放的是:生命周期长的对象,将不再由临时对象GC回收,而是由老年代对应的GC负责回收
- 如果这里没有分代,那么每次回收时,将会全员检测,相当耗费资源
2.5 堆的默认大小
默认空间大小:
- 初始大小:物理内存大小 / 64
- 最大内存大小:物理内存大小 / 4
那么如何查看本机空间大小呢?
public class EdenSurvivorTest {
public static void main(String[] args) {
EdenSurvivorTest test = new EdenSurvivorTest();
test.method1();
// test.method2();
}
/**
* 堆内存大小示例
* 默认空间大小:
* 初始大小:物理电脑内存大小 / 64
* 最大内存大小:物理电脑内存大小 / 4
*/
public void method1(){
long initialMemory = Runtime.getRuntime().totalMemory();
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("初始内存:"+(initialMemory / 1024 / 1024));
System.out.println("最大内存:"+(maxMemory / 1024 / 1024));
try {
Thread.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
运行结果
初始内存:245
最大内存:3621
复制代码
当然也可以使用jstat命令查看
如图所示
这里简单的提一下这里面的类型表示什么意思,[更多jstat命令查看](blog.csdn.net/u010399248/…l)
- 结尾C 代表总量
- 结尾U代表已使用量
- S0 S1代表 survivor区的From 与 To
- E代表的是 Eden区
- OC代表 老年总量 OU代表老年使用量
3、对象分配过程
到这里才开始讲解本篇的重点
注意:Java 阈值是15,Android阈值是6,这里就拿Android举例
3.1 正常分配过程
如图所示
所有变量的产生都在Eden区,当Eden区满了时,将会触发minorGC
如图所示
当minorGC 触发后,不需要的变量将会被回收掉,正在使用中的变量将会移动至From区,并且对应的阈值+1
如图所示
当下一次Eden区满了后,对应minorGC,将会带同From区、Eden区一起,标记对象
如图所示
回收成功后,对应的From区以及Eden区,正在使用的的都会进入To区,对应阈值+1
同理,当下一次Eden满了后,对应To区和Eden区都会被对应minorGC标记,正在使用中的对象又全部移动至From区,一直来回交替!对应的阈值也会自增
如图所示
当对应的From区或者To区存在未回收的对象的阈值满足进入老年代条件时,对应的对象将会移动至老年代!
当然在老年代里面,如果内存满了,也会触发Full GC,未被回收的对象阈值+1
为了加深印象,这里用一段小故事来描述整段过程!
-
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们 在Eden区中玩了挺长时间。
-
有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区, 我就开始了我漂泊的人生,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所
-
直到我18岁(阈值达到老年代)的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代 里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次 GC加一岁),然后被回收。
这就是一整段很标准的内存分配过程,那么如果存在特殊情况将会是怎样的呢?
比如说,产生的对象Eden直接装不下的那种
3.2 非正常分配过程
如图所示
进入老年代的方式有四种方式:
-
正常的阈值达到老年代要求
-
在From/To区放不下时也会晋升老年代(就是阈值没达到老年代,但是Eden产生的正在使用的对象过多)
-
对象申请时,Eden区直接放不下,将会直接进入老年代判断
-
如果Old区放的下,那就直接晋升老年代
-
如果Old区放不下,那就触发Major GC,如果放得下就晋升,否则就OOM
-
3.3 验证对象分配过程
3.3.1 短生命周期分配过程
说了这么多,来验证一把哇
public class EdenSurvivorTest {
public static void main(String[] args) {
EdenSurvivorTest test = new EdenSurvivorTest();
test.method2();
}
public void method2(){
ArrayList list = new ArrayList();
for (;;) {
TestGC t = new TestGC();
// list.add(t);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
复制代码
这里我们大概分析下代码,在for死循环里,对象TestGC
生命周期仅限于当前循环里,属于短生命周期对象,那么我们来看看具体是对象是如何分配的!
如图所示
打开JDK-BIN 目录,然后双击对应的exe
注意:
-
JDK11以上好像没有对应exe
-
首次打开该exe时,需要安装对应插件,然后关闭,再次打开即可!
一切准备就绪后,运行上面代码,然后打开该exe,就能看到
如图所示
图里面该说的都说了,不过注意的是,这里OLD区并没有任何数据!
因为在上面代码解析的时候就已经说了,产生的对象生命周期仅限于For循环里,并非长生命周期对象
那么能否举一个有长生命周期对象的例子呢?
3.3.2 长生命周期分配过程
public class EdenSurvivorTest {
public static void main(String[] args) {
EdenSurvivorTest test = new EdenSurvivorTest();
test.method2();
}
public void method2(){
ArrayList list = new ArrayList();
for (;;) {
TestGC t = new TestGC();
list.add(t);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
复制代码
运行该代码,然后再次查看刚刚的Exe
如图所示
因为对应变量的生命周期不再仅限于for内部,因此当阈值满足老年代要求时,将直接进入老年代
如图所示
因为老年代里面的对象一直持有,并没有未使用的对象,当老年代满了时,就会触发OOM异常!!
在上面提到过好几个GC,那么不同的GC有什么区别呢?
3.4 MinorGc/MajorGC/FullGC的区别
JVM在进行GC时,并非每次都对上面三个内存区域一起回收,大部分的只会针对于Eden区进行 在JVM标准中,他里面的GC按照回收区域划分为两种:
-
一种是部分采集(Partial GC ):
- 新生代采集(Minor GC / YongGC):(只采集新生代数据)
- 老年代采集(Major GC / Old GC):(只采集老年代数据,目前只有CMS会单独采集老年代)
- 混合采集(Mixed GC)(采集新生代与老年代部分数据,目前只有G1使用)
-
一种是整堆采集(Full GC):
- 收集整个堆与方法区的所有垃圾
3.4.1 GC触发策略
年轻代触发机制
- 当年青代空间不足时,就会触发MinorGc,这里年轻代满值得是Eden区中满了
- 因为Java大部分对象都是具备朝生熄灭的特性,所以MinorGC非常频繁,一般回收速度也快
- MinorGc会出发STW行为,暂停其他用户的线程
老年代GC触发机制:
- 出现MajorGC经常会伴随至少一次MinorGC(非绝对,老年代空间不足时会尝试触发 MinorGC如果空间还是不足则会出发MajorGC)
- MajorGC比MinorGC速度慢10倍,如果MajorGC后内存还是不足则会出现OOM
FullGC触发
- 调用System.gc()时
- 老年代空间不足时
- 方法区空间不足时
- 通过MinorGC进入老年代的平均大小大于老年代的可用内存
- 在Eden使用Survivor进行复制时,对象大小大于Survivor的可用内存,则该对象转入老年代,且 老年代的可用内存小于该对消
Full GC 是开发或者调优中尽量要避开的
3.4.2 GC日志查看
如图所示
在这里添加:-Xms9m -Xmx9m -XX:+PrintGCDetails
提交后,再次运行代码:
Connected to the target VM, address: '127.0.0.1:53687', transport: 'socket'
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->740K(9728K), 0.0032500 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2291K->504K(2560K)] 2544K->2280K(9728K), 0.0040878 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2343K->504K(2560K)] 4120K->4104K(9728K), 0.0010760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2341K->504K(2560K)] 5942K->5912K(9728K), 0.0013867 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 5408K->5741K(7168K)] 5912K->5741K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0044415 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 1859K->600K(2560K)] [ParOldGen: 5741K->6941K(7168K)] 7601K->7541K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0042249 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1836K->1800K(2560K)] [ParOldGen: 6941K->6941K(7168K)] 8778K->8742K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0018656 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 1800K->1800K(2560K)] [ParOldGen: 6941K->6925K(7168K)] 8742K->8725K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0043790 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 2560K, used 1907K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 93% used [0x00000000ffd00000,0x00000000ffedcfd8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 6925K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 96% used [0x00000000ff600000,0x00000000ffcc3688,0x00000000ffd00000)
Metaspace used 3369K, capacity 4556K, committed 4864K, reserved 1056768K
class space used 364K, capacity 392K, committed 512K, reserved 1048576K
复制代码
就能查看对应的GC日志了。
结束语
OK!到这里对象的分配过程已经讲完了!相信看到这的小伙伴已经对对象分配过程有了清晰的认知!在下一篇中,将会重点讲解GC与调优!