狂神说——JVM笔记

三种JVM

●Sun公司HotSpot Java Hotspot™ 64-Bit Server VM (build 25.181-b13,mixed mode)

●BEA JRockit

●IBM J9VM

我们学习都是: Hotspot

JVM体系结构

JVM的位置

在这里插入图片描述

JVM的体系结构

在这里插入图片描述

  • 方法区:方法区存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据;是jvm规范中的一部分,并不是实际的实现,在实际实现上并不相同(HotSpot在1.7版本以前和1.7版本,1.7后都有变化)。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
  • Java堆:仅有一个堆,Java堆用于存放new出来的对象的内容。是垃圾收集器管理的主要区域。可细分为:新生代和老年代;新生代又可分为Eden,from Survivor,to Survivor。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  • Java虚拟机栈:存放的东西:八大基本类型+new出来的对象引用地址+实例方法的引用地址。每一条java虚拟机线程都有自己私有的java虚拟机栈,这个栈和线程同时创建,用于存储栈帧。Java虚拟机栈是Java方法执行的内存模型,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧(Stack Frame)存储局部变量表,操作数栈,动态链接,方法出口等信息,随着方法的调用而创建,随着方法的结束而销毁。在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
  • 本地方法栈:本地方法栈和虚拟机栈非常相似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
  • 程序计数器:java虚拟机可以支持多个线程同时运行,每个java虚拟机线程都有自己的程序计数器(PC寄存器),在任一时刻,一个java虚拟机的线程,只会执行一个方法的代码。那么程序计数器记录当前线程所执行的Java字节码的地址。当执行的是Native方法时,程序计数器为空。程序计数器是JVM规范中唯一一个没有规定会导致OOM(OutOfMemory)的区域。

类加载的过程

在这里插入图片描述

1、如上图所示,Java源代码文件会被Java编译器编译为字节码文件(.class后缀)然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。

2、百分之99的JVM调优都是在堆中调优,Java栈、本地方法栈、程序计数器是不会有垃圾存在的。

类加载的过程

在这里插入图片描述

其中加载、验证、准备、初始化、卸载这五个阶段的过程是固定的,在类加载过程中必须按照这种顺序按部就班的进行,而解析阶段则不一定,可以在初始化以后进行,是为了支持java语言的运行时绑定

加载

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据

验证

这一阶段主要是为了确保Class文件的字节流中包含的信息符合虚拟机的要求,并且不会危害虚拟机自身的安全。

四个校验动作

  • 文件格式验证:验证字节流是否符合Class文件格式的规范
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
  • 字节码验证:通过数据流和控制流分析。确定程序语义是合法的、符合逻辑的
  • 符号引用验证:确保解析动作能正确执行

准备

是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都将在方法区分配

进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在java堆中, 初始值通常情况下是数据类型默认的零值

解析

是将虚拟机常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定附

符号引用:与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。虚拟机能接收的符号引用必须是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。

直接引用:可以是指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄,如果有了直接引用那么引用目标必定已经在内存中存在

初始化

类初始化时类加载的最后一步,处理加载阶段,用户可以通过自定义的类加载器参数,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码

类初始化的主要工作时为了静态变量赋程序设定的初值

static int a=100; 在准备阶段a被赋默认值0,在初始化阶段就会被赋值为100

java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:

  • 使用new创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行初始化。
  • 通过java.lang.reflect包的方法对类进行反射调用的时候,要是类没有进行过初始化,则要首先进行初始化
  • 当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化
  • 当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类
  • 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

类加载器

作用:加载.class文件。

新建的对象放入堆里面,引用(地址)放到栈,其中引用指向堆里面对应的对象。

在这里插入图片描述

1)启动类(根)加载器 Bootstrap ClassLoader
2)扩展类加载器 Extension ClassLoader
3)应用程序(系统类)加载器 Application ClassLoader

1-启动类加载器,负责加载jre\lib目录下的rt.jar包

2-扩展类加载器:负责加载jre\lib\ext目录下的所有jar包

3-应用程序类加载器:负责加载用户类路径上所指定的类库,如果应用程序中没有自定义加载器,那么次加载器就为默认加载器。

双亲委派机制

在这里插入图片描述

