JUC与JVM(7)JVM GC垃圾回收器与java内存模型(volatile解析)

GC日志信息

如第6话所说:第6话.

[GC (Allocation Failure) [PSYoungGen: 2028K->495K(2560K)] 2028K->603K(9728K), 0.0008646 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

详解

[GC (Allocation Failure)

[PSYoungGen: 2028K->495K(2560K)] 2028K->603K(9728K), 0.0008646 secs]

在yong区发生GC,gc回收前新生代内存大小:占用:2028K,回收后:495K,总大小2560K

堆内存,回收前,占用:2028k,回收后603K,总大小9728K;

[Times: user=0.00 sys=0.00, real=0.01 secs]

JUC与JVM(7)JVM GC垃圾回收器与java内存模型(volatile解析)_第1张图片

[Full GC (Ergonomics)

[PSYoungGen: 368K->0K(1536K)]

新生代

[ParOldGen: 5972K->1414K(7168K)] 6340K->1414K(8704K),

老年代

[Metaspace: 3091K->3091K(1056768K)], 0.0038133 secs]

元空间

[Times: user=0.01 sys=0.00, real=0.01 secs]

full GC同minorGC

GC回收算法

JVM GC回收的3个区域:新生代,老年代,元空间

JVM在进行GC时,并非每次都对上述3个区域进行一起回收,大部分的回收都是新生代;

​ GC如上述分为了2种,一种是轻量级的普通GC(minorGC),一种是全局GC(fullGC);

####minorGC和fullGC的区别:

普通GC:只针对新生区域的GC,发生在新生代的收集器,因为java对象的存活率不高,所以minorGC非常频繁,一般回收速度都快

fullGC(majorGC/fullGC):只发生在老年代的垃圾收集动作,出现了majorGC,经常会伴随至少一次minorGC(但不是绝对),majorGC的速度要比minorGC慢10倍以上;

慢的原因:范围空间更大;

四大算法

  1. 引用计数法
  2. 复制算法
  3. 标记清除
  4. 标记压缩

引用计数法

一个对象的引用,每有一处引用,则这个对象即+1;少一个引用则-1;

当该对象的引用数=0时,则清除;

缺点:

  1. 每次对象赋值均要维护次算法,且计数器本身也有消耗;
  2. 较难处理循环引用
    1. 循环引用:参见下列代码
public class Test2 {
    private Object instance ;
  	private byte[] bigSize = new byte[2*1024*1024]; //用来占用内存
    public static void loopQuote(){
        Test2 objectA = new Test2();
        Test2 objectB = new Test2();
        objectA.instance = objectB;
        objectB.instance = objectA;
        objectA = null;
        objectB = null;
        System.gc();//让gc线程执行gc,注:开发环境一般禁用此方法
    }
}

故JVM的实现一般不采用此算法

复制算法(copy)

年轻代采用的是minorGC,这种gc一般采用复制算法;

复制算法的基本思想是将内存分为两块,每次只用其中一块,当这一块内存(from)快用完时,就将对象复制到另一块(to区)上面,复制算法不会产生内存碎片

from区和to区,谁空谁是to区,即minorGC的复制算法的核心;但是实际上,eden区会有部分对象依然会进入from区;

原理:从根集合(GC Root)开始,通过Tracing从From找到存活对象,拷贝到To中;

​ From和to区交换身份;

采用该算法的原因:eden区的对象存活率不高,一般采用两块10%的内存作为空闲和活动空间,而另外80%的内存,用来个给新建对象分配内存,

优点:不会产生内存碎片;没有标记和清除过程,效率高;

缺点:

  1. 浪费了一半的内存;
  2. 如果对象存活率高,极端假设为100%,那么需要把所有对象都复制一遍,复制所花时间加大,当对象存活率达到一定程度时,将变得不可忽视,复制算法想要使用,最起码对象的存活率要非常低才行;

标记清除(Mark-Sweep)

JUC与JVM(7)JVM GC垃圾回收器与java内存模型(volatile解析)_第2张图片

因为复制算法的浪费空间的,故而设计的标记清除算法;

用于老年代;老年代特点:对象存活时间长,对象存活率高

原理:算法分为标记和清除两个阶段,先标记要清除的对象,然后统一回收;形如:

优点:

  1. 不需要额外空间

缺点:

  1. 两次扫描,耗时严重
  2. 会产生内存碎片(内存是不连续的)

当程序运行时,当可以使用的内存被耗尽时,GC线程就会被触发,并将程序暂停,随便把要回收的对象标记一遍,最终统一回收这些对象,完成标记清除工作后,让程序恢复;

标记压缩(Mark-Compact)

全称:标记-清除-压缩算法,即相比标记清除算法,多一步压缩内存空间的一步;

多出来了一步压缩内存空间的一步,但是加大了时间的消耗;

在这里插入图片描述
在这里插入图片描述
优点:

  1. 不消耗多余内存
  2. 没有内存的碎片化

缺点:

  1. 更加耗时,效率不高,不仅要标记所有存活的对象,同时要整理所有存活对象的引用地址
  2. 标记/整理算法的效率低于复制算法

故可以合并标记清除和标记压缩;

  1. 将Mark-Sweep和Mark-Compact合并
  2. 多次进行Mark-Sweep,后执行Compact

总结

没有最好的算法,只有合适的算法

内存效率:复制算法>标记清除>标记压缩

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

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

效率上:复制算法最佳,浪费太多空间;

JMM java内存模型

JMM(Java Memory Model)本事是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范,定义了程序的各个变量(包括实例,静态字段和构成数组对象的元素)的访问方式
JUC与JVM(7)JVM GC垃圾回收器与java内存模型(volatile解析)_第3张图片
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作空间(栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都在主内存(堆),主内存是共享区域,所有线程都可以访问,但线程堆对变量的操作(读写)必须在工作内存中进行,首先将变量(对象的属性)从主内存中拷贝到线程自己的工作内存中,然后对变量进行操作,操作完成后,再将变量写回主内存中,不同线程无法访问对方的工作内存,所以线程的通信必须借助主内存来完成;如下图所述:

代码验证:

public class Test3 {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            try{
                System.out.println(Thread.currentThread().getName()+" start");
                Thread.sleep(3000);
                number.addTo1024();
                System.out.println(Thread.currentThread().getName()+" update number, number: "+ number.number);
            }catch (Exception e){
                e.printStackTrace();
            }
        },"thread 1").start();

        while (number.number==10){
            //能否跳出此循环?,
        }
        System.out.println("mission over ");
    }
}
class Number{
    int number = 10;
    public void addTo1024(){
        this.number=1024;
    }
}

