码迷,mamicode.com
首页 > 其他好文 > 详细

JVM学习总结

时间:2021-04-19 15:40:17      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:程序   平台   高性能   监控软件   结束   识别   内存监控   nim   区别   

jvm探究(Java Virtual Machine)------java虚拟机模型

一、JVM的基本介绍

JVM 是 Java Virtual Machine 的缩写,它是一个java实现的虚拟计算机,一种规范。通过在实际的计算机上仿真模拟各类计算机功能实现···

好,其实抛开这么专业的句子不说,就知道JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作。 技术图片

1.Java文件是如何被运行的

比如我们现在写了一个 HelloWorld.java ,而我们的 JVM 是不认识文本文件的,所以它需要一个 编译 ,让其成为一个它会读二进制文件的 HelloWorld.class

① 类加载器

如果 JVM 想要执行这个 .class 文件,我们需要将其装进一个 类加载器 中,它就像一个搬运工一样,会把所有的 .class 文件全部搬进JVM里面来。 技术图片

② 方法区(Method Area)

方法区 是用于存放类似于元数据信息方面的数据的,比如类信息,常量,静态变量,编译后代码···等

类加载器将 .class 文件搬过来就是先丢到这一块上

③ 堆(Heap)

主要放了一些存储的数据,比如对象实例,数组···等,它和方法区都同属于 线程共享区域 。也就是说它们都是 线程不安全

jvm的调优就是在调线程共享区域。大部分调堆

④ 栈(Java栈Stack)

这是我们的代码运行空间。我们编写的每一个方法都会放到 里面运行。

我们会听说过 本地方法栈(Native Method Stack) 或者 本地方法接口 这两个名词,不过我们基本不会涉及这两块的内容,它俩底层是使用C来进行工作的,和Java没有太大的关系。

⑤ 程序计数器

主要就是完成一个加载工作,类似于一个指针一样的,指向下一行我们需要执行的代码。和栈一样,都是 线程独享 的,就是说每一个线程都会有自己对应的一块区域而不会存在并发和多线程的问题。

线程独享的这个区域是不会进行垃圾回收的 技术图片

技术图片

小总结

  1. Java文件经过编译后变成 .class 字节码文件
  2. 字节码文件通过类加载器被搬运到 JVM 虚拟机中
  3. 虚拟机主要的5大块:方法区,堆都为线程共享区域,有线程安全问题,栈和本地方法栈和计数器都是独享区域,不存在线程安全问题,而 JVM 的调优主要就是围绕堆,栈两大块进行

2.一个java文件运行举例

一个简单的学生类 技术图片

一个main方法 技术图片

执行main方法的步骤如下:

  1. 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载
  2. JVM 找到 App 的主程序入口,执行main方法
  3. 这个main中的第一条语句为 Student student = new Student("tellUrDream") ,就是让 JVM 创建一个Student对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中
  4. 加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有 指向方法区中的 Student 类的类型信息 的引用
  5. 执行student.sayName();时,JVM 根据 student 的引用(变量名在中)找到 student 对象(堆中),然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。
  6. 执行sayName()

其实也不用管太多,只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找。

二、类加载器的介绍

1.类加载器的作用

  • 作用:负责加载.class文件到内存中

它们在文件开头会有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容中静态数据结构转换成运行时数据区的方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,而是否能够运行则由 Execution Engine(执行引擎) 来决定

2.类加载器的流程

从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接

3.加载

  1. 通过“类全名”来获取定义此类的二进制字节流(文件)

  2. 将字节流class文件也就是这个类的信息加载到运行时数据区的方法区中(见上面的简单的代码例子)

  3. 将字节流(文件)中静态数据结构转化成运行时数据区的方法区中运行时的数据结构

  4. 在堆中生成一个代表这个类的 java.lang.Class对象,作为方法区这些数据的访问入口

    • 一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

    加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

