JVM系列之:内存与垃圾回收篇(二)
##本篇内容概述:
1、堆Heap Area
2、方法区Method Area
3、运行时数据区总结
4、对象的实例化内存布局和访问定位
一、堆 Heap Area
1、堆的核心概念
·一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
·Java堆区在JVM启动的时候即被创建,其空间大小也就被确定下来了。是JVM管理的最大一块内存空间。
·堆可以处于物理上不连续的内存空间中,但在逻辑上他应该被视为连续的。
·所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区TLAB(Thread Local Allocation Buffer)
【所以说堆空间不是完全共享的,因为还有TLAB】
·堆内存的大小是可以调节的:初始堆空间-Xms10m 最大堆空间-Xmx10m
##解释:
一个进程对应一个JVM实例,一个JVM实例对应一个Runtime Data Area,
一个进程对应一个堆和方法区,一个进程有多个线程,每个线程对应一个PC寄存器、虚拟机栈和本地方法栈。
因此,堆和方法区在进程中会被多个线程共享。
·几乎所有的对象实例 以及 数组 都应该在运行时分配在堆上。
·数组和对象可能永远不会存储在栈上,因为栈帧中保存应用,这个引用指向对象或者数组在堆中的位置。
·在方法结束后,堆中的对象不会马上被移除,仅仅在GC垃圾收集的时候才会被移除。
·堆,是GC垃圾回收期执行垃圾回收的重点区域
##拓展
堆中创建对象的命令 : 创建对象new 创建数组newarray
2、堆的内存细分
##堆空间细分为:
JDK7及之前:新生区 + 养老区 + 永久区
JDK8及之后:新生区 + 养老区 + 元空间
【新生区 = Eden区 + Survivor区】
(现代垃圾收集器大部分都是基于分代收集理论设计)
名称: 新生区 = 新生代 = 年轻代
养老区 = 老年区 = 老年代
永久区 = 永久代
3、设置堆内存大小与OOM
·Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了。
可以通过-Xmx -Xms来进行设置:
> -Xms表示堆区的起始内存,等价于-XX:InitialHeapSize
> -Xmx表示堆区的最大内存,等价于-XX:MaxHeapSize
·一旦堆区中的内存大小超过-Xmx所指定的最大内存时,就会抛出OutOfMemoryError异常
·通常会将-Xms 和 -Xmx 两个参数配置相同的值:
其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提升性能。
(堆的起始内存 如果 不等于 堆的最大内存,在使用的过程中会不断的 扩容 和 释放,造成了不必要的系统压力,也避免了GC后的重新分隔堆区大小)
·默认情况下
初始内存大小 = 物理电脑内存大小 / 64
最大内存大小 = 物理电脑内存大小 / 4
##拓展:
-X 是JVM的运行参数 ms是memory start
##查看设置的参数:
方式一: 查看进程jps / 查看内存使用 jstat -gc 进程ID
方式二:-XX:+PrintGCDetails 打印GC详情
[堆区对象存储的时候 Eden + s0或s1 幸存者区s0和s1只能使用其中的一个]
##下图S0 S1表示幸存者区0和1,S0U S1U表示幸存者区是use使用大小
##EC和EU表示伊甸园Eden区的大小
4、新生代与老年代
·存储在JVM中的Java对象可以被划分为两类:
>一类是生命周期较短的瞬时对象,这类对象的创建消亡都非常迅速
>另一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的声明周期保持一致。
·Java堆区进一步细分的话,可以划分为 "年轻代"(YoungGen) 和 "老年代"(OldGen)
·其中 "年轻代" 又可以划分为: Eden空间、Survivor0空间、Surivivor1空间(有时也可叫做from区、to区)
·下面这些参数开发中一般不会调----配置新生代与老年代在堆结构的占比:
>默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
>可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
·在HotSpot中,Eden空间 和 另外两个Survivor空间的所占比例是8:1:1
·当然开发人员可以通过选项 -XX:SurvivorRatio 调整空间比例。
如:-XX:SurivivorRatio=8
·几乎所有的Java对象都是在Eden区被new出来的。
·绝大部分的Java对象的 销毁 都是在 新生代 进行了(80%)
·可以使用选项 "-Xmn" 设置新生代最大内存大小
>这个参数一般使用默认值就可以了(如果同时设置了-XX:NewRatio,以-Xmn为准)
·JVM会默认开启 自适应 内存分配策略
>我们可以通过 -XX:-UseAdaptiveSizePolicy 来关闭自适应
[-XX:+UseAdaptiveSizePolicy表示使用 -XX:-UseAdaptiveSizePolicy表示关闭]
5、对象的分配过程
1> new的对象先放伊甸园区。此区有大小限制
2> 当伊甸园区的空间填满时,程序此时需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC / YGC)。
将伊甸园区中不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
(垃圾回收期GC时,STW[Stop the World],Java应用程序的其他所有线程都会被挂起,以方便垃圾回收器判断谁该被回收)
【伊甸园区满了会触发YGC,但是S0S1幸存者区满了不会触发YGC,而是会直接晋升到老年代】
3> 然后将伊甸园区中的剩余对象移动到幸存者0区
4> 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有被回收,就会放到幸存者1区。同时伊甸园区幸存下来的也放到幸存者1区。
5> 如果再次经历垃圾回收,此时会重新放回幸存者0区(伊甸园幸存者也是),接着再去幸存者1区(伊甸园幸存者也是)
6> 啥时候能去养老区呢?可以设置次数:默认是15次
参数-XX:MaxTenuringThreshold=<N>进行设置
7> 在老年代,相对悠闲。当老年代内存不足时,再次出发GC(Major GC),进行老年区的内存清理。
8> 若老年区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常
(java.lang.OutOfMemoryError : Java heap space)
##总结:
·幸存者S0S1区,复制之后有交换,谁空谁是to
·关于垃圾回收:频繁在新生区收集,很少在养老区收集,几回不在永久区/元空间收集
##拓展:常用的调优工具
·JDK命令:jinfo jstat jmap javap等
·Eclipse:Memory Analyzer Tool
·Jconsole
·JVisualVM
·Jprofiler
·Java Flight Recorder
·GCViewer
·GC Easy
6、Minor GC、Major GC与Full GC
·JVM在进行GC时,并非每次都对上面三个内存区域一起回收(新生代、老年代、方法区),
大部分时候回收的都是 新生代
·针对HotSpot VM的实现,GC按照回收区域又分为两种大类型:部分收集(Partial GC)整堆收集(Full GC)
>部分收集:不是完整收集整个JAVA堆的垃圾收集。其有细分为:
新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
老年代收集(Major GC/Old Gc):只是老年代的垃圾收集
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
[注意:只有CMS GC会有单独收集老年代的行为]
[注意:很多时候Major GC会和Full GC混淆起来一块使用,需要具体分辨是老年代回收还是整堆回收]
[注意:只有G1 GC会有Mixed GC]
>整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
·Minor GC(年轻代GC)触发机制:
>当年轻代空间不足时,会出发Minor GC,这里的年轻代满了是指Eden代满,Survivor满不会出发GC(每次Minor GC会清理年轻代的内存)
>因为Java对象大多都是朝生夕灭的特性,所以MinorGC非常频繁,一般回收速度也比较快。
>Minor GC会引发STW,暂停其他用户的线程,等垃圾回收技术,用户线程才恢复。
·Major GC(老年代GC)触发机制:
>Major GC指发生在老年代的GC,对象从老年代被清理。
>出现Major GC,会经常伴随至少一次的Minor GC(但非绝对)
[也就是老年代空间不足时,会出尝试出发Minor GC。如果之后空间还不足则出发Major GC]
>Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
>如果Major GC后,内存还不足,就报OOM。
·Full GC触发机制:
>触发Full GC的情况有如下五中:
①、调用System.gc()时,系统执行Full GC,但是不必然执行
②、老年代空间不足
③、方法区空间不足
④、有Eden区、S0区向S1区复制时,对象大小大于S1可用内存,则把该对象转存到老年代,且老年代的可用内存小于对象大小。
[注意:Full GC是开发或者调优中尽量要避免的,这样暂时时间会短一些]
7、堆空间分代思想
##为什么需要把JAVA堆分代?不分代就不能正常工作了吗?
·经研究,不同对象的声明周期不同。70%~90%的对象是临时对象。
>新生代:有Eden、两块大小相同的Survivor构成
>老年代:存放新生代中经历多次GC仍然存活的对象
·其实部分带完全可以,分代的唯一理由就是优化GC性能。
如果没有分代,那所有的对象都在一块,就如同吧一个学校的人都关在一个教室。GC的时候要找
到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的
话,吧新创建的对象放到某一地方,当GC的时候先把这块存储 朝生夕死 对象的区域进行回
收,这样就会腾出很大的空间
8、内存分配策略
·如果对象在Eden出生并经过一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被
移动到Survivor空间中,并将对象年龄设为1.对象在Survivor区中每熬过一次MinorGC,
年龄就增加1岁,当它的年龄增加到一定程度(默认15)时,就会被晋升到老年代。
[对象晋升老年代的年龄阈值,可以通过-XX:MaxTenuringThreshold来设置。]
·针对不同年龄段的对象分配原则如下:
>优先分配到Eden
>大对象直接分配到老年代
[尽量避免程序中出现过多的大对象]
[大到Eden区的剩余空间放不下了,只能直接放到 老年区]
>长期存活的对象分配到老年代
>动态对象年龄判断
如果s区中相同年龄的所有对象大小的综合大于s空间的一般,年龄大于或等于该年龄的对
象可以直接进入到老年代,无需等到MaxTenuringThreshold中要求的年龄
>空间分配担保
-XX:HandlePromotionFailure
###空间分配担保:
在发生Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
>如果大于,则此次Monor GC是安全的
>如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败
如果HandlePromotionFailure=true,name会继续检查老年代最大可能连续空间是否大于历次晋升到老年代的对象的平均大小。
√如果大于,则尝试进行Minor GC,但这次Minor GC依然是有风险的
√如果小于,则改为进行一次Full GC
如果HandlePromotionFailure=false,则改为进行一次Full GC
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。规则变为了 只要老年代的连续空间大于新生代对象总大小 或者 历次晋升的品骏大小就会进行MinorGC,否则将进行FullGC
9、对象分配过程:TLAB
##什么是TLAB?
·从内存模型而不是垃圾回收的角度,堆Eden区域继续进行划分,JVM为每个线程分配了一个私有缓冲区域,它包含在Eden空间内。
·多线程同时分配内存是,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分
配的吞吐量,因此我们可以将这种内存分配方式称之为 快速分配策略
·OpenJDK衍生出来的JVM都提供TLAB设计。
##为什么有TLAB(Thread Local Allocation Buffer)?
·堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
·由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
·为避免多个线程操作统一地址,需要使用加锁等机制,进而影响分配速度,所以要有TLAB
##TLAB说明
·不是所有的对象实例都够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
·在程序中,可以通过-XX:UseTLAB设置是否开启TLAB(默认开启)
·默认情况下,TLAB空间的内存非常小,进栈整个Eden空间的1%,当然我们可以通过
-XX:TLABWasteTargetPercent设置TLAB空间所占Eden空间百分比大小
·一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用 加锁机制 确保数据操作的原子性,从而直接在Eden空间中分配内存。
10、堆空间的参数设置
-XX:+PrintFlagsInitial 查看所有的参数的默认初始值
-XX:+PrintFlagsFinal 查看所有的参数的最终值
具体查看某个参数的指令:jps 查看当前运行中的进程
jinfo -flag SurvivorRatio 进程ID
-Xms 初始堆空间内存(默认屋里内存1/64)
-Xmx 最大堆空间内存(默认屋里内存1/4)
-Xmn 设置新生代大小(初始值及最大值)
-XX:NewRatio 配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio 配置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出GC处理日志
打印GC简要信息:-XX:+PrintGC -verbose:gc
-XX:HandlePromotionFailure 设置空间分配担保
11、堆是分配对象存储的唯一选择吗?
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
·如何将堆上的对象分配到栈,需要使用逃逸分析手段
·这是一种可以有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
·通过逃逸分析,java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
·逃逸分析的基本行为就是分析对象动态作用域:
>当一个对象在方法中被定义后,对象只在方法内部使用,责备认为没有发生逃逸。
>当一个对象在方法中被定义后,他被外部方法所引用,则认为发生逃逸。
例如作为调用参数传递到其他地方中
结论:开发中能使用局部变量的,就不要在方法外定义。
12、逃逸分析:代码优化
使用逃逸分析,编译器可以对代码做如下优化:
>栈上分配。
将堆分配转化为栈分配。
>同步省略。
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
>分离对象或标量替换
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或全部可以不存储在内存,而是存储在CPU寄存器中
二、方法区
1、栈、堆、方法区的交互关系
2、方法区的概述
·方法区还有一个别名叫做Non-Heap(非堆),尽管所有的方法区在逻辑上是属于堆的一部分。
所以方法区可以看做是一块独立于java堆的内存空间。
·方法区(Method Area)与java堆一样,是各个线程共享的内存区域。
·方法区在JVM启动的时候被创建,并且他的实际的屋里内存空间中和堆区一样都是不连续的
·方法区的大小和堆空间一样,可以选择固定大小或者可扩展
·方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出。
虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGen space或
者java.lang.OutOfMemoryError:Metaspace
·关闭JVM就会释放这个区域的内存
##方法区演进
Java7及以前,习惯上把方法区称之为永久代。
Java8开始,使用了元空间取代了永久代
【Java8以后,是用元空间来实现的方法区;在Java8之前,则是用永久代实现的方法区】
【元空间使用的是 本地内存 非JVM内存】
【原来永久代使用的是JVM的内存,现在元空间改用 本地内存】
3、设置方法区大小与OOM
##Java7及之前:
-XX:PermSize 设置永久代初始分配空间。默认20.75m
-XX:MaxPermSize 设置永久代最大可分配空间 32位机默认64M,64位机默认82M
当JVM加载的类信息容量超过了这个值会报OutOfMemoryError:PermGen Space
【-XX:PermSize=100m -XX:MaxPermSize=500M】
##Java8及之后:
-XX:MetaspaceSize 设置元空间初始分配空间 默认21M
-XX:MaxMetaspaceSize 设置元空间最大可分配空间 默认-1,没有限制
当JVM加载的类信息容量超过了这个值会报OutOfMemoryError:Metaspace Space
【-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=500M】
默认-XX:MetaspaceSize为21MB,这就是初始的高水位线,一旦触及这个水位线,
Full GC将会被触发并卸载没用的类(即这个类对应的类加载器不再存活),然后这个高水
位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那
么在不超过MaxMetaspaceSize时,适当提高该值、如果释放空间过多,则适当降低该值
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收期的日
志可以观察到Full GC多次调用。为了避免频繁地GC,减一将XX:MetaspaceSize设置为
一个相对较高的值
如何解决OOM
1、要解决OOM异常或heap space异常,一般的手段是首先通过内存映射分析工具堆dump
出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底
是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
2、如果内存泄漏(Memory Leak),可以进一步通过工具查看泄漏对象到GC Roots的引用
链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动
回收他们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地
定位出泄漏代码的位置。
3、如果不存在内存泄漏,换句话说就是内存中的对象却是都还必须存活着,那就应当检查虚
拟机的堆参数(-Xmx与-Xms),与机器屋里内存对比看看是否还可以调大,从代码上检查是
否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
4、方法区的内部结构
·方法区的存储内容:
>类型信息(域信息、方法信息)、常量、静态变量、即使编译器编译后的代码缓存等
##一、类型信息
堆每个加载的类型(类class\接口interface\枚举Enum\注解annotation),JVM必须在方法区中存储一下类型信息:
>这个类型的完整有效名称(全名=包名.类名)
>这个类型直接父类的完整有效名
>这个类型的修饰符(public abstract final的某个子集)
>这个类型直接接口的一个有序列表
##二、域Field信息(成员变量)
JVM必须在方法区中保存类型的所有域相关信息以及域的声明顺序
>域的相关信息包括:域名城、域类型、域修饰符(public private protected static final volatile transient的某个子集)
##三、方法信息
JVM必须保存所有方法的一下信息,同域信息一样包括声明顺序:
>方法名称
>方法的返回类型(或void)
>方法参数的数量和类型(按顺序)
>方法的修饰符(public private protected static final synchronized abstract native的一个子集)
>方法的字节码、操作数栈、局部变量表及大小
>异常表(每个异常处理的开始位置、结束为止、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引)
·non-final的类变量
>静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
>类变量被类的所有实例共享,即使没有类实例时也可以访问
>全局常量:static final,被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
##四、运行时常量池 VS 常量池
·方法区:内部包含了 运行时常量池runtime constant pool
·字节码文件:内部包含了 常量池constant pool
[注:字节码文件内的常量池,被加载到了方法区中就叫做运行时常量池]
常量池表Constant Pool Table,包括了各种 字面量 和对类型、域和方法的"符号引用"。
·为什么需要常量池?
一个java源文件中的类、接口,编译后产生一个字节码文件。而java中的字节
码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种
方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时
候会用到运行时常量池。
如:
public class SimpleClass{
public void sayHello(){
System.out.println("hello");
}
}
以上虽然代码很小,但是里面却使用了String\System\PrintSream及
Object等结构。这里的代码量其实已经很小了,如果代码多,引用到的结构会
更多!这里就需要常量池了!
(我们在class字节码文件里,不会引入string、system、printstream、Object等的源文件,只需引入其引用即可-->符号引用)
·常量池中有什么?
常量池内存储的数据类型包括:数量值、字符串值、类引用、字段引用、方法引用
常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
·运行时常量池
>运行时常量池是方法区的一部分
>常量池是class文件的一部分,用于存放编译器生成的各种字面量与符号引
用,这部分内容将在类加载后存放到方法区的运行时常量池中
>运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池
>JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据就像数
组一样,是通过索引访问的
>运行时常量池中包含多种不同的常量,包括编译器就已经明确的数值字面量,
也包括到运行期解析后才能够获得的方法或字段引用,此时不再是常量池中符号
地址了,这里换位真实地址。
>运行时常量池类似于传统编程语言中的符号表,但是它所包含的数据却比符号
表更加丰富些
>当创建类或接口的运行时常量池时,如果狗仔运行时常量池所需的内存空间超
过了方法区所能提供的最大值,则JVM会抛出OutOfMemoryError异常
实例:
public class MethodArea {
public static void main(String[] args) {
int x=500;
int y=100;
int a=x/y;
int b=50;
System.out.println(a+b);
}
}
编译后的文件
Classfile /D:/WorkSpace/DailyCodePackage/basic/target/classes/com/lee/jvm/rundataarea/MethodArea.class
Last modified 2020-12-7; size 670 bytes
MD5 checksum 42390faabe2fc63519914b0ac436af5f
Compiled from "MethodArea.java"
public class com.lee.jvm.rundataarea.MethodArea
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#25 // java/lang/Object."<init>":()V
#2 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #28.#29 // java/io/PrintStream.println:(I)V
#4 = Class #30 // com/lee/jvm/rundataarea/MethodArea
#5 = Class #31 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/lee/jvm/rundataarea/MethodArea;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 x
#18 = Utf8 I
#19 = Utf8 y
#20 = Utf8 a
#21 = Utf8 b
#22 = Utf8 MethodParameters
#23 = Utf8 SourceFile
#24 = Utf8 MethodArea.java
#25 = NameAndType #6:#7 // "<init>":()V
#26 = Class #32 // java/lang/System
#27 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#28 = Class #35 // java/io/PrintStream
#29 = NameAndType #36:#37 // println:(I)V
#30 = Utf8 com/lee/jvm/rundataarea/MethodArea
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (I)V
{
public com.lee.jvm.rundataarea.MethodArea();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lee/jvm/rundataarea/MethodArea;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: return
LineNumberTable:
line 6: 0
line 7: 4
line 8: 7
line 9: 11
line 10: 15
line 11: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 args [Ljava/lang/String;
4 22 1 x I
7 19 2 y I
11 15 3 a I
15 11 4 b I
MethodParameters:
Name Flags
args
}
SourceFile: "MethodArea.java"
5、方法区的演进
版本
变化
jdk1.6及之前
有永久代,静态变量存放在永久代
jdk1.7
有永久代,但已将字符串常量池、静态变量移除,保存在堆中
jdk1.8及之后
无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
##永久代为什么被元空间所替代
·永久代设置空间大小很难确定
在某些场景下,如果动态加载类过多,容易产生Perm的OOM。元空间和永久代之间的最大区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
·对永久代进行调优很困难
Full GC时间长,造成长时间的STW,影响程序的效率
##StringTable为什么要调整
JDK7将StringTable字符串常量池放在了堆中,因为永久代的回收效率很低,在
Full GC的时候才会触发。而Full GC是老年代的空间不足、永久代不足时才会触
发。这就导致了StringTable的回收效率不高。而我们开发中会有大量的字符串被创
建,回收效率低,导致永久代内存不足。放到堆中,能即使回收内存。
6、方法区的垃圾回收
方法区的GC主要包含两部分内容:常量池中废弃的常量 和 不再使用的类型
三、运行时数据区总结
思考如下:
##一、说一下JVM内存模型,有哪些区,分别干什么?
##二、JAVA8的内存分代改进?
##三、栈和堆的区别?堆的结构?为什么两个survivor区?
##四、Eden和Survivor的分配比例?
##五、JVM内存分区,为什么要分新生代、老年代和持久代?
##六、什么时候对象会进入老年代?
##七、JVM永久代中会发生垃圾回收吗?
四、对象的实例化内存布局与访问定位
对象的实例化:
1、创建对象的方式:
>new
>Class的newInstance
>Constructor的newInstance(xxx)
>使用clone()
>使用反序列化
>第三方库Objenesis
2、创建对象的步骤:
>判断对象对应的类是否加加载、链接、初始化
虚拟机遇到一条new指令,首先去检查这个指令的参数能否在metaspace的常量池中定位
到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。
如果没有,那么在双亲委派模式下,使用当前类加载器以classLoader+包名+类名为
key进行查找对应的.class文件。如果没有找到文件,则抛出
classNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类
对象。
>为对象分配内存
首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可(4个字节大小)
>处理并发安全问题
采用CAS失败重试、区域加锁保证更新的原子性
每个线程预先分配一块TLAB
>初始化分配到的空间
所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
>设置对象的对象头
将对象的所属类、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现
>执行init方法进行初始化
初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
对象的内存布局:
1、对象头Header
>运行时元数据
哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
>指向类元数据InstanceKlass,确定该对象所属的类型
即指向了元空间或者方法区中的对象的所属具体类型
(注:如果是数组,还需记录数组的长度)
2、实例数据Instance data
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类集成下来的和本身拥有的)
3、对齐填充
不是必须的
对象的访问定位:
对象访问方式主要有两种:
>句柄访问(浪费空间且效率低)
栈帧中的refrence指向对应的堆空间的句柄池,句柄池有两部分组成,一个是到对象实
例数据的指针指向堆中对象的实例数据,另一个是到对象类型数据的指针指向方法区的对
象类型数据
>直接指针(HotSpot采用的)(节省空间速度快)
栈帧中的refrence直接指向对象的实例数据,对象的实例数据通过到对象类型数据的指
针指向方法区中的对象类型数据
思考如下:
##一、对象在JVM中是怎么存储的?
##二、对象头信息里面有哪些东西?
##三、JVM是如果通过栈帧中的对象引用访问到其内部的对象实例的?