JVM 指南

JVM

概述

JVM的位置

JVM运行在操作系统之上, 和硬件无直接的交互。

JVM体系结构图

  上图我们清晰的看到的JVM的体系结构图, 针对不同的区域下面会进行详细的讲解。这里需要提到的是, 我们常说的JVM优化, 通常就是优化Heap和Method Area,优化的所有线程共享的数据。

三种JVM

JVM是有一套规范的

Sun公司的HotSpot

  提起HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。但不一定所有人都知道的是,这个目前看起来“血统纯正”的虚拟机在最初并非由Sun公司开发,而是由一家名为“Longview Technologies”的小公司设计的;甚至这个虚拟机最初并非是为Java语言而开发的,它来源于Strongtalk VM,
  而这款虚拟机中相当多的技术又是来源于一款支持Self语言实现“达到C语言50%以上的执行效率”的目标而设计的虚拟机,Sun公司注意到了这款虚拟机在JIT编译上有许多优秀的理念和实际效果,在1997年收购了Longview Technologies公司,从而获得了HotSpot VM

BEA公司的JRockit

上面这两个公司被Oracle公司收购合并之后推出了震惊业界的Java8

IBM公司的J9 VM

ClassLoader(类装载器)

什么的类装载器

  我们一句话简单的解释: 我们都知道使用java c 命令编译之后会得到一个后缀名是.class的字节码文件, 然后使用java命令可以执行这个字节码文件中的内容,那么JVM需要执行这个字节码文件的前提是什么呢?,是不是首选要将.class加载进JVM呢?

这里就引出了类装载器的概念和作用了,类似一个快递员的职能。

Execution Engine执行引擎负责解释命令,提交操作系统执行。

虚拟机的类加载机制

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

类加载器的种类

虚拟机自带的类装载器