4.连接

  1. 验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查
  2. 准备
    • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
      • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
      • 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。
  3. 解析
    • 虚拟机将常量池内的符号引用替换为直接引用的过程。也就是得到类或者字段、方法在内存中的指针或者偏移量。(符号引用比如我现在import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
      • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
      • 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能简介定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。(举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。)

5.初始化

  • 初始化其实就是执行类构造器的<clinit>()方法的过程,而且要保证执行前父类的<clinit>()方法执行完毕。这个方法由编译器收集,顺序执行所有类变量(static修饰的成员变量)显式初始化和静态代码块中语句。(例子:此时准备阶段时的那个 public static int value=111 由默认初始化的0变成了显式初始化的111. 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。)

    • 注意:类构造器方法<clinit>()不同于类的构造函数,这个方法是字节码文件中只能给JVM识别的特殊方法。
  • 对类进行初始化(只有主动去使用类才会初始化类):

    1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。

      • 当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。
      • 当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
      • 当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。
      • 当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。
    2. 使用 java.lang.reflect 包的方法对类进行反射调用时如Class.forname("..."), newInstance()等等。如果类没初始化,需要触发其初始化。

    3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。

    4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。

      = = = = = = = = = = = = == = = = = = = = = = = = 了解== = = = == = == = = == = = = = = = = =

    5. MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。

    6. 「补充,来自issue745 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

    = = = = = = = = = = = = == = = = = = = = = = = = 了解== = = = == = == = = == = = = = = = = =

6.卸载

  • GC将无用对象从内存中卸载

  • 卸载类即该类的Class对象被GC。

    卸载类需要满足3个要求:

    1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
    2. 该类没有在其他任何地方被引用
    3. 该类的类加载器的实例已被GC

    所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

    只要想通一点就好了,jdk自带的BootstrapClassLoader, ExtClassLoader, AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

7.类加载器的双亲委派模型

  • 类加载器对字节流class文件的加载需要靠classLoader

    • JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
      1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
      2. ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
      3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
  • 双亲委派模型

    • 每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型
      • 类加载器收到类加载请求,系统会首先判断当前类是否被加载过,系统会自底向上检查类是否被加载,已经被加载的类会直接返回,否则才会尝试加载。
      • 加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,自顶向下尝试加载类。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

技术图片

  • 双亲委派机制的好处
    • 双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。
  • 如果我们不想用双亲委派模型怎么办?
    • 可以使用自定义加载器
      • 自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法

三.jvm(java虚拟机)的内存分析

1.运行时数据区(java内存区域划分)----物理层面

  • Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。

    见图如下:

    技术图片

    技术图片

  • 线程私有的:

    • 程序计数器
    • 虚拟机栈
    • 本地方法栈

    线程共享的:

    • 方法区
    • 直接内存 (非运行时数据区的一部分)

2.程序计数器(PC寄存器:Program Counter Register)

  • 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器

  • 程序计数器的作用

    • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
    • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
  • 注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

3.本地方法栈(Native Method Stack)

关键在于native关键字

  • 本地方法栈(Native Method Stack)的作用是:登记带了native关键字的方法,在执行时,通过JNI(本地方法接口)加载本地方法库中的方法
    • 凡是带了native关键字的方法,说明java的作用范围达不到了,它会去调用底层C语言的库
    • 该带了native关键字的方法,会进入到本地方法栈,而本地方法栈就会去调用本地方法接口(JNI)
    • 本地方法接口(JNI)的作用是:扩展Jave的使用,融合不同的编程语言为Java所用,(最初是融合C、C++)
  • 例子:Java程序驱动打印机,管理系统等

(现在可以通过调用其他接口进行融合:比如Socket http~)

4.虚拟机栈(Stack)

  • 虚拟机栈和本地方法栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

  • 与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

    • Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。

      • Java 方法有两种返回方式:return 语句和抛出异常。

      • 不管哪种返回方式都会导致栈帧被弹出,线程结束,栈内存也就释放了(栈就为空了)

  • 栈和队列

    • 栈先进后出,后进先出
    • 队列先进先出(FIFO:First Input First Output)
  • 栈中可以存放:8大基本类型的变量+对象的引用变量(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置))+实例的方法(包括main方法---->>第一个入栈的)

  • Java 虚拟机栈会出现两种错误:StackOverFlowErrorOutOfMemoryError

    • StackOverFlowError 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

    通常是程序中存在死循环或者深度递归调用造成的

    • OutOfMemoryError Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常异常。

    通常是栈大小设置太小

  • 解决方法:

    • 可以通过虚拟机参数 -Xss 来设置每个线程栈的大小。

