java实战--JVM终极总结

 关注微信公众号【BigData512】,了解更多大数据技术,还有免费资料等你哦 watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FBNTE4MTg5,size_16,color_FFFFFF,t_70

CPU和内存的交互

                     了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区。但是随着cpu的发展,内存的读写速度也远远赶不上cpu。因此cpu厂商在每颗cpu上加上高速缓存,用于缓解这种情况。现在cpu和内存的交互大致如下。

cpu、缓存、内存

cpu上加入了高速缓存这样做解决了处理器和内存的矛盾(一快一慢),但是引来的新的问题 - 缓存一致性

在多核cpu中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个 。

CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找,每个cpu有且只有一套自己的缓存。

如何保证多个处理器运算涉及到同一个内存区域时,多线程场景下会存在缓存一致性问题,那么运行时保证数据一致性?

为了解决这个问题,各个处理器需遵循一些协议保证一致性。【如MSI,MESI啥啥的协议。。】

多核CPU

                   多核CPU即多个CPU组成,这些CPU集成在一个芯片里,可以通过内部总线来交互数据,共享数据,这些CPU中分配出一个独立的核执行操作系统,每个核都有自己的寄存器,alu运算单元等(这些都是封装在cpu内部的),但是一级二级缓存是共享的,这些CPU通过总线来交互数据,并且工作是并行的,资源分配是由操作系统来完成的,操作系统来决定程序cpu的控制权分配,所以一个多核cpu的工作效率大多体现在操作系统的分配上,因为一个CPU基本上可以执行很多个程序,通过PCB进程控制块的方式存储当前代码段,然后来回跳转,所以当你的CPU核过多时,操作系统在分配时可能会导致部分CPU闲置!

大概如下

cpu与内存.png

在CPU层面,内存屏障提供了个充分必要条件

 内存屏障(Memory Barrier)

               CPU中,每个CPU又有多级缓存【上图统一定义为高速缓存】,一般分为L1,L2,L3,因为这些缓存的出现,提高了数据访问性能,避免每次都向内存索取,但是弊端也很明显,不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

  • 硬件层的内存屏障分为两种:Load BarrierStore Barrier即读屏障和写屏障。【内存屏障是硬件层的】

为什么需要内存屏障

由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存再不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题.
简单来说:
1.在不同CPU执行的不同线程对同一个变量的缓存值不同,为了解决这个问题。
2.用volatile可以解决上面的问题,不同硬件对内存屏障的实现方式不一样。java屏蔽掉这些差异,通过jvm生成内存屏障的指令。
对于读屏障:在指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。

内存屏障的作用

cpu执行指令可能是无序的,它有两个比较重要的作用
1.阻止屏障两侧指令重排序
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

volatile型变量

            当我们声明某个变量为volatile修饰时,这个变量就有了线程可见性,volatile通过在读写操作前后添加内存屏障。

用代码可以这么理解

//相当于读写时加锁,保证及时可见性,并发时不被随意修改。
public class SynchronizedInteger {
  private long value;
  public synchronized int get() {
    return value;
  }
  public synchronized void set(long value) {
    this.value = value;
  }
}

volatile型变量拥有如下特性

1.可见性,对于一个该变量的读,一定能看到读之前最后的写入。
2.原子性,对volatile变量的读写具有原子性,即单纯读和写的操作,都不会受到干扰。

Java内存区域

前提:本文讲的基本都是以Sun HotSpot虚拟机为基础的,Oracle收购了Sun后目前得到了两个【Sun的HotSpot和JRockit(以后可能合并这两个),还有一个是IBM的IBMJVM】

之所以扯了那么多计算机内存模型,是因为java内存模型的设定符合了计算机的规范。Java程序内存的分配是在JVM虚拟机内存分配机制下完成。java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

简要言之,jmm是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

从下面这张图可以看出来,Java数据区域分为五大数据区域。这些区域各有各的用途,创建及销毁时间。

其中方法区和堆是所有线程共享的,栈,本地方法栈和程序虚拟机则为线程私有的。

jvm五大区作用简介

  1. 计数器:保证线程执行切换的时候就可以在上次执行的基础上继续执行(每个线程独有计数器也是唯一一个没有OOM异常的区域。
  2. 方法区:方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。
  3. 用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
  4. 本地方法区:  本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务
  5. 堆:对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制,所有new出来的对象都存在堆中。
  6. 虚拟机栈(栈):每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程(对象的引用)

根据java虚拟机规范,java虚拟机管理的内存将分为下面五大区域。

五大内存区域

 程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

为什么需要程序计数器

             在多线程的情况下,为了让每个线程正常工作就提出了程序计数器(Programe Counter Register),每个线程都有自己的程序计数器这样当线程执行切换的时候就可以在上次执行的基础上继续执行,仅仅从一条线程线性执行的角度而言,代码是一条一条的往下执行的,这个时候就是程序计数器;JVM就是通过读取程序计数器的值来决定下一条需要执行的字节码指令,进而进行选择语句、循环、异常处理等;我们熟悉的分支操作、循环操作、跳转、异常处理和线程恢复等基础模型都需要依赖这个计数器来完成。

我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。

注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域

 Java栈(虚拟机栈)

          同计数器也为线程私有,生命周期与相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出,下图栈1先进最后出来】

栈帧: 是用来存储数据和部分过程结果的数据结构。
栈帧的位置:  内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里]
栈帧大小确定时间: 编译期确定,不受运行期数据影响。

