Java高级开发之走进JVM与GC

JVM简介

概念:JVM==Java Virtual Machin,意为Java虚拟机。
JVM是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪, JVM是一台被定制过的现实当中不存在的计算机
.VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器

Java内存区域与内存溢出异常

JVM会在执行Java程序的过程中将他管理的内存分为若干个区域:

  • 线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
  • 线程共享区域:Java堆、方法区、运行时常量池

线程私有区域详谈

程序计数器(pc)

是一块较小的内存空间,用来指示当前执行线程字节码执行的地址。若当前执行的是Native方法,则计数器值为空。

由于JVM的多线程是由CPU不断的分配CPU时间片段给不同的线程,因此一个处理器只会执行一条线程中的指令,因此切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各个线程间计数器互不影响,独立存储。

程序计数器内存区域是唯一一个在JVM规范中没有规定OOM(OutOfMemoryError)的区域。

Java虚拟机栈

Java虚拟机栈是描述Java方法执行的内存模型。每个方法都会创建一个栈帧用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每一个方法从调用到执行完成,就对应一个栈帧在虚拟机中入栈和出栈的过程。

虚拟机栈的局部变量表:存在了编译器可知的8种数据类型,对象引用。局部变量表所需的内存在编译期间完成分配,当进入一个方法时,这个方法在帧中分配局部变量空间的大小是完全确定的,不会改变。

Java虚拟机栈会产生的异常:

  • 线程请求的栈(递归调用)的深度大于虚拟机能够允许的深度(1000-2000),会抛出StackOverFlowError异常
  • 虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(OutOfMemoryError)

若是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆(-Xmx参数设置)和减少栈容量(-Xss参数设置)的方式换取更多的线程。

本地方法栈

本地方法栈与虚拟机栈作用完全一样,区别在于本地方法栈为虚拟机使用native方法服务,而虚拟机栈为Java方法服务。

线程共享区域详谈

Java堆

Java堆是JVM所管理的最大的内存区域。Java堆是所有线程共享的一块区域,在Java启动时创建,存放的是对象实例(所有的对象实例和数组都要在堆上分配)。

Java堆是垃圾回收器管理的主要区域(GC堆),Java堆可以处于物理不连续的内存空间中,如果在堆中没有足够的内存完成实例分配并且也无法拓展时,将会抛出OOM

方法区(MateSpace)

用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译i去编译后的代码等数据,在JDK8以前的HotSpot虚拟机中,方法区也被称为“永久代”,JDK8称为元空间。
此区域内存回收主要是针对常量池的回收以及对类型的卸载。
当方法区无法满足内存分配需求时,将抛出OOM

运行时常量池(方法区的一部分)

此区域是方法区的一部分,存放字面量(字符串、final常量、基本数据类型的值)与符号引用(类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符)。

Java堆溢出

Java堆OOM异常是最常见的内存溢出,当异常信息提示"Java heap space",则明确表示OOM发生在堆上。

内存泄漏:泄漏对象不能被GC
内存溢出:内存对象

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

程序计数器、虚拟机栈、本地方法栈的生命周期与相关线程有关,随线程生而生,死而死。这三个区域的内存分配与回收具有确定性,因为当线程结束,内存就跟着线程被回收了。

判断对象已死的三种方法