双亲委派机制的工作过程:

  1. 类加载器收到类加载的请求;
  2. 把这个请求委托给父加载器去完成,一直向上委托,直到启动类(根)加载器;
  3. 启动类加载器检查能不能加载(使用findClass()方法),能加载就结束;否则抛出异常,通知子加载器进行加载;
  4. 重复步骤三.

举个例子

大家所熟知的String类,String默认情况下是启动类加载器进行加载的。假设我也自定义一个String,并且制定加载器为自定义加载器。现在你会发现自定义的String可以正常编译,但是永远无法被加载运行。

因为申请自定义String加载时,总是启动类加载器加载,而不是自定义加载器,也不会是其他的加载器。

在这里插入图片描述

为什么要设计这种机制

这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

Native、本地方法栈

Native

  • native :凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
  • 会进入本地方法栈,然后通过本地接口 (JNI),调用本地方法库
  • JNI作用:开拓Java的使用,融合不同的编程语言为Java所用,Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序
  • 它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法
  • 在最终执行的时候,通过本地接口 (JNI),加载本地方法库中的方法
  • private native void start0();

本地方法栈(Native Method Stack)

它的具体做法是Native Method Stack中登记native方法,在执行引擎执行的时候通过本地接口 (JNI),加载本地方法库(Native Libraies)。

本地接口(Native Interface)JNI

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序, Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了块区域处理标记为native的代码,它的具体做法是在Native Method Stack 中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。
  目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!

栈、堆、方法区

在这里插入图片描述

PC寄存器

程序计数器: Program Counter Register
 每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计

方法区Method Area

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量和数组的内容存在堆内存中,和方法区无关

在这里插入图片描述

栈 stack

在这里插入图片描述

栈:栈内存,主管程序的运行,生命周期和线程同步;

线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题

一旦线程结束,栈就Over!

在这里插入图片描述

堆 Heap

一个JVM仅有一个堆内存,堆内存大小可以调节

在这里插入图片描述

  1. JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。

  2. 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。Eden满了就触发轻GC,经过轻GC存活下来的就到了幸存者区,幸存者区满之后意味着新生区也满了,则触发重GC,经过重GC之后存活下来的就到了老年代。

  3. 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。

  4. 老年代:在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

    在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

  5. 非堆内存用途:永久代,也叫方法区存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

分代概念

1、新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
2、老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。
Minor GC : 清理年轻代
Major GC : 清理老年代
Full GC : 清理整个堆空间,包括年轻代和永久代
所有GC都会停止应用所有线程。

元空间

在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
元空间有注意有两个参数:

  • MetaspaceSize :初始化元空间大小,控制发生GC阈值
  • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

移除永久代原因

​ 为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。有了元空间就不再会出现永久代OOM问题了!

堆内存调优

在这里插入图片描述

在这里插入图片描述

System.out.println(“最大内存Max_memory=+Runtime.getRuntime().maxMemory()/(double)1024/1024+M);
System.out.println(“初始化内存大小Total_memory=+Runtime.getRuntime().totalMemory()/(double)1024/1024+M);

在这里插入图片描述

虚拟机参数打印信息:在这里插入图片描述

JPofiler工具分析OOM

MAT, Jprofiler作用

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

MAT是eclipse集成使用 在这里不学

Jprofile使用

1.在idea中下载jprofile插件
2.联网下载jprofile客户端

3.在IDEA设置中指定jprofile的客户端exe文件地址

3.在idea中VM参数中写参数 -Xms1m -Xmx8m -XX: +HeapDumpOnOutOfMemoryError
4.运行程序后在jprofile客户端中打开找到错误 告诉哪个位置报错
命令参数详解
// -Xms设置初始化内存分配大小/164
// -Xmx设置最大分配内存,默以1/4
// -XX: +PrintGCDetails // 打印GC垃圾回收信息
// -XX: +HeapDumpOnOutOfMemoryError //oom DUMP

垃圾回收算法GC

在这里插入图片描述

JVM在进行GC时,并不是对这三个区域(jdk1.8以后不存在永久区,改名元空间,不在JVM,是在本地内存中的)统一回收。 大部分时候,回收都是新生代~
●新生代
●幸存区(form区,to区)
●老年区

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

●JVM的内存模型和分区~详细到每个区放什么?

●堆里面的分区有哪些? Eden, form, to, 老年区,说说他们的特点!