启动类加载器(Bootstrap)C++
这个类加载器负责将存放在$JAVA_HOME/lib目录中, 或者被 -Xbootclasspath参数所指定的路径中的,
并且是虚拟机识别的(仅按照文件名识别, 如rt.jar名字不符合的类库,即使放在lib目录中也是不会被加
载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定类加载器时,如果
需要把加载请求委派给引导类加载器,那么直接使用null代替即可。
扩展类加载器(Extension)Java
扩展类加载器(Extension ClassLoader): 这个加载器由sun.misc.Launcher$ExtClassLoader实现,它
负责加载$JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所制定的路径中的所有类库,开
发者可以直接使用扩展类加载器
应用程序类加载器(App)Java
应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader
实现, 由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值, 所以一般也称为
系统类加载器。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,
如果应用程序中没有自定义过类加载器,一般情况这个就是程序中默认的类加载器

用户自定义的类装载器

用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式

类加载器的层次关系

Java代码的体现

public class ClassLoaderTest {

    public static void main(String[] args) {

        Object obj = new Object();
        Inner inner = new ClassLoaderTest().new Inner();

        System.out.println(obj.getClass().getClassLoader());
        System.out.println(inner.getClass().getClassLoader().getParent().getParent()); // null
        System.out.println(inner.getClass().getClassLoader().getParent());  // ExtClassLoader
        System.out.println(inner.getClass().getClassLoader());              // AppClassLoader
  }

    class Inner {

    }
}

双亲委派模型

当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

native

Native Method Interface (本地方法接口)

  Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

  本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合 C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为Native的代码,它的具体做法是Native Method Stack中登记Native方法,在Execution Engine 执行时加载Native libraries。

   目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等等,不多做介绍。

Native Method Stack (本地方法栈)

  它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。

Java代码的体现

使用java.lang.Thread.start()方法代码片段为例

PC寄存器

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

Method Area (方法区)

1:方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也定义在此, 简单说, 所有定义的方法的信息都保存在该区域, 此区域属于共享区间。

比如:运行时常量池+静态变量+常量+字段+方法字节码+在类/实例/接口初始化用到的特殊方法等。

2:通常和永久区关联在一起(Java7之前),但具体的跟JVM的实现和版本有关。

Tips: 实例变量存在堆内存中, 和方法无关。

Java Stack (栈)

Stack是什么?

​ 栈也叫栈内存, 主管Java程序的运行, 是在线程创建时创建, 它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题, 只要线程一结束栈就Over, 生命周期和线程一致,是线程私有的。 8中基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的栈内存中分配。

栈帧中主要保存3类数据:

本地变量(Local Vriables): 输入参数和输出参数以及方法内的变量。

栈操作(Operand Stack) : 记录出栈、入栈的操作。

栈帧数据(Frame Data): 包括类文件、方法等等。


经典异常 : Exception in thread "main" java.lang.StackOverflowError

栈 + 堆 + 方法区的交互关系

relation

Heap (堆)

  一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真是信息,以方法执行器执行,堆内存分为三部分:

堆内存逻辑上分为三部分: 新生 + 养老 + 永久

堆内存物理上分为三部分: Young + Old

name desc
Young Generation Space 新生区 Young/New
Tenure Generation Space 养老区 Old/Tenure
Permanent Space 永久区 Perm

Heap堆(Java7之前)

一个JVM

实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行

新生区的描述

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区.若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

养老区

Minor GC会把Eden中的所有活的对象都移到Survivor区域中, 如果Survivor区中放不下,那么剩下的活的对象就被迁移到Old Generation中,也即收集后,Eden是就变成了空的了

永久区

 永久存储区是一个常驻内存区域, 用于存放JDK自身携带的Class, Interface的元数据, 也就是说它存储的是运行环境必须的类信息被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭了JVM才会释放此区域所占用的内存

如果出现java.lang.OutOfMemoryError: PermGen space,说吗是Java虚拟机对永久代Perm内存设置不够, 一般出现这种情况都是程序启动需要加载大量的第三方jar包,例如“在一个Tomcat下部署了太多的应用, 或者大量动态反射生成的类不断被加载,最终导致Prem区被占满

version desc
JDK1.6及之前 有永久代,常量池1.6在方法区
JDK1.7 有永久代, 但已经逐步“去永久代”,常量池1.7在堆
JDK1.8及之后 无永久代,常量池1.8在元空间

实际而言,方法区(Method Area)和堆一样, 是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息 + 普通常量 + 静态敞亮 + 编译器编译后的代码等等, 虽然JVM规范将方法区描述为堆的一个逻辑部分,但他却还有一个别名叫做Non—Heap(非堆), 目的就是要和堆分开


对于HotSpot虚拟机,很很多开发者习惯将方法区称之为"永久代"(Parmanent Gen),但是严格本质上说两者不同, 或者说使用永久代来实现方法区而已(相当于一个接口interface)的一个实现JDK1.7中,已经将原来放在永久代的字符常量池移走。


常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本,字段,方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载户进入方法区的运行时常量池中存方法

## 堆内存调优

参数

-Xms: 起始使用的内存 (默认是物理内存的1/64)

-Xmx: 最大使用的内存 (默认是物理内存的1/4)

-Xms2048m -Xmx2048m -XX:+PrintGCDetails
public static void main(String[] args) {

    long maxMemory = Runtime.getRuntime().maxMemory();//返回 Java 虚拟机试图使用的最大内存量。
    long totalMemory = Runtime.getRuntime().totalMemory();//返回 Java 虚拟机中的内存总量。
    System.out.println("MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double) 1024 / 1024) + "MB");
    System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double) 1024 / 1024) + "MB");

}

运行结果:

笔者证明: 前面的概念全部都是来自官方, 证据

MAT插件(eclipse)

MAT是eclipse中一个内存分析器插件

演示类:

public class JVM01 {

    public static void main(String[] args) {

        ArrayList<JVM01> list = new ArrayList<JVM01>();

        try {

            while (true) {

                list.add(new JVM01());
            }
        } catch (Exception e) {
            System.out.println(".................show");
            e.printStackTrace();
        }
    }
}

在运行main()时,添加如下的VM参数

-Xms8m -Xmx8m   -XX:+HeapDumpOnOutOfMemoryError           OOM时导出堆到hprof文件。

效果图:

MAT内存分析日志

猜你喜欢

转载自www.cnblogs.com/chenyichen/p/10561462.html
JVM
今日推荐