1.引用计数法:给对象增加一个引用计数器,每当有一个地方引用他时,计数器就加一,当引用失效就减一,任何时刻计数器为0的对象就是不能再被使用,判定对象已死。
python语言采用引用计数法进行内存管理。
但是引用计数法无法解决对象的循环引用问题。
在这里插入图片描述
2.可达性分析算法
Java(C#等语言)默认采用可达性分析算法判断对象是否已死。其核心思想是:通过一系列“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时,证明此对象是不可用的。
在这里插入图片描述
在Java中,可作为GC Roots的对象有:1.虚拟机栈中引用的对象2.方法区中类静态属性引用的对象3.方法区中常量引用的对象4.本地方法栈中native方法引用的方法

Java的引用分为四种:

1.强引用:类似于"Object obj = new Object()",只要强引用存在,垃圾回收器就不会收掉被引用的对象实例。
2.软引用:对于软引用关联的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
3.弱引用:例如ThreadLocal中的key就是以弱引用的方式保存的。被弱引用的对象只能存活到下一次垃圾回收之前,当GC开始,无论当前内容是否够用,都会回收掉只被弱引用关联的对象。
4.虚引用:无法通过虚引用取得一个对象实例,因此一个对象是否有虚引用的存在不会对其生存时间构成影响。为一个对象设置虚引用的唯一目的就是能在对象回收前收到一个系统通知。

在可达性分析算法下的判定对象至少要经历两次标记过程:如果没有与GC Roots相连接的引用链将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否执行finalize()方法。当对象没有覆写或此方法已经被调用过了,那么此时的对象才是真正的“死”对象。
被判定要执行finalize()方法的对象会被放置在F-Queue的队列中,GC会对队列中的对象进行第二次标记,被标记上的对象被移除“即将回收”的集合。
任何一个对象的finalize()方法只会被调用一次

回收方法区

方法区(永久代)的垃圾回收主要收集两部分内容:废弃常量和无用的类

判定一个类是无用类的条件:
1.该类所有的实例都被回收
2.加载该类的ClassLoader已经被回收
3.该类对应的Class对象没有在任何地方被引用,无法通过反射访问该类的方法

垃圾回收算法

1.标记-清除算法
这是最基础的收集算法,需经历“标记”“清除”两个阶段的两次遍历。

算法缺点:1.效率不高2.产生大量不连续碎片,当需要一大片连续空间时就会出发另一次垃圾收集

2.复制算法(新生代回收算法)
他将可用内存划分为大小相等的两块,当其中一块需要垃圾回收时。会将此区域还或者的对象复制到另一块上,然后把已使用的内存一次性全清理。
此算法弥补了标记-清理算法的缺点,现在商用的虚拟机都采用此新生代回收算法来回收新生代,但是实际上并不是分为相等的两块空间,而是将新生代内存分为较大空间(Eden)和较小的两块空间(Survivor From,Survivor To);Eden:Survivor From:Survivor To空间的大小比例大约为8:1:1.

每次只使用Eden和Survivor中的一块,当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor中,最后全部清理刚才用过的Eden和Survivor空间。
当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.分代收集算法
当前JVM垃圾收集都是采用的分代收集算法。将Java堆分为新生代和老年代,在新生代中,每次垃圾收集都有大批对象死去,只有少量存活,因此采用复制算法。而老年代中对象存活率高,没有额外空间对他进行担保,采用标记-清理算法
==(面)==解Minor GC和Full GC么,这两种GC有什么不一样吗
1 Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
2 Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随 至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。 Major GC的速度一般会比Minor GC慢10倍以上。

垃圾收集器

并发:在单CPU中,多条垃圾收集线程并行工作,用户仍处于等待状态。
并行:在多核CPU中,用户线程与垃圾线程同时执行(不一定并行,可能交替执行),用户程序继续运行,而垃圾收集器在另一个CPU上。
吞吐量:吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
GC停顿时间的缩短是以牺牲吞吐量和新生代 空间作为代价的

七种垃圾收集器

Serial收集器(新生代收集器,串行GC)

特性:是单线程收集器,在进行垃圾收集时必须暂停其他工作线程直到它收集结束。
应用场景:在Client模式下默认的新生代收集器
注:Client模式:可开发桌面程序,但在JDK1.8中已取消
Server模式:服务器运行,不加载桌面程序

优势:简单高效

ParNew收集器(新生代收集器,并行GC)

特性:Serial收集器的多线程版本,除了Serial收集器外,目前只有它能与CMS收 集器配合工作
应用场景:在Server模式下的虚拟机中首选的新生代收集

Parallel Scavenge收集器(新生代收集器,并行GC)

特性: Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,具有GC自适应的调节策略(监控JVM状态,调整新生代与Survivor区域的比例;调整合适的停顿时间和吞吐量)Parallel Scavenge收集器也经 常称为“吞吐量优先”收集器
应用场景:停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用 CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
Parallel Scavenge收集器 VS ParNew收集器: Parallel Scavenge收集器与ParNew收集器的一个重要区别 是它具有自适应调节策略

Serial Old收集器(老年代收集器,串行GC)

特性: Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
应用场景
Client模式使用。
Server模式下:一种用途是在JDK 1.5以及之前的版本中与 Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用

Parallel Old收集器(老年代收集器,并行GC)

特性: Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS收集器(老年代收集器,并发GC)

特性:基于标记清理算法,是一种以获取最短停顿时间为目标的收集器(响应速度高)
CMS的运作过程:

  • 初识标记,速度快,但需要Stop-The-World
  • 并发标记,是进行GC Roots Tracing的过程
  • 重新标记,修正并发标记期间用户运作程序产生的标记变动。比并发标记时间短,但需要Stop-The-World
  • 并发清除:清除对象

缺点:
占用线程,对CPU资源敏感; CMS默认启动的回收线程数是(CPU数量+3)/ 4
CMS收集器无法处理浮动垃圾(与CMS并行运行的用户线程产生的垃圾)
CMS收集器会产生大量空间碎片

CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作

Parallel Scavenge收集器 VS CMS等收集器: Parallel Scavenge收集器的特点是它的关注点与其他收集器不 同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的 目标则是达到一个可控制的吞吐量(Throughput)。

G1收集器(唯一一款全区域的垃圾回收器

G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region块,然后 并行的对其进行垃圾回收。
G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同。
执行流程:

  • 初始标记,需要Stop-The-World,从根对象出发,根minor GC一起发生
  • 并发标记:标记的同时G1计算每个块对象的存活率
  • 最终标记:采用SATB算法标记可达对象
  • 筛选回收:对低存活率的块进行回收,与minor GC一起发生。

七种收集器的配合关系
在这里插入图片描述

理解GC日志

一般GC日志:

[GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]     
[Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]  

在这里插入图片描述

内存的分配与回收策略

1.对象优先在Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机发生一次Minor GC.
2.大对象(需要大量连续空间的Java对象)直接进入老年代,但是经常出现大对象容易导致内存不足触发GC。
3.长期存活的对象将进入老年代。在Eden出生的对象每经历一次Minor GC年龄就增加一岁,当增加到15岁时就晋升为老年代;或是在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的直接进入老年代。
4.空间分配担保
在这里插入图片描述
取平均值是一种概率事件,失败后会重新发起Full GC.

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

1.JDK\bin目录下的监控和故障处理工具
在这里插入图片描述

Java内存模型

主内存(操作系统内存)与工作内存(线程拥有的区域)

Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该 线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存进行而不能直接 读写主内存中的变量不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主 内存来完成

内存间交互操作

lock()锁定
unlock()解锁
read()读取
load()载入
use()使用
assign()赋值
store()存储
write()写入
线程、主内存、工作内存三者的交互关系如下所示
在这里插入图片描述

Java内存模型的三大特性

1.原子性:Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和read,若需要大范围的原子性,需要synchronized关键字
2.可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,。volatile、 synchronized、final三个关键字可以实现可见性。
3.有序性:在本线程内观察,所有的操作都是有序的(线程内表现为串行);如果在线程中观察另外一个线程,所有的操作都是无 序的。

Java内存模型保证有序性的条件是遵循happens-before原则
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、 Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

volatile型变量的特殊规则

关键字volatile可以说是JVM提供的轻量级的同步机制,当一个变量被定义为volatile后,它具备两种特性:
1.保证此变量对所有线程可见(同指同步),volatile变量在各个线程中是一致的,但是volatile变量的运算在并发下一样是不安全的。在不符合【1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值 2. 变量不需要与其他的状态变量共同参与不变约束】,我们仍然需要通过加锁(synchronized或 者lock)来保证原子性
2.volatile变量禁止指令重排
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经 对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句 放到其前面执行
例如单例模式中的double check可用volatile声明变量。

public class Singleton{
    private Singleton() {
    }
    private  volatile static Singleton instance;
    public  static Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance=new Singleton();
                }
            }
        }
        return instance;
    }
}