●GC的算法有哪些? 标记清除法,标记整理,复制算法,引用计数器

●轻GC和重GC分别在什么时候发生?

GC四大算法

引用计数算法

在这里插入图片描述

在JVM中几乎不用,每个对象在创建的时候,就给这个对象绑定一个计数器(有消耗)。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。

优点:

  • 简单
  • 计算代价分散
  • “幽灵时间”短(幽灵时间指对象死亡到回收的这段时间,处于幽灵状态)

缺点:

  • 不全面(容易漏掉循环引用的对象)
  • 并发支持较弱
  • 占用额外内存空间(计数器消耗)

复制算法

将可用内存划分为两块,每次只是用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。

这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就行,实现简单,运行高效。

优点: 空间连续,没有内存碎片,运行效率高。

缺点: 每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。复制收集算法在对象存活率高的时候,效率有所下降, 所以复制算法主要用在新生代幸存者区中的from区和to区,因为新生代对象存活率低。

在这里插入图片描述

标记-清除算法

为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

优点:

  • 实现简单,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。
  • 此外,这个算法相比于引用计数法更全面,在指针操作上也没有太多的花销。更重要的是,这个算法并不移动对象的位置。

缺点:

  • 需要进行两次动作,标记获得的对象和清除死亡的对象,所以效率低。
  • 死亡的对象被GC后,内存不连续,会有内存碎片,GC的次数越多碎片越严重。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qRNSLoHO-1632746875784)(E:\笔记Typora\JVM\img\19.jpg)]

标记-压缩/整理算法

标记-整理法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段不是进行直接清理,而是令所有存活的对象向一端移动,然后直接清理掉这端边界以外的内存。

**优点:**不会像标记-清除算法那样产生大量的碎片空间。

缺点: 如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。

总结

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

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

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

GC:分代收集算法

年轻代特点存活率低,所以适合于复制算法;

老年代存活率高,适合于标记清除+标记压缩混合实现

JMM

1、什么是JMM?

Java内存模型(Java Memory Model)

2、它干嘛用的?

JMM是用来定义一个一致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则。

内存可见性

在Java中,不同线程拥有各自的私有工作内存,当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存变量副本中,当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值

在这里插入图片描述

上图右侧每个线程都有自己的工作区域,可以改变变量的值,所以就存在共享对象可见性不一致的问题,这时就可以使用关键字voliate,保证共享对象可见性的问题,只要右边的线程变量的值改变就会立即被刷新到主内存中。

指令重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,指令重排序使得代码在多线程执行时会出现一些问题。

其中最著名的案例便是在初始化单例时由于可见性重排序导致的错误。

单例模式案例一

public class Singleton {
    private static Singleton singleton;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

以上代码是经典的懒汉式单例实现,但在多线程的情况下,多个线程有可能会同时进入if (singleton == null) ,从而执行了多次singleton = new Singleton(),从而破坏单例。

单例模式案例二

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

以上代码在检测到singleton为null后,会在同步块中再次判断,可以保证同一时间只有一个线程可以初始化单例。但仍然存在问题,原因就是Java中singleton = new Singleton()语句并不是一个原子指令,而是由三步组成:

  1. 为对象分配内存
  2. 初始化对象
  3. 将对象的内存地址赋给引用

但是当经过指令重排序后,会变成:

  1. 为对象分配内存
  2. 将对象的内存地址赋给引用(会使得singleton != null)
  3. 初始化对象

所以就存在一种情况,当线程A已经将内存地址赋给引用时,但实例对象并没有完全初始化,同时线程B判断singleton已经不为null,就会导致B线程访问到未初始化的变量从而产生错误。

单例模式案例三

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

以上代码对singleton变量添加了volatile修饰,可以阻止局部指令重排序

那么为什么volatile可以保证变量的可见性和阻止指令重排序?

volatile

原理

  1. 规定线程每次修改变量副本后立刻同步到主内存中,用于保证其它线程可以看到自己对变量的修改
  2. 规定线程每次使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
  3. 为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障防止指令重排序

注意:

  1. volatile只能保证基本类型变量的内存可见性,对于引用类型,无法保证引用所指向的实际对象内部数据的内存可见性。关于引用变量类型详见:Java的数据类型
  2. volilate只能保证共享对象的可见性,不能保证原子性:假设两个线程同时在做x++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值