JVM内存结构和JMM内存模型

1 Java内存区域

Java运行时数据区域包括:线程私有区域和线程共享区域

1.1 线程私有区域

(1)程序计数器

  • 程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
  • 如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。
  • 程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM(out of memory)情况的区域

(2)Java虚拟机栈
我们常说的栈就是此处的虚拟机栈,也就是局部变量表部分
局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。
此区域一共会产生以下两种异常:
1、如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。
2.、虚拟机在动态扩展时无法申请到足够的内存,会抛OOM(OutOfMemoryError)内存溢出

(3)本地方法栈
与虚拟机栈的作用一样,主要区别是本地方法栈为虚拟机使用的Native方法服务,而虚拟机栈为JVM执行的Java方法服务。

1.2 线程共享区域

(1)Java堆
是Java虚拟机管理的最大的一块内存,也是GC的主战场,在JVM启动时创建,所有的对象实例以及数组都要在堆上分配 ,又因为是垃圾回收器所管理的主要区域,又叫“GC”堆,如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM 。-Xms:设置堆的最小值、-Xmx:设置堆最大值
(1)从内存回收角度,Java堆被分为新生代和老年代;这样划分的好处是为了更快的回收内存;
(2)从内存分配角度,Java堆可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB);这样划分的好处是为了更快的分配内存;
在这里插入图片描述
(2)方法区
用于存储已被虚拟机加载的类信息常量静态变量, 即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OOM异常。
(3)运行时常量池
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

2 内存溢出异常
2.1 Java堆溢出

不断地产生对象,并且不能被GC处理,对象数量就会达到最大堆容量,此时会产生内存溢出异常

实例
public class HeadTest {
    static class OO {

    }
    public static void main(String[] args) {
        List<OO> list = new ArrayList<>();
        while(true){
            list.add(new OO());
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

内存泄漏 : 泄漏对象无法被GC
内存溢出 : 内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM堆内存调大或者检查对象的生命周期是否过长。

3 垃圾回收器与内存分配策略

对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。

3.1判断对象存活的方法

(1)引用计数法(不采用)
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。缺点是:没办法解决对象的循环引用问题,故未在JVM中使用。
(2)可达性算法分析(采用)
3.1.1算法核心: 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的,对象已死,判定为可回收对象。
在Java语言中,可作为GC Roots的对象包含下面几种:

1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中JNI(Native方法)引用的对象

3.1.2 引用:如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。

  • 强引用:强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类的引用,对象被任意一个强引用指向,即使抛出OOM,垃圾回收器也不会回收掉被强引用指向的对象实例。
  • 软引用:软引用是用来描述一些还有用但是不是必须的对象。若对象只被软引用指向,当前内存够用时不发生回收当抛出OOM时,一次性回收所有被软引用指向的对象。在JDK1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用:弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,**无论当前内容是否够用,都会回收掉只被弱引用关联的对象。**在JDK1.2之后提供了WeakReference类来实现弱引用。
  • 虚引用:不对对象的生存周期产生影响,无法通过该引用取得对象实例,为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 在JDK1.2之后,提供了PhantomReference类来实现虚引用。

3.1.3判断对象死亡还是存活
要宣告一个对象的真正死亡,至少要经历两次标记过程 : 如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,看此对象是否有必要执行finalize()方法。
(1)当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况都视为"没有必要执行",此时的对象才是真正"死"的对象。
(2)如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法,但不承诺会等到它运行结束)。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。

public class Test1 {
    public static Test1 test;

    public void isAlive() {
        System.out.println("I am alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        //与引用建立关联
        test = this;
    }

    public static void main(String[] args) throws Exception {
        test = new Test1();
        //第一次成功拯救自己
        test = null;
        System.gc();
        
        Thread.sleep(500);
        if (test != null) {
            test.isAlive();
        } else {
            System.out.println("no,I am dead :(");
        }
        //下面代码与上面完全一致,但是此次自救失败
        test = null;
        System.gc();
        Thread.sleep(500);
        if (test != null) {
            test.isAlive();
        } else {
            System.out.println("no,I am dead :(");
        }
    }
}
finalize method executed!
I am alive :)
no,I am dead :(

一次逃脱成功,一次失败,任何一个对象的finalize()方法都只会被系统自动调用一次,如果相同的对象在逃脱一次后又面临一次回收,它的finalize()方法不会被再次执行,因此第二次的自救行动失败。

3.2 回收方法区

方法区(永久代)的垃圾回收主要收集两部分内容 : 废弃常量和无用的类。
废弃的常量:假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池的"abc"常量,也没有在其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个"abc"常量会被系统清理出常量池。
无用的类":

  1. 该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法
3.3 垃圾回收算法
3.3.1

1. 标记-清除算法
"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
缺点: 标记和清除过程效率不高,之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
2. 复制算法(新生代回收算法)
每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。
新生代分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间(一个称为From区,另一个称为To区域)
Eden:Survivor From : Survivor To = 8:1:1。
每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
HotSpot实现的复制算法流程如下:
1.当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
3. 标记-整理算法(老年代回收算法)
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
4 . 分代收集算法
根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
面试题:Minor GC和Full GC么有什么不一样?
1.新生代GC(Minor GC) : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
2.老年代GC(Full GC /Major GC) : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

3.4 垃圾收集器

并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态
并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个CPU上。
吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。在这里插入图片描述

3.5 内存分配与回收策略

对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发生一次Minor GC
大对象直接进入老年代
所谓的大对象是指,需要大量连续空间的Java对象,典型的大对象就是那种很长的字符串以及数组,虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,
避免Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)
长期存活的对象将进入老年代
对象在 Survivor空间中每"熬过"一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将晋升到老年代中对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
动态对象年龄判定
为了能更好的适应不同程序的内存状况,JVM并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间, 如果大于,则此次Minor GC是安全的 如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。 如果为true,那么会继续检查老年代可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者设置值false,则改为进行一次Full GC。 老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,如果出现了分配担保失败,就只能在失败后重新发起一次Full GC

4. 常用JVM性能监控与故障处理工具

在这里插入图片描述
jps : 虚拟机进程状态工具
jstat : 虚拟机统计信息监视工具
jinfo : Java配置信息工具
jmap : Java内存映像工具
jhat : 虚拟机转存储快照分析工具
jstack : Java堆栈跟踪工具

5. Java内存模型(Java Memory Model,JMM
5.1 主内存与工作内存

Java内存模型规定了所有的变量,包括实例字段、静态字段和构成数组对象的元素,都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

5.2 内存间交互操作

8种
Java内存模型的三大特性 :

  • 原子性 : 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store。大致可以认为,基本数据类型的访问读写是具备原子性的。如若需要更大范围的原子性,需要synchronized关键字约束。(即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行)
  • 可见性 : 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile、synchronized、final三个关键字可以实现可见性。
  • 有序性 : 如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外一个线程,所有的操作都是无序的。前半句是指"线程内表现为串行",后半句是指"指令重排序"和"工作内存与主内存同步延迟"现象。
    happens-before原则(先行发生原则):如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性

volatile型变量的特殊规则

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值