对象的深浅拷贝

深拷贝:会同时拷贝一个实例化对象,实现深拷贝需用序列化实现(类实现序列化接口)
浅拷贝:拷贝的对象虽然是双份的,但是器对象的引用是共享的,没有被拷贝。
//深拷贝

public class CloneTest2 {
    public static void main(String[] args) throws CloneNotSupportedException {
        //深拷贝,会同时拷贝一个实例化对象
        //实现深拷贝需用序列化实现(类实现序列化接口)
        Teacher teacher=new Teacher("刘","java");
        Student student=new Student("zhang",12,teacher);
        System.out.println("原student"+student);
        Student cloneStudent=student.cloneStudent();
        System.out.println("克隆student"+cloneStudent);

        teacher.setName("Peter");
        System.out.println("改变teacher信息");
        System.out.println("原student"+student);
        System.out.println("克隆student"+cloneStudent);

    }
}

//浅拷贝

public class CloneTest1 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Teacher teacher=new Teacher("刘","java");
        Student student=new Student("zhang",12,teacher);
        System.out.println("原Student"+student);
        System.out.println("克隆的student"+student.clone());
        //检验teacher是否拷贝了
        //如果改变teacher信息,student和克隆student中关于teacher的信息都改变了,说明teacher是共享的
        //如果改变teacher信息,student关于teacher的信息都改变了,
        // 克隆student中关于teacher的信息未改变了,明teacher是不共享的

        //最终结果克隆对象也改变了,因此克隆是浅拷贝(仍保留同一个引用对象)
        teacher.setName("Peter");
        System.out.println("改变teacher信息");
        System.out.println("原student"+student);
        System.out.println("克隆student"+student.clone());
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_42962924/article/details/86615771
今日推荐