JVM快速入门(下)

  • 前言
  • 10.三种JVM
  • 11.堆
  • 12.新生区
  • 13.永久区
  • 14.堆内存调优
  • 使用JPofiler工具分析OOM原因
  • 引用计数法:
  • 复制算法:
  • 标记清除:
  • 标记压缩
  • 标记清除压缩
  • 总结
  • JMM
  • 1.什么是JMM?
  • 2.它干嘛的?:官方,其他人的博客,对应的视频!
  • 3.它该如何学习?


前言

这篇文章是对JVM快速入门(上)的补充,没有看上一篇的小伙伴可以去温习一下。这一篇文章的内容主要针对JVM堆内存来讲讲垃圾回收。

10.三种JVM

  • Sun公司 HotSpot
  • java am java阿曼之蛮荒法则_后端

  • BEA JRockit
  • IBM J9VM

我们学习的都是:HotSpot

11.堆

Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。

类加载器读取了类文件后,一般会把什么东西放在堆中?类,方法,常量,变量~,保存我们所有引用类型的真实对象;

堆内存中还要细分为三个区域:

  • 新生区(伊甸园区)
  • 养老区
  • 永久区

java am java阿曼之蛮荒法则_JVM_02

一些***内存垃圾***会在***新生区的伊甸园区***中的***幸存区***经过***GC层层筛选***后,再决定是否***过渡到养老区***中。

GC垃圾回收,主要是再伊甸园区和养老区~

假设内存满了,OOM,堆内存不够!java.lang.OutOfMemoryError:Java heap space

在JDK8以后,永久存储区改了个名字(元空间);

12.新生区

  • 类:诞生和成长的地方,甚至死亡;
  • 伊甸园,所有的对象都是在伊甸园区new出来的
  • 幸存者区(0,1)

java am java阿曼之蛮荒法则_java am_03

**整体过渡过程:**伊甸园==(轻GC)=>幸存者0区、1区(重GC)=>养老区(满了后)==>OOM 报错

真理:经过研究,***99%的对象***都是临时对象!

13.永久区

这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟就会释放这个区域的内存

一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM;

  • jdk1.6之前:永久代,常量池是在方法区;
  • jdk1.7 :永久代,但是慢慢的退化了,去永久代,常量池在堆中
  • jdk1.8之后:无永久代,常量池在元空间

java am java阿曼之蛮荒法则_java am_04

元空间:逻辑上存在:物理上不存在

设置为8M测试OOM溢出

-Xms8m -Xmx8m -XX:+PrintGCDetails

java am java阿曼之蛮荒法则_后端_05


java am java阿曼之蛮荒法则_后端_06

在一个项目中,突然出现了OOM故障,那么该如何排除研究为什么出错

  • 能够看到代码第几行出错:内存快照分析工具,MAT(eclipse),Jprofiler
  • Debug,一行行分析代码!

MAT(eclipse),Jprofiler作用:

  • 分析Dump内存文件,快速定位内存泄漏;
  • 获取堆中的数据
  • 获得大的对象~

14.堆内存调优

使用JPofiler工具分析OOM原因

首先需要在Setting中进行Vm设置,然后会在相关文件夹生成hprof文件,打开后在线程中可以找到错误的点。

package com.kuang.Jvm;

import java.util.ArrayList;

//Setting设置
// -Xms 设置初始化内存分配大小 1/64
// -Xmx 设置最大分配内存,默认1/4
// -XX:+PrintGCDetails  //打印GC垃圾回收信息
// -XX:+HeapDumpOnOutOfMemoryError  //oom Dump

//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

public class Jprofilter_test {

    byte[] array = new byte[1*1024*1024]; //1m
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();
        int count = 0;

        try{
            while(true){
                list.add(new Object()); //问题所在
                count = count + 1;
            }
        }catch (Error e){
            System.out.println("count"+count);
            e.printStackTrace();
        }
    }
}

15.GC

GC的作用范围一般是方法区Method Area和堆Heap

java am java阿曼之蛮荒法则_JVM_07

JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代~

  • 新生代
  • 幸存区(from , to)
  • 老年区

GC两种类:轻GC(普通的GC),重GC(全局GC)

GC题目:

  • JVM的内存模型和分区~详细到每个区放什么?
  • 堆里面的分区有哪些?Eden,form,to,老年区,说说他们的特点!
  • GC的算法有哪些?标记清除法,标记整理/压缩,复制算法,引用计数器,怎么用的?
  • 轻GC和重GC分别在什么时候发生?

引用计数法:

java am java阿曼之蛮荒法则_JVM_08

复制算法:

java am java阿曼之蛮荒法则_java am_09


java am java阿曼之蛮荒法则_后端_10

  • 好处:没有内存的碎片~
  • 坏处:浪费了内存空间~:多了一半空间永远是空to。假设对象100%存活(极端情况)OOM

复制算法最佳使用场景:对象存活度较低的时候;新生区~

标记清除:

java am java阿曼之蛮荒法则_开发语言_11

  • 优点:不需要额外的空间!
  • 缺点:两次扫描,严重浪费时间,会产生内存碎片。

标记压缩

在标记清除的基础上加了一个移动成本再次进行了扫描。

java am java阿曼之蛮荒法则_JVM_12

标记清除压缩

先标记清除5次,在进行一下压缩。

总结

内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程

思考一个问题:难道没有最优算法吗?

答案:没有,没有最好的算法,只有最合适的算法----->GC:分代收集算法

年轻代:

  • 存活率低
  • 复制算法!(主要要快)

老年代:

  • 区域大:存活率高
  • 标记清除(内存碎片不是太多)+标记压缩混合 实现(主要是要清理干净且需要内存较少!!!)

JMM

1.什么是JMM?

JMM:(java Memory Model的缩写)

2.它干嘛的?:官方,其他人的博客,对应的视频!

java am java阿曼之蛮荒法则_java am_13

作用:缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)。

JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

java am java阿曼之蛮荒法则_开发语言_14

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:

  • 线程之间的共享变量存储在主内存(Main Memory)中
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
  • Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。

解决共享对象可见性这个问题:volilate

3.它该如何学习?

JMM:抽象的概念、理论

Java内存模型定义了以下八种操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。