JVM探究(二):堆和垃圾回收,GC四种算法,JMM

9 堆

Heap, 一个JVM只有一个堆内存,堆内存大小可调节.

默认情况下:分配的总内存是电脑内存的1/4,初始化的内存:1/64

堆内存分为三个区域:

  • 新生区(伊甸园区)(Young/New)
  • 养老区(Old)
  • 永久区(Perm)
    JVM探究(二):堆和垃圾回收,GC四种算法,JMM_第1张图片

GC垃圾回收,主要是在伊甸园区(轻GC)和养老区(重GC).幸存0区和幸存1区是动态交换的,经过1次或者多次GC仍存活的对象,进入幸存区.超过次数阈值后,进入养老区,养老区内对象一般不会被回收.内存满了(OOM),堆内存不够,严重错误.99%的对象都是临时对象.

出现OOM:

1.尝试扩大堆内存空间,看结果

2.仍出错,分析内存

//-Xms8m -Xmx8m -XX:+PrintGCDetails
public class Hello {
    public static void main(String[] args) {
        String str = "hello,world~~~~~~~~~~";
        while (true) {
            str += str + new Random().nextInt(888888888)+ new Random().nextInt(999999999);
        }
    }
}

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

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

MAT,Jprofiler作用:

  • 分析Dump内存文件,快速定位内存泄漏
  • 获得堆中的数据
  • 获得大的对象等等
//虚拟机设置调优
//-Xms 设置初始化内存分配大小  1/64
//-Xmx 设置最大分配内存 默认1/4
//-XX:+PrintGCDetails  //打印GC垃圾回收信息
//-XX:+HeapDumpOnOutOfMemoryError  //oom Dump

import java.util.ArrayList;

//-Xms1m -Xmx8m -XX:HeapDumpOnOutOfMemoryError
public class Demo01 {
    byte[] array = new byte[1*1024*1024];  //1m

    public static void main(String[] args) {
        ArrayList<Demo01> list = new ArrayList<>();
        int count = 0;

        try {
            while (true) {
                list.add(new Demo01()); //
                count = count + 1;
            }

        }catch (Error e) {
            System.out.println("count:"+count);
            e.printStackTrace();
        }
    }
}

JDK8以后,永久存储区改为**(元空间)**

新生区:类诞生,成长的地方

  • 伊甸园区:所有的对象都是在伊甸园区new出来的
  • 幸存者区(0,1)

老年区

永久区

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

元空间在逻辑上存在,物理上不存在(实际年轻代+老年代的内存=堆内存大小)

永久区OOM:一个启动类,加载了大量的第三方jar包.Tomcat部署了太多的应用.大量动态生成的反射类.若这些东西不断被加载有可能OOM

  • jdk1.6之前: 永久代,常量池在方法区;
  • jdk1.7: 永久代,逐渐退化,体场去永久代,常量池在堆中
  • jdk1.8:无永久代,常量池在元空间.
//测试元注解
@MyAnnotation
public class Test01 {

    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();  //字节
        //返回jvm的初始化总内存
        long total = Runtime.getRuntime().totalMemory();

        System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)+"MB");
        System.out.println("total="+max+"字节\t"+(max/(double)1024/1024)+"MB");

    }
    @MyAnnotation
    public void test(){

    }
}

//定义一个注解
//Target 表示注解可以用在什么地方
@Target(value = {ElementType.METHOD,ElementType.TYPE})

//Retention 表示注解在什么地方还有效
@Retention(value = RetentionPolicy.RUNTIME)

@Documented
@Inherited
@interface MyAnnotation {
}

10 垃圾回收GC

GC作用域:只在堆和方法区

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

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

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

GC 题目:

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

引用计数法:给每个对象设置一个计数器,根据对象使用次数决定回收

JVM探究(二):堆和垃圾回收,GC四种算法,JMM_第2张图片

复制算法(主要用于年轻代):

1.每次GC都会将Eden活的对象移到幸存区中:一旦Eden区被GC后,就会是空的!

2.幸存区,谁空谁是to.from和to会不停地切换.

3.当一个对象经历15次(默认)GC,还没有死,进入养老区(-XX:MaxTenuringThreshold = 10)设定进入老年代的时间

  • 优点:没有内存碎片.
  • 缺点:浪费内存空间:多了一半空间永远为空.假设对象100%存活(极端下),

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

JVM探究(二):堆和垃圾回收,GC四种算法,JMM_第3张图片

大致过程:第一次GC后,Eden区和To区为空,Eden中存活对象进入To区,From区中对象复制进入To区,此时原本的From幸存区为空,置为To区,重复此过程.幸存区活过15次的对象进入养老区,没活过,死掉了.

标记清除算法:

JVM探究(二):堆和垃圾回收,GC四种算法,JMM_第4张图片

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

标记压缩算法

再优化:再次扫描,向一端移动存活的对象,多了一个移动成本

标记清除压缩算法:先标记清除几次,再压缩

JVM探究(二):堆和垃圾回收,GC四种算法,JMM_第5张图片

总结:

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

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

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

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

年轻代:存活率低,复制算法

老年代:存活率高,区域大,标记清除(内存碎片不是太多)+标记压缩混合实现

11 JMM:Java Memomy Model

学习新技术思路:

1 . 什么是JMM?(官方定义,百科)

2.它干嘛的?(官方;其他人的博客,对应的视频)

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

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

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

3.如何学习?

JMM:抽象的概念,理论(落到实际)

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

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

JMM对这八种指令的使用,制定了如下规则:

    • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
    • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
    • 不允许一个线程将没有assign的数据从工作内存同步回主内存
    • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
    • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
    • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
    • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
    • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

程序员:学习新东西是常态

面试学习:3/10通过,pass,总结成面经,分析这10个题,触类旁通类似的题(百度,牛客,面试题),通过大量面试总结,得出一套解题的思路(3-4道解题思路).了解问题的本质.

你可能感兴趣的:(Java,jvm,java,编程语言)