JVM-1.自动内存管理

目录:

JVM-1.自动内存管理

JVM-2.字节码和字节码指令

JVM-3.类的加载机制

JVM-4.字节码执行和方法调用

JVM-5.程序编译与代码优化

JVM-6.Java线程内存模型和线程实现

和大多数高级语言一样,java也有自动回收内存的机制。给程序员们带来河大的便利,而不用和c++一样,时刻当心内存溢出。那么,jvm的自动内存管理机制是如何实现的?

关键点

  • 1.jvm运行时内存区域
  • 2.垃圾回收器和垃圾回收算法
  • 3.对象的内存分配和回收

1.JVM的运行时内存区域

jvm运行时内存被分配为不同的块,以实现不同的作用。
在这里插入图片描述
java 1.7和之前的区域分划;

  • java堆
    java堆的作用是保存各种实例变量,当使用new创建一个实例后,实例机会被存储在java堆中。java堆是线程共享的。
  • 方法区(永久代),更改为元空间
    方法区是java1.7之前的产物,也被称为永久代,用于存放类的元数据,运行时常量池也在方法区。在java1.8之后,方法区就被遗弃了,取而代之的是元空间,作用和方法区一样,不过字符串常量池被移动到了java堆中。而且把元空间的数据存放在了本地内存上,而不是堆内存,减少了内存溢出发生的概率。
  • 虚拟机栈
    虚拟机栈是线程私有的,用于存储局部变量表,操作数栈,方法出口等方法执行时需要的数据。
  • 本地方法栈
    本地方法栈和虚拟机栈式一样的,不过运行的式本地的方法,hotspot把虚拟机栈和本地方法栈合并在一起
  • 程序计数器
    程序计数器也是线程私有的,用于记录当前线程锁执行的字节码的行号指示器。
  • 运行时常量池
    运行时常量池保存了常量值,而且还预设了一些值,比如整性的-128~127就已经被缓存到了常量池中。而对于字符串,所有的字面量会在类加载的时候缓存到常量池。关于字符串常量池的可以看我的另一篇文章java 字符串常量池.
  • 本地内存
    元空间使用本地内存,NIO也会使用本地内存,为了避免在java堆和内核内存直接频繁的复制,开辟一个堆外内存并映射到内核内存的同一个地址,这样能提高效率。不过值得注意的是,这一块内存由于不包含在堆中,所以容易被忽略,导致内存溢出。

2.垃圾回收器和垃圾回收算法

垃圾回收是java这类语言的一个特点。在考虑GC的时候,会有以下问题。

  • 哪些内存需要回收
  • 什么时候回收
  • 怎么回收

哪些内存需要回收(如何判断对象已死亡)

  • 引用计数法
    引用计数法记录每一个对象的引用次数,当引用次数等于0的时候,对象被判断为需要回收。这种算法的问题在于,如果循环引用,就会导致对象永远不会被清除。
  • 可达性分析
    可达性算法是从GC root出发,标记所有被引用的数据,GC root是指在虚拟机栈,元空间中的类的静态引用对象和常量的值。如果没有被标记就是没有引用的对象,这样避免了循环引用的问题,当然,效率比引用计数法低一些。java C#等语言都是使用该方法标记死亡的对象。
  • 永久代/元空间的垃圾回收
    永久代/原空间并不是不回收垃圾,在元空间中,当内存达到参数“MaxMetaspaceSize”设置的值的时候,就会触发对死亡对象的回收。此时的死亡对象包括
    • 废弃的常量:无引用的常量。(常量池在1.8被移动到java堆中,所以不会在这里回收)
    • 废弃的类:所有的实例都被回收,class对象没有被引用,加载该类的ClassLoader已经被回收。
  • java中的四种引用
    • 强引用:普通的使用new 实例化的对象。只有当引用不存在时才会被回收。
    • 软引用:软引用会在内存不足的时候就会不回收,而不用等待无引用的时候才会回收。
    • 弱引用:弱引用会在下一次垃圾回收时就会被回收。
    • 虚引用:虚引用并不会影响对象的生命周期,唯一的作用是,当对象被回收时收到一个系统通知。
//软引用
SoftReference<T> obj = new SoftReference<>(str);
//弱引用
WeakReference<T> obj = new WeakReference<>(str);
//虚引用
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), new ReferenceQueue<>())

2.1垃圾回收算法

标记清除算法

标记清除法时基于可达性分析的最基础的垃圾回收算法。后续的垃圾回收算法几乎都是对这个算法的改进。算法的过程是先标记死亡的对象,然后再清理内存。这个算法的缺点是:清理速度缓慢,而且会使得空间变得破碎。

复制算法

复制算法是,把内存分成两块,只用其中的一块内存,当使用的那一块内存满了,就会标记活着的对象,并把或者的对象移动到另外一块内存。并把原内存清空。

优点是:清理速度非常块,直接删除一整块内存空间就可以。

缺点是,太耗费内存,只能使用其中的一半内存。所以实际上的使用并不是直接分成两块内存,而是分成三块(Eden占80%,两个Survior,每个占10%)。实际上数据先按照普通的复制算法在两个Survior之间流转,当垃圾清理之后,已经无法存放在一个Survior中了,才会把数据移动到Eden内存块中。

这种算法非常适合新生代的对象,因为新生代的对象都是朝生夕死。每次垃圾回收都只有很少的存活对象,使用快速的复制算法能够高效的回收新生代的对象。

标记-整理算法