通常有人将java内存区分为栈和堆,实际上java内存比这复杂,这么区分可能是因为我们最关注,与对象内存分配关系最密切的是这两个。

平时说的栈一般指局部变量表部分。

局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。

reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

returnAddress类型:指向一条字节码指令的地址【深入理解Java虚拟机】怎么理解returnAddress

栈帧

     需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常:

  1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  2. 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

本地方法区

         本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

           对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制,所有new出来的对象都存在堆中。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。

参考逃逸分析

注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代,再细致点还有Eden(伊甸园)空间之类的不做深究。

根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

 方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

运行时常量池是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。】

jdk1.7开始逐步去永久代。从String.interns()方法可以看出来
String.interns()
native方法:作用是如果字符串常量池已经包含一个等于这个String对象的字符串,则返回代表池中的这个字符串的String对象,在jdk1.6及以前常量池分配在永久代中。可通过 -XX:PermSize和-XX:MaxPermSize限制方法区大小。
public class StringIntern {
    //运行如下代码探究运行时常量池的位置
    public static void main(String[] args) throws Throwable {
        //用list保持着引用 防止full gc回收常量池
        List<String> list = new ArrayList<String>();
        int i = 0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}
//如果在jdk1.6环境下运行 同时限制方法区大小 将报OOM后面跟着PermGen space说明方法区OOM,即常量池在永久代
//如果是jdk1.7或1.8环境下运行 同时限制堆的大小  将报heap space 即常量池在堆中

字符串常量池(String Constant Pool):

字符串常量池在Java内存区域的哪个位置?

  • 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
  • 在JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。
  • JDK8以后也还是放在了Heap空间中,并没有已到元空间

字符串常量池是什么?

  • 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。
  • 在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
  • 在JDK7.0中,StringTable的长度可以通过参数指定:
-XX:StringTableSize=66666

字符串常量池里放的是什么?

  • 在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
  • 在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。关于String在内存中的存储和String#intern()方法的说明,可以参考我的另外一篇博客:

需要说明的是:字符串常量池中的字符串只存在一份! 
如:

String s1 = "hello,world!";
String s2 = "hello,world!";

即执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。(这里具体的字符串如何分配就不细说了,可以看我的另一篇博客)

2.class常量池(Class Constant Pool):

2.1:class常量池简介:

  • 我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
  • 每个class文件都有一个class常量池。

2.2:什么是字面量和符号引用:

  • 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
  • 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。

3.运行时常量池(Runtime Constant Pool):

  • 运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用
  • JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

idea设置相关内存大小设置

这边不用全局的方式,设置main方法的vm参数。

做相关设置,比如说这边设定堆大小。(-Xmx5m -Xms5m -XX:-UseGCOverheadLimit)

jdk8真正开始废弃永久代,而使用元空间(Metaspace)

java虚拟机对方法区比较宽松,除了跟堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾收集。

内存溢出

两种内存溢出异常[注意内存溢出是error级别的]
1.StackOverFlowError:当请求的栈深度大于虚拟机所允许的最大深度
2.OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间[一般都能设置扩大]

java -verbose:class -version 可以查看刚开始加载的类,可以发现这两个类并不是异常出现的时候才去加载,而是jvm启动的时候就已经加载。这么做的原因是在vm启动过程中我们把类加载起来,并创建几个没有堆栈的对象缓存起来,只需要设置下不同的提示信息即可,当需要抛出特定类型的OutOfMemoryError异常的时候,就直接拿出缓存里的这几个对象就可以了。

比如说OutOfMemoryError对象,jvm预留出4个对象【固定常量】,这就为什么最多出现4次有堆栈的OutOfMemoryError异常及大部分情况下都将看到没有堆栈的OutOfMemoryError对象的原因。

参考OutOfMemoryError解读

Snip20180904_8.png

两个基本的例子

public class MemErrorTest {
    public static void main(String[] args) {
        try {
            List<Object> list = new ArrayList<Object>();
            for(;;) {
                list.add(new Object()); //创建对象速度可能高于jvm回收速度
            }
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
        }
        try {
            hi();//递归造成StackOverflowError 这边因为每运行一个方法将创建一个栈帧,栈帧创建太多无法继续申请到内存扩展
        } catch (StackOverflowError e) {
            e.printStackTrace();
        }
    }
    public static void hi() {
        hi();
    }
}

Java类加载机制

1.概述

       Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。

      虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

2.工作机制

      类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过以下步骤:

     (1) 装载:查找和导入Class文件;

     (2) 链接:把类的二进制数据合并到JRE中;

        (a)校验:检查载入Class文件数据的正确性;

        (b)准备:给类的静态变量分配存储空间;

        (c)解析:将符号引用转成直接引用;

     (3) 初始化:对类的静态变量,静态代码块执行初始化操作

   java程序经过编译后形成*.class文件。通过类加载器将字节码(*.class)加载入JVM的内存中。JVM将类加载

类加载器的分类

Java类加载器采用双亲委派模型:

1.启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。

2.扩展类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。

3.应用程序类加载器:这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。

4.自定义加载器:用户自己定义的类加载器。

扫一扫加入大数据公众号,了解更多大数据技术,还有免费资料等你哦

扫一扫加入大数据公众号,了解更多大数据技术,还有免费资料等你哦

扫一扫加入大数据公众号,了解更多大数据技术,还有免费资料等你哦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿华田512

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值