5.方法区(非堆)

  • 方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(构造函数,接口定义)、常量(final)、静态变量(static)、运行时的常量池

    • 运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。

      • 这个错误,一般出现于大量Class 或者 jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。另外,过多的常量,尤其是字符串,也会导致方法区溢出
      1. JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
      2. JDK1.7 字符串常量池被从方法区拿到了堆中, 运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
      3. JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
    • 理解运行时常量池和字符串常量池:

      • 运行时常量池是在类加载器的加载步骤完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类加载器在连接步骤的解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

      • 字符串池里的内容是在类加载器的加载步骤完成之后,经过类加载器的连接步骤中的验证,准备阶段之后,如果有创建一个对象实例中包含了字符串,那么就会在中生成该对象实例和生成字符串对象实例(字符串也是个对象),然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)

        public class Test(){
            private int a;
           private Sting name="lyj"; 
        }
        public static void main(String[] args){
            Test test1=new Test();
        }
        //此处创建的实例是new Test(),因为该对象中包含了字符串,所以又要在堆中创建一个字符串对象实例,而字符串对象实例的引用值“lyj”会存放到字符串常量池中
        
    • 理解方法区和永久代的关系:

      • 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot (其中一种JVM)虚拟机对虚拟机规范中方法区的一种实现方式。
      • 永久代这个区域是常驻内存的,用来存放JDK自身携带的Class对象,接口数据,存储的是Java运行时的一些环境信息,永久代这个区域是不存在垃圾回收的
    • hotspot移除了永久代 (PermGen) 用元空间(Metaspace)取而代之的原因:

      • 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,可以进行调整,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

      当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace

      -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。

      -XX:MetaspaceSize 标志定义元空间的初始大小,如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

      • 元空间(元空间不在虚拟机中,在本地内存)里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
  • 注意:对象的实例变量存放在堆内存中,字符串常量池在JDK1.7后也放入了堆内存中,和方法区无关

6.堆

  • Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。一个JVM只有一个堆内存,堆内存大小是可以调节的,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

6.1、堆中的垃圾回收流程:

  • Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间(伊甸园)、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

    技术图片

      • 上图所示的 eden区、s0区(survive幸存0区)、s1区(survive幸存1区)都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,当Eden空间满了之后,会触发一个叫做Minor GC(就是一个发生在年轻代的GC)的操作,存活下来的对象移动到Survivor0区(from指针),并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1)。

        这时form指针指向的survivor0区的 里面有对象,这时to指针指向的survivor1区的 里面没有有对象

      • 继续存放对象,也是首先在Eden区(伊甸园)存放,当Eden满之后,又触发Minor GC,原先form指针指向的survivor0区存活的对象会被复制到to指针指向的survivor1区,Eden区存活的对象也会放入to指针指向的survivor1区,然后form to指针进行互换

        这时form指针指向的survivor1区的 里面有对象,这时to指针指向的survivor0区的 里面没有有对象

      • 如果from指针指向的Survivor区满后也触发 Minor GC,就会将存活对象移动到to指针指向的Survivor区,此时还会把from和to两个指针交换,这样保证了一段时间内总有一个survivor区为空且to所指向的survivor区为空。

    此处包含 动态对象年龄判定 的知识点

    • 当survive区满,触发Minor GC(进行垃圾回收),存活下来的对象它的年龄会增加,当增加到一定程度(默认为15岁,因为年龄标志位只有4位,最多为15),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

      或者

    • Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”,并且该年龄段开始及大于的年龄对象就要进入老年代。

      或者

    • survive区中Minor GC 一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。

    经过多次的 Minor GC后仍然存活的对象,就会移动到老年代

    • 注意1:大对象直接进入老年代,因为大对象需要大量连续内存空间的对象(比如:字符串、数组)。这样做可以避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
    • 注意2:要存入一个名为allocation1的对象,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。

    技术图片

    老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。

    而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xmx来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。

  • 当老年区执行了full gc之后仍然无法进行对象保存的操作,就出现了堆中最容易出现的 OutOfMemoryError 错误,这就是虚拟机中的堆内存不足,并且出现这种错误之后的表现形式还会有几种,比如:

    1. OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
    2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发该错误。(和本机物理内存无关,和你配置的内存大小有关)

