jvm学习笔记

栈有着后进先出 / 先进后出的特点,栈管理程序运行 ,存储一些基本类型的值、对象的引用、方法等。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。

栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放。

对于栈来说不存在垃圾回收问题,只要线程一旦结束,该栈就Over,生命周期和线程一致,是线程私有的。

方法自己调自己就会导致栈溢出

为什么main方法最后执行?

在程序中main方法最后执行的原因就是它是压在栈的最底部只有上面的方法走完了main方法才能执行。

栈帧

栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。他是独立于线程的,一个线程有自己的一个栈帧。封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。 第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
当一个方法 A 被调用时就产生了一个栈帧 F1 ,并被压入到栈中, A 方法又调用了 B 方法,于是产生了栈帧 F2也被压入栈中, B 方法又调用了 C 方法,于是产生栈帧 F3 也被压入栈中 ......... 执行完毕后,先弹出 F3, 然后弹出F2 ,在弹出 F1........
遵循 先进后出” / "后进先出 " 的原则。

堆(Heap)

Heap 堆,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
  • 新生区 Young Generation Space Young/New
  • 养老区 Tenure generation space Old/Tenure
  • 永久区 Permanent Space Perm
堆内存 逻辑上 分为三部分:新生,养老,永久(元空间 : JDK8 以后名称)

新生区和养老区

新生区是类诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden Space )和幸存者区( Survivor Space ),所有的类都是在伊甸区被new 出来的。
幸存区有两个: 0 区 和 1 区,当伊甸园的空间用完时,程序又需要创建对象, JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC )。将伊甸园中的剩余对象移动到幸存 0 区,若幸存 0 区也满了,再对该区进行垃圾回收,然后移动到1 区,那如果 1 区也满了呢?(这里幸存 0 区和 1 区是一个互相交替的过程)再移动到养老区,若养老区也满了,那么这个时候将产生MajorGC Full GC ),进行养老区的内存清理,若养老区执行了Full GC 后发现依然无法进行对象的保存,就会产生 OOM 异常“OutOfMemoryError ”。
如果出现 java.lang.OutOfMemoryError java heap space 异常,说明 Java 虚拟机的堆内存不够,原因如下:
1 Java 虚拟机的堆内存设置不够,可以通过参数 -Xms (初始值大小), -Xmx (最大大小)来调整。
2 、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环

永久区 

1.永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM 才会释放此区域所占用的内存。
2.如果出现 java.lang.OutOfMemoryError PermGen space ,说明是 Java 虚拟机对永久代 Perm 内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar 包,例如:在一个 Tomcat 下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm 区被占满。

堆内存调优

jvm调优基本上都是在对堆进行调优

IDEA 调整堆内存大小测试

-Xms:设置初始分配大小,默认为物理内存的 “1/64”
-Xmx:最大分配内存,默认为物理内存的 “1/4”
-XX:+PrintGCDetails : 输出详细的GC 处理日志

 发现,默认的情况下分配的内存是总内存的 1/4,而初始化的内存为 1/64

尝试触发oom

 

 可以看到在三次gc之后发现老年区已经满了无法在将新生区的对象转入,因此触发了full gc进行回收再最后两区已经都满了因此出现了oom问题。

Dump内存快照

先下载jprofiler:https://www.ej-technologies.com/download/jprofiler/files
idea插件下载jprofiler,然后配置

 -XX:+HeapDumpOnOutOfMemoryError 打印快照出来

 打开快照分析

 根据里面的内容对内存快照进行分析

 gc算法

GC 按照回收的区域分了两种类型,一种是普通的 GC minor GC ),一种是全局 GC major GC
or Full GC
普通 GC :只针对新生代区域的 GC
全局 GC :针对老年代的 GC ,偶尔伴随对新生代的 GC 以及对永久代的 GC

复制算法

年轻代中使用的是Minor GC,采用的就是复制算法(Copying)
Minor GC 会把 Eden 中的所有活的对象都移到 Survivor 区域中,如果 Survivor 区中放不下,那么剩下的活的对象就被移动到Old generation 中, 也就是说,一旦收集后, Eden 就是变成空的了
当对象在 Eden (包括一个 Survivor 区域,这里假设是 From 区域)出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块Survivor 区域所容纳 (上面已经假设为 from 区域,这里应为 to 区域,即to 区域有足够的内存空间来存储 Eden From 区域中存活的对象),则使用 复制算法 将这些仍然还活着的对象复制到另外一块Survivor 区域(即 to 区域)中,然后清理所使用过的 Eden 以及 Survivor区域(即form 区域),并且将这些对象的年龄设置为 1 ,以后对象在 Survivor 区,每熬过一次 Minor GC ,就将这个对象的年龄 + 1 ,当这个对象的年龄达到某一个值的时候(默认是 15 岁,通过 -XX:MaxTenuringThreshold 设定参数)这些对象就会成为老年代
好处:没有内存碎片,坏处:浪费内存空间
-XX:MaxTenuringThreshold

标记清除

老年代一般是由标记清除或者是标记清除与标记整理的混合实现

当堆中的有效内存空间被耗尽的时候,就会停止整个程序(也被称为 stop the world ),然后进行两项工作,第一项则是标记,第二项则是清除。
1.标记:从引用根节点开始标记所有被引用的对象,标记的过程其实就是遍历所有的 GC Roots ,然后将所有GC Roots 可达的对象,标记为存活的对象。
2.清除: 遍历整个堆,把未标记的对象清除。
缺点:这个算法需要暂停整个应用,会产生内存碎片。
用通俗的话解释一下 标记 / 清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候, GC 线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。
劣势:
1. 首先、它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行 GC 的时候,需要停止应用程序,这会导致用户体验非常差劲
2. 其次、主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随机的出现在内存的各个角落,现在把他们清除之后,内存的布局自然乱七八糟,而为了应付这一点,JVM 就不得不维持一个内存空间的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

标记压缩

在整理压缩阶段,不再对标记的对象作回收,而是通过所有存活对象都像一端移动,然后直接清除边界以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉,如此一来,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标记整理算法不仅可以弥补 标记清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价;

总结

内存效率:复制算法 > 标记清除算法 > 标记整理算法 (时间复杂度)
内存整齐度:复制算法 = 标记整理算法 > 标记清除算法
内存利用率:标记整理算法 = 标记清除算法 > 复制算法
对不同内存区采用不同算法称为分代收集

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值