【Java】——详解JVM与GC

一、JVM内存模型

1.1 JVM的主要组成

JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
在这里插入图片描述

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime - - data area中的method area。
  • Execution engine(执行引擎):执行classes中的指令。
  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):这就是我们常说的JVM内存

1.2 Java程序的运行机制

  • 首先使用IDE集成开发工具编写Java源代码,此时得到的源文件的后缀为.java;
  • 再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
  • 运行字节码的工作是由解释器(java命令)来完成的。
  • 紧接着,类加载器又将这些.class文件加载到JVM中。在这里插入图片描述

一句话总结归纳:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

1.3 JVM内存模型

Java 虚拟机在执行 Java 程序的过程中,会将其所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。
Java 虚拟机所管理的内存被划分为如下几个区域:
在这里插入图片描述
其中各个部分的作用如下:

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
  • Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

二、内存溢出

2.1 Java的内存泄漏

JVM是使用了引用计数法和可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么在这种情况下,由于代码的实现不同,便会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)
一句话总结:堆内存中一个对象不再使用时,垃圾回收器却无法从内存中删除他们。长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。

2.2 内存泄漏的解决

内存泄漏几种情况分析:

2.2.1 ThreadLocal

  • ThreadLocal
    使用ThreadLocal时,每个线程只要处于存货状态就可保留对其ThreadLocal变量副本的隐式调用,且将保留其自己的副本。使用不当,就会引起内存泄露。

    一旦线程不在存在,ThreadLocals就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾回收器回收。所以使用到ThreadLocals来保留线程池中线程的变量副本时,ThreadLocals没有显示的删除时,就会一直保留在内存中,不会被垃圾回收。

  • 解决方法

  1. 不再使用ThreadLocal时,调用remove()方法,该方法删除了此变量的当前线程值。
  2. 不要使用ThreadLocal.set(null),它只是查找与当前线程关联的Map并将键值对设置为当前线程为null

2.2.2 static

  • static
    大量使用static字段会潜在的导致内存泄露,在Java中,静态字段通常拥有与整个应用程序相匹配的生命周期。

    比如:单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。

  • 解决方法

  1. 最大限度的减少静态变量的使用;
  2. 单例模式时,依赖于延迟加载对象而不是立即加载方式。

2.2.3 集合容器

  • 集合容器
    通常我们将一些对象的引用加入到了集合容器(比如ArrayList)中,如果在不需要该对象时,没有把对象的引用从集合中清理掉,这样这个集合就会越来越大。

    如果这个List是临时的,那没问题,List被回收后里边的对象引用也就不会被持有了(对象不可达),对象引用也会被回收。如果这个List不是临时的,那么就会导致内存占用越来越大。

    如果这个集合是static的话,那情况就更严重了。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

  • 解决方法
    如果是static类型的集合,在退出程序之前,将集合中的内容clear,然后置为null,再退出程序。

三、垃圾回收GC

3.1 Java的垃圾回收机制

在java中,作为程序员我们是不需要显示的释放一个对象的内存,而是由虚拟机自行执行的。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

3.2 如何找“垃圾”

在java中我们主要有两种方式来找出“垃圾”:

3.2.1 引用计数法

基本思想:给对象增加一个引用计数器, 每当有一个地方引用它时, 计数器就 + 1; 当引用失效时, 计数器就 - 1; 任何时刻计数器为 0 的对象就是不能再被使用的对象。

3.2.2 可达性分析

基本思想: 通过一系列称为 " GC Roots" 的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称之为 " 引用链", 当一个对象到 GC Roots 没有任何的引用链时, 则证明是不可用的对象。

3.3 垃圾回收算法

3.3.1 标记-清除法

作为最基本的回收算法,标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
在这里插入图片描述
从图中可以很容易看出标记-清除法实现起来较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

3.3.2 复制法

为了解决标记-清除法的缺陷,复制算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
在这里插入图片描述
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。显然,复制法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么复制法的效率将会大大降低。

3.3.3 标记-整理法(压缩法)

为了解决复制法的缺陷,充分利用内存空间,提出了标记-整理法。该算法标记阶段和标记-清除法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
在这里插入图片描述

3.3.3 分代收集法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理法(压缩法)。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值