6.2、解决虚拟机--堆中出现的OOM错误

  • 可能会是堆内存设置的大小过小,这个可以通过虚拟机参数-Xms(初始堆大小)、-Xmx(最大堆大小)来尝试扩大堆内存进行调整
  • 使用内存监控软件(内存快照分析工具MAT\Jprofiler)查找程序中的错误代码(未看)
    • MAT\Jprofiler的作用
      • 分析Dump内存文件,快速定位内存泄露
      • 获得堆中的数据
      • 获得大的对象

7.直接内存 (非运行时数据区的一部分)

  • 直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

    • 因为:本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
  • JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据

四.GC:垃圾回收

  • 在前面的三.6堆中提到了:目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。(见四.4.4)

1.垃圾回收的区域

  • 程序计数器、虚拟机栈、本地方法栈,3个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。而Java堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的。因此垃圾收集器所关注的都是堆和方法这部分内存。

2.主要的GC(垃圾回收)的分类

  • 部分收集 (Partial GC):

    • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
    • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
    • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
  • 整堆收集 (Full GC):收集整个 Java 堆和方法区。

3.垃圾回收前判断哪些对象还存活

  • 2.1)引用计数法(引用计数器)

    • 给对象添加一个引用计数器,每次引用这个对象时计数器加一,引用失效时减一,计数器等于0时就是不会再次使用的。
    • 弊端:无法解决循环引用的问题
  • 2.2)可达性分析算法

    • 这是一种类似于二叉树的实现,将一系列的GC ROOTS作为起始的存活对象集,从这个节点往下搜索,搜索所走过的路径成为引用链,把能被该集合引用到的对象加入到集合中。搜索当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。

      • 引用类型:强引用,软引用,弱引用,虚引用(引用强度逐渐减弱)

      • 强引用:垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

      • 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。

        • 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
        • 但是具有软引用的对象,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
      • 虚引用:任何时候都可能被垃圾回收。

        虚引用主要用来跟踪对象被垃圾回收的活动

    • 优点:可以解决循环引用的问题

    • 缺点:它的实现需要耗费大量资源和时间,同时它的分析过程引用关系不能发生变化,所以需要停止所有进程

4.垃圾回收算法

4.1标记-清除算法

  • 该算法分为“标记”和“清除”阶段:

    • 首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
    1. 效率问题:要进行两次扫描,浪费时间
    2. 空间问题(标记清除后会产生大量不连续的碎片)

4.2标记-压缩(整理)算法

  • 解决标记-清除算法中的空间出现碎片问题

    • 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

      技术图片

    • 缺点:还是存在效率问题(多次扫描)

4.3标记-复制算法

  • 为了解决效率问题,复制算法就出现了。它将可用内存按容量划分成两等分,每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了,就把存活的对象copy到另一块toPlace上,然后交换指针的内容。这样就解决了碎片的问题。

    技术图片

    • 缺点:
      • 浪费了(一半)内存空间,堆内存的使用效率就会变得十分低下
      • 在极端情况下,对象的存活率高,复制存活的对象到另外一个区域,也会造成效率低

4.4每种算法的比较

技术图片