对于老年代(生存超过15次垃圾回收的对象),特点和新生代对象相反,存活率高,且新生代晋级到老年代的数量也少。因此,并不适合复制算法。而是使用标记-整理算法,所谓标记整理就是先标记出所有存活的对象,并把所有存活的对象整理到一边。并一次性把位于另外一边的所有数据清空。
这样做的相比于没有整理的好处是,没有碎片空间,且删除效率高。

分代式算法

从上面的描述其实可以知道,虚拟机并不一定使用一样的算法来处理新生代和老年代(G1和ZGC垃圾回收器没有分代),而是对于不同的代使用不同的算法来清除垃圾。

HotSpot回收算法实现

HotSpot是甲骨文公司实现的虚拟机,如果使用的是甲骨文公司的JDK,一般都是使用HotSpot虚拟机。

  • 枚举根节点
    枚举根节点即找到所有的gc root,在执行这个操作时,必须停止所有的线程,被称之为stop the world(STW),这个时候就需要考虑什么时候停止线程才算安全。
    停止线程有两种做法

    • 抢先式中断:不需要线程配合,直接中断所有线程
    • 主动式中断:当标记为需要STW时,线程运行到安全点的或者安全区域就会停止线程。
  • 安全点

    • 循环的末尾
    • 方法返回前
    • 调用方法的call之后
    • 抛出异常前
  • 安全区域:长时间闲置的

    • Thread.sleep()
    • 线程阻塞

2.2 垃圾回收器

新生代垃圾回收器
  • Serial 单线程回收,标记和整理回收都会STW
  • ParNew 多线程标记,单线程整理回收,适用于多cpu
  • Parallel Scavenge:以吞吐量为目标的垃圾回收,上面两个垃圾回收器是以缩短每次垃圾收集的停顿时间,而吞吐量是以缩短总垃圾回收时间,适用于没有实时性要求的服务,如大量的数据分析。如果误用在实时性要求高的服务,可能会导致偶尔非常卡顿。
老年代垃圾回收器
  • Serial Old Serial的老年代收集器,单线程,使用标记整理
  • Parallel Old:Parallel Scavenge的老年代,使用标记整理
  • CMS:获取最短回收停顿时间为目标的收集器,多次标记,使用并发标记,除了初始标记和重新标记,其他的都是并发标记。这样就节省了标记的时间,不过因为并发标记也占用了CPU资源。cms为了低停顿,并没有使用标记整理,而是直接使用标记清除,虽然停顿时间短了,但是缺点也很明确,就是会产生大量的空间碎片。容易触发full GC

注意老年代和新生代并不是任意搭配的。
在这里插入图片描述

不分代
  • G1
    和前面的垃圾回收不一样的是,G1取消了新生代,老年代的整体内存区域划分。取而代之的是把内存划分为若干块。但是每一块任然是区分代际的,所以这里分为不分代其实没有那么严谨
    在这里插入图片描述
    仍然对新生代使用复制算法(Survivor块),对老年代使用标记整理算法。
    特别的是,出现了Humongous块,这个是当一个对象大小超出了分区50%的对象,会被认为是巨型对象,单独放在Humongous块。如果一个块放不下,就会寻找连续的区。如果没有找到连续的区,就会触发一次full GC。

G1垃圾回收器的特点是停顿时间短,而且由于被分成了细粒度的区块,所以可以控制停顿的时间。当时间达到限制时间时,就会停止回收其他区块而等待下一次回收。

-XX:MaxGCPauseMillis:G1的每次停顿限制时间配置,默认200毫秒

3.内存的分配和回收策略

新生代的分配和回收(Minor GC)

新生代的垃圾回收被称为Minor GC,他并不是简单的复制算法。
1.首先,数据会先分配到使用中的Eden区,当Eden满了,就会把存活的对象移动到使用中的Survivor区,并清理掉一个Eden区。
2.当Survivor满了,会把Eden区和Survivor区存活的对象移动到新的Survivor区。
2.但是,如果一个Survivor区已经无法放入所有存活的对象呢?只好通过分配担保机制提前转移到老年区(元空间)。
5.如果老年区也无法存放对象,那么就会触发一次Full GC
在这里插入图片描述

老年代的分配和回收机制

进入老年代的时机

  • 当存活超过15垃圾回收时,对象就会晋升到老年代。(通过参数-XX:MaxTenuringThreshold设置)
  • 大对象会直接进入老年区(G1是进入Humongous),这并不是一个好消息。因为新对象大部分都是朝生夕灭。一个已经死亡的对象,却在老年代占用大量的空间,长时间得不到清除。
  • 动态对象年龄判断:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
  • 空间分配担保:如上文所述,当Minor gc导致的空间不足,也会触发Full GC。

当老年代空间不够时,就会触发Full gc。由于使用的是标记清除或者标记整理算法,Full GC要比Minor GC慢上很多。

4.内存溢出实战

保存Dump

通过设置 -XX:+HeapDumpOnOutOfMemoryError,让发生内存溢出时自动生成dump文件。

找出占用内存最高的元凶

第一步 使用jps找出虚拟机线程

jps -l

第二步 键入 “top” 然后键入大写的"M"(一定要大写)就会按内存使用率排序,另外如果键入大写的“P”就会按CPU使用率排序。
找到元凶的pid
在这里插入图片描述
第三步 将PID转换成16进制

printf 5470

第三步 使用jstack 打印运行中的代码

jstack 10765 | grep ‘0x2a34’ -C5 --color

jstack 后面跟着的是虚拟机id,通过grep查找 16进制的线程ID

  • -C5 表示显示5行
  • –color 为找到的作色

也可以使用下面的命令直接保存成文件

jstack 10765 >> 123.txt

//-l 额外输出锁信息
jstack -l 10765 >> 123.txt

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值