结果:
JUC与JVM(7)JVM GC垃圾回收器与java内存模型(volatile解析)_第4张图片

​ 1.修改了值,但是while循环依然没有退出,原因如上所述;

  1. 可见性(A线程让给内存中的数据等发生改变,B线程可见)
  2. 原子性(就是说一个操作不能被打断,要么执行完要么不执行,举例:long 和 double 类型是 64 位,在 32 位 JVM 中会将 64 位数据的读写操作分成两次 32 位来处理,所以 long 和 double 在 32 位 JVM 中是非原子操作
  3. 有序性(如果在本线程内观察,所有操作都是有序的;如果一个线程中观察另一个线程,所有的操作都是无序的。前一句是指线程内表现为串行的语义,后一句是指指令重排序现象和工作内存与主内存同步延迟的现象。)

以上是线程安全性的保证

volatile关键字

volatile是java虚拟机提供的轻量级的同步机制 ;

  1. 保证了可见性,如上述代码;
  2. 不保证原子性
  3. 禁止指令重排

代码修改

class Number{
    volatile int number = 10;
    public void addTo1024(){
        this.number=1024;
    }
}

结果

JUC与JVM(7)JVM GC垃圾回收器与java内存模型(volatile解析)_第5张图片
通知了main线程关于number值的修改,故正常退出了while循环

你可能感兴趣的:(JUC,JVM)