4.5分代回收算法

  • 把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
    • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,(存活率低且内存利用率不高)那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
    • 而老年代中因为对象存活率高,没有额外空间对它进行分配担保,(内存利用率高)就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

4.6了解一些垃圾回收器

HotSpot VM中的垃圾回收器,以及适用场景 [技术图片

  • Parallel 和 ParNew 收集器类似是多线程的,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。

  • CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

    • 特点:并发收集、低停顿

    • 缺点:CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片

  • 到jdk8为止,默认的垃圾收集器是Parallel(并行) Scavenge 和 Parallel Old

  • 从jdk9开始,G1整堆回收器)成为默认的垃圾收集器 目前来看,G1回收器停顿时间最短而且没有明显缺点,非常适合Web应用。

    • 特点:
      • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
      • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
      • 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
      • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
  • = == = = = == = = = = = = = == = = = == = == = = = = == = = = = = = = =

    并行:真正的“同时”运行,在同一时刻,有多个任务同时执行。(例如,在多核处理器上,有两个线程同时执行同一段代码。

    并发:两个或多个任务可以在重叠的时间段内启动,运行和完成

    并行(多个线程同时执行) 一定是并发,并不一定意味着并发一定要求是并行(包含关系)

  • = == = = = == = = = = = = = == = = = == = == = = = = == = = = = = = = =

五.JVM调优

对JVM进行调优,主要针对堆内存

1.jvm调优工具

2.jvm调优参数

2.1 调整最大堆内存和最小堆内存

-Xmx –Xms:指定java堆最大值(默认值是物理内存的1/4(<1GB))和初始java堆最小值(默认值是物理内存的1/64(<1GB))

默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。简单点来说,你不停地往堆内存里面丢数据,等它剩余大小小于40%了,JVM就会动态申请内存空间不过会小于-Xmx,如果剩余大小大于70%,又会动态缩小不过不会小于–Xms。就这么简单

2.2调整新生代和老年代的比值

-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;

2.3 调整Survivor区和Eden区的比值

-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;

// -Xmn5120m:代表新生代
//-XXSurvivorRatio=3:代表Eden:Survivor = 3    
根据Generation-Collection算法(目前大部分JVM采用的算法),一般根据对象的生存周期将堆内存分为若干不同的区域,一般情况将新生代分为Eden ,两块Survivor;   
    //计算Survivor大小, Eden:Survivor = 3,总大小为5120,3x+x+x=5120  x=1024

根据实际事情调整新生代、幸存代的大小,官方推荐新生代占java堆的3/8,幸存代占新生代的1/10

六.理解String类和常量池

1.String 对象的两种创建方式:

String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中创建一个新的对象
String str3 = new String("abcd");//堆中创建一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false

这两种不同的创建方法是有差别的。

  • 第一种方式是在常量池中拿对象;
  • 第二种方式是直接在堆内存空间创建一个新的对象。

记住一点:只要使用 new 方法,便需要创建新的对象

2.String s1 = new String("abc");这句话创建了几个字符串对象?

将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果字符串常量池中没有字符串常量“abc”,那么它将首先在字符串常量池中创建一个“abc”对象,然后在堆空间中再创建一个String类的对象--字符串常量“abc”,因此将创建总共 2 个字符串对象。

验证:

		String s1 = new String("abc");// 堆内存的地址值
		String s2 = "abc";
		System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。

3.字符串的拼接

  • 字符串连接是通过StringBuilder (或StringBuffer )类及其append方法实现的。
    • StringBuffer:使用append实现
      StringBuilder:使用append实现。
  • String字符串拼接通过StringBuilder走中间过程,通过append方法实现。

4.String和StringBuffer和StringBuilder的区别

  • String是值不可变类,每次在String对象上的操作都会生成一个新的对象;
  • StringBuffer和StringBuilder则允许在原来对象上进行操作,而不用每次增加对象;StringBuffer是线程安全的,但效率较低,而StringBuilder效率最高,但非线程安全。

5.使用equals和‘==‘进行字符串比较的差异?

  • 对于基本类型来说,== 比较的是值是否相等;

  • 对于引用类型来说,== 比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方);

  • 对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String,本身的equals方法就被重写了),则比较的是地址里的内容。

    • 字串中equals是重写的一个方法,比较字符串中value字符数组中字符是否一致,即比较的是字符串的值,==不仅比较字符串的值,而且还比较两个字符串所在内存地址是否相同。

6.String类中intern()方法

作用:如果字符串常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。

  String s1 = new String("计算机");
	      String s2 = s1.intern();
	      String s3 = "计算机";
	      System.out.println(s2);//计算机
	      System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象,一个是常量池中的 String 对象
	      System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象

JMM(Java Memory Model)----java内存模型

一、定义java内存模型的原因

  • 为了解决线程之间的信息共享,数据一致性(完成底层封装)

  • 为了屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果,Java虚拟机规范中定义了Java内存模型。

二、JMM(java内存模型)和JVM的内存模型的区别

  • JVM静态存储模型就是jvm内存模型:它只是对内存的物理划分而已,它只局限于内存,而且只局限于jvm内存。

三、CPU缓存模型(计算机内存的缓存)

技术图片

  • 处理器(cpu): 寄存器:每个cpu都包含一系列寄存器,他们是cpu的基础,寄存器执行的速度,远大于在主存上执行的速度
  • cpu高速缓存:由于处理器与内存访问速度差距非常大,所以添加了读写速度尽可能接近处理器的高速缓存,来作为内存与处理器之间的缓冲,将数据读到缓存中,让运算快速进行,当运算结束,再从缓存同步到主存中,就无须等待缓慢的内存读写了。CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题
  • 内存:一个计算机包含一个主存,所有cpu都可以访问主存,主存通常远大于cpu中的缓存
  • 运作原理: 通常,当一个cpu需要读取主存时,他会将主存的内容读取到缓存中,将缓存中的内容读取到内部寄存器中,在寄存器中执行操作,当cpu需要将结果回写到主存中时,他会将内部寄存器的值刷新到缓存中,然后会在某个时间点将值刷新回主存
    原文链接:https://blog.csdn.net/wangnanwlw/article/details/86466782

四、java内存模型

  • java内存模型(Java Memory Model, JMM)是一种规范。它规范了java虚拟机与计算机内存是如何协同工作的。它规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值。以及在必须时如何同步的访问共享变量 。

    线程之间通信必须要通过主内存(主内存其实是jvm内存模型的堆内存)

  • java内存模型一般指的就是下图。

技术图片

  • 线程之间的共享变量存储在主内存里面,每个线程都有一个私有的本地内存(又称工作内存
  • 本地内存是java内存模型的抽象概念,它并不是真实存在的。本地内存存储了该线程以读或写共享变量拷贝的副本。(比如线程A 要使用主内存中的变量a,线程A会先拷贝出变量a 的副本存储在自己的本地内存中),它涵盖了缓存,写缓存区,寄存器以及其他硬件和编译器的优化。
  • 线程对变量的操作都在自己的本地(工作)内存中,操作完成后再将变量更新至主内存;其他线程再通过主内存来获取更新后的变量信息,即线程之间的交流通过主内存来传递

五、java内存模型结合计算机缓存模型的底层分析

  • 从更低的层次来说主内存就是计算机硬件系统的内存,上面CPU缓存模型的内存

  • 为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。Java内存模型中的线程的工作内存是cpu的寄存器和高速缓存的抽象描述。

  • 而JVM静态存储模型就是jvm内存模型它只是对内存的物理划分而已,它只局限于内存,而且只局限于jvm内存。

  • 线程之间通信必须要通过主内存(主内存其实是堆内存)。

JVM学习总结

标签:程序   平台   高性能   监控软件   结束   识别   内存监控   nim   区别   

原文地址:https://www.cnblogs.com/lyj-study/p/14669888.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!