JVM内存结构

1. Java编程语言简介

1.1 编程语言概述

  • 系统级和应用级
    • 系统级:C,C++,go,erlang
    • 应用级:C#,Java,Python,Perl,Ruby,php
      • 虚拟机:jvm(java虚拟机)、pvm(python虚拟机)
      • 动态网站:asp.net,jsp
  • 编程语言的类别:(程序=指令+数据)
    • 面向过程:以指令为中心,围绕指令组织数据
    • 面向对象:以数据为中心,围绕数据组织指令
  • 动态网站
    • 客户端动态
    • 服务器动态:CGI
  • webapp server
    • jsp:tomcat,jboss,jetty
    • php:php-fpm

1.2 Java编程语言

1.2.1 Java的特性

  • 面向对象(完全面向对象)、多线程、结构化错误处理
  • 垃圾收集、动态链接、动态扩展

1.2.2 JVM、JRE、JDK

  • JVM(Java Virtual Machine),Java虚拟机。
    • 它只能识别 .class类型的文件,能够将class文件中的字节码指令进行识别并调用操作系统上的API完成动作。
    • JVM是Java能够跨平台的核心。
  • JRE(Java Runtime Environment),Java运行时环境。
    • 它主要包含两个部分:jvm的标准实现、java的一些基本类库。
    • 它相对于jvm来说,多出来的是一部分的java类库。
  • JDK(Java Development Kit),Java开发工具包。
    • JDK是整个java开发的核心,它集成了JRE和一些好用的小工具,如javac、java、jar等。

  • 三者的嵌套关系:JDK > JRE > JVM

1.2.3 Java的执行过程

  • Java体系结构
    • Java编程语言
    • Java Class文件格式
    • Java API
    • Java VM
  • JVM的核心组成部分
    • Class Loader(类加载器)
    • 执行引擎

1.2.4 Java的三个技术流派

  • J2SE ==> Java 2 SE
  • J2EE ==> Java 2 EE
  • J2ME ==> Java 2 ME

1.3 Java 2 EE 平台

1.3.1 Servlet

  • Servlet是运行在web服务器上的程序,它是作为http客户端的请求和http服务器上数据库或应用程序之间的中间层,与CGI所实现的效果类似。
  • Servlet Container:将html标签硬编码在应用程序中,如println("<h1>")

1.3.2 JSP

  • JSP(Java Server Pages),是JavaWeb服务器端的动态资源,与html页面作用类似,显示数据和获取数据。
  • JSP=html+Java脚本(代码片段)+JSP动态标签
  • JSP图示:(JSP能自动将代码转换为Servlet格式)

1.3.3 Java Bean

Java Bean一般情况下指的是实体类,所有属性为private,提供默认构造方法和getter,setter,如果一个JavaBean需要在不同的JVM的进程中进行传递,还需要实现Serializable接口。

1.3.4 EJB

EJB(Enterprise JavaBean),企业级JavaBean,普通Java Bean 的区别是,JavaBean的使用可以不需要容器,EJB的运行一般需要EJB容器(即应用服务器,如JBoss/Weblogic/Websphere…

1.3.5 JMS

JMS(Java Message Service),Java消息服务接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

Java消息服务的规范包括两种消息模式,点对点和发布者/订阅者。

1.3.6 JMX

JMX(Java Management Extensions),是一个为应用程序植入管理功能的框架。

JMX是一套标准的代理和服务,用户可以在任何Java应用程序中使用这些代理和服务实现管理。

1.4 Web Container

1.4.1 包含三个容器

  • JDK
  • Servlet
  • JSP

1.4.2 实现

  • 商业实现:
    • WebSphere(IBM) 
    • WebLogic (BEA --> Oracle)
    • Oc4j
    • Glassfish
    • Geronimo
    • JOnAS
    • JBoss
  • 开源实现:
    • Tomcat
    • jetty
    • resin

2. JVM运行时区域简介

2.1 JVM运行时区域图示

  • 运行为多个线程,这些是为这些线程所使用的
  • OutOfMemoryError(OOM)内存溢出

2.2 Java内存结构

  • Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,每个区域都有各自的作用
  • 分析JVM内存结构,主要就是分析JVM运行时数据存储区域
  • JVM的运行时数据区主要包括:堆、栈、方法区、程序计数器
  • JVM的优化问题主要在线程共享的数据区中:堆、方法区

2.3 各区域简介

2.3.1 方法区(共享)

  • 线程共享的内存区域
  • 用于存储被虚拟加载的类信息、常量、静态变量等
  • 该区域也被称为:永久代(或者叫非堆内存区域)

2.3.2 堆(共享)

  • Java堆是jvm所管理的内存中最大的一部分,主要包含了java运行中所存放的对象
  • Java堆也是GC管理的主要区域,主流的算法都基于分代收集算法方式进行:
    • 新生代
      • Eden Space
      • Survivor Space 存活区
    • 老年代(Tenured Gen)
  • 堆内存是共享的,所有的java线程共享的内存区域
  • 堆中存放的是类实例化出来的对象

2.3.3 Java栈(线程私有)

  • 每一个线程都有自己的栈,线程启动起来就自动给它创建一个栈,这个虚拟机栈中描述的是java方法执行的内存模型
  • 每个方法被执行时,都会给它创建一个栈帧,用于存储线程自己的局部变量
  • Java栈内存为线程私有,存放线程自己的局部变量等信息

2.3.4 PC寄存器(Program Counter Register)(线程私有)

  • 程序计数器,线程独占的内存空间
  • 一般是非常小的内存空间

2.3.5 本地方法栈(线程私有)

  • Native Method Stacks
  • 它不是通过虚拟机栈为虚拟机执行java方法,而是为虚拟机所使用到的本地方法提供服务
    • 这里的本地方法对于Windows和Linux是不同的,它表明的是它真正能够在那个主机上所能执行的特有方法
    • 所以具体的实施方案是依赖于平台的

2.3.6 GC 垃圾回收器

  • GC垃圾回收面向于堆内存空间
  • 主要目的就是回收那些不用了的对象

2.4 运行时数据区域图示

3. 栈(JVM栈 & 本地方法栈)

  • JVM中的栈包括Java虚拟机栈本地方法栈
    • Java虚拟机栈为JVM执行Java方法服务
    • 本地方法栈则为JVM使用到的Native方法服务

3.1 本地方法栈

  • JDK中有很多方法是使用Native修饰的,Native方法不是以Java语言实现的,而是以本地语言实现的(比如C或C++)
  • Native方法是与操作系统直接交互的,比如通知垃圾收集器进行垃圾回收的代码System.gc(),就是使用native修饰的

3.2 JVM栈

3.2.1 栈为何物?

  • 对栈的定义:
    • 限定仅在表头进行插入和删除操作的线性表
  • 栈的特性:
    • 压栈(入栈)和弹栈(出栈)都是对栈顶元素进行操作的,所以栈是先进后出的
    • 栈是线程私有的,它的生命周期与线程相同,每个线程都会分配一个栈的空间,即每个线程有独立的栈空间

3.2.2 栈中存储的内容

  • 栈帧是栈的元素,每个方法在执行时都会创建一个栈帧
  • 栈帧中存储了局部变量表、操作数栈、动态链接方法出口等信息
  • 每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程

3.2.3 JVM栈中存储的内容详解

局部变量表

  • 栈帧中,由一个局部变量表存储数据
    • 局部变量表中存储了基本数据结构(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)
    • 和对象的引用(String、数组、对象等),但是不存储对象的内容
  • 局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小
  • 局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型
    • 对于64位的数据类型(long、double),JVM会为其分配两个连续的变量槽来存储
  • JVM通过索引定位的方式使用局部变量表,索引的范围从0开始至局部量表表中最大的Slot数量
    • 普通方法与static方法在第0个槽位的存储有所不同,非static方法的第0个槽位存储方法所属对象实例的引用

局部变量表的Slot复用:

1. 为了尽可能的节省栈帧空间,局部变量表中的Slot是可以复用的

  • 方法中定义的局部变量,其作用域不一定会覆盖整个方法
  • 当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的Slot就可以交给其他变量使用

2. Slot复用示例:

public void test(boolean flag)
{
    if(flag)
    {
        int a = 66;
    }
    int b = 55;
}

  • 过程分析:
    • 当虚拟机运行test方法时,就会创建一个栈帧,并压入到当前线程的栈中
    • 当运行到int a=66时,在当前栈帧的局部变量中创建一个Slot存储变量a,
    • 当运行到int b=55时,此时已经超出变量a的作用域了(变量a的作用域在{}所包含的代码块中)
      • 此时a就失效了,变量a占用的Slot就可以交给b来使用,这就是Slot复用
    • Slot复用虽然节省了栈帧空间,但是会伴随一些额外的副作用,Slot的复用会直接影响到系统的垃圾收集行为

3. Slot复用的原理分析:

写法一:

public class TestDemo {
    public static void main(String[] args){
        byte[] placeholder = new byte[64 * 1024 * 1024];
        System.gc();
    }
}

  • 分析:
    • 上述代码先向内存中填充了64M的数据,然后通知虚拟机进行垃圾回收
    • 虚拟机没有回收这64M内存,
    • 因为执行System.gc()方法时,变量placeholder还在作用域范围内,
    • 虚拟机是不会回收的,它还是有效的

写法二:

public class TestDemo {
    public static void main(String[] args){
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        System.gc();
    }
}

  • 分析:
    • 当运行到System.gc()方法时,变量placeholder的作用域已经失效了
      • 但是虚拟机还是没有回收placeholder变量占用的64M内存
    • 因为虽然限定了placeholder的作用域,但是之后并没有任何对局部变量表的读写操作
      • placeholder变量在局部变量表中占用的Slot没有被其他变量所复用
      • 所以作为GC Roots一部分的局部变量表仍然保持着对它的关联,所以placeholder变量没有被回收

写法三:

public class TestDemo {
    public static void main(String[] args){
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        int a = 0;
        System.gc();
    }
}

  • 分析:
    • 此时placeholder变量占用的64M内存空间被回收了
    • 运行到int a=0时,已经超过了placeholder变量的作用域,此时placeholder在局部变量表中占用的Slot可以交给其他变量使用
      • 而变量a正好复用了placeholder占用的Slot,至此局部变量表中的Slot已经没有placeholder的引用了
      • 虚拟机就回收了placeholder占用的64M内存空间

  • placeholder变量能否被回收的关键:
    • 局部变量表中的Slot是否还存有关于placeholder对象的引用

操作数栈:

  • 操作数栈是一个先进先出栈,操作数栈的元素可以是任意的Java数据类型
  • 方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作
  • 通常进行算术运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递
  • 操作数栈可以理解为栈帧中用于计算的临时数据存储区

栈中可能出现的异常:

  • StackOverflowError:栈溢出错误
    • 如果一个线程在计算时所需要用到栈大小  >  配置允许最大的栈大小,
    • 那么Java虚拟机将抛出StackOverflowError
  • OutOfMemoryError:内存不足
    • 栈进行动态扩展时如果无法申请到足够内存,会抛出OutOfMemoryError异常

设置栈参数:

  • 使用-Xss设置栈大小,通常几百k就够用了
  • 由于栈是线程私有的,线程数越多,占用栈空间越大
  • 栈决定了函数调用的深度,这也是慎用递归调用的原因
    • 递归调用时,每次调用方法都会创建栈帧并压栈
    • 当调用一定次数之后,所需栈的大小已经超过了虚拟机运行配置的最大栈参数,就会抛出StackOverflowError异常

4. Java堆(Java Heap)

4.1 Java堆概述

4.1.1 java堆简介

  • 堆是java虚拟机所管理的内存中最大的一块存储区域,堆内存被所有线程共享
  • 堆中主要存放的是使用new关键字创建的对象,所有对象实例以及数组都要在堆上分配
  • 垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)

4.1.2 java堆的空间

  • 年轻代(Young Generation)
    • 伊甸园(Eden区)
    • 幸存区(Survivor区)
      • From Survivor空间(S0,存活区1)
      • To Survivor空间(S1,存活区2)
  • 老年代(Old Generation)
  • 永久代(Permanent Generation)

4.2 Java堆的参数设置

4.2.1 Java堆内存图示

 

4.2.2 Java堆内存空间的调整参数

4.2.3 catalina.sh中的两个环境变量

  • CATALINA_OPTS:仅对启动运行tomcat实例的java虚拟机有效
  • JAVA_OPTS:对本机上所有java虚拟机都有效

定义示例:CATALINA_OPTS="-Xmx256m"

4.3 垃圾回收(GC)

4.3.1 年轻代和老年代的内存清理机制

  • 年轻代:mark(标记)---> compact(打包)---> 清除
    • 年轻代存储“新生对象”,新创建的对象存储在年轻代中
    • 当年轻代内存占满之后,会触发Minor GC,清理年轻代内存空间
  • 老年代
    • 老年代存储长期存活的对象和大对象
    • 年轻代中存储的对象,结果多次GC后仍然存活的对象会移动到老年代中存储
    • 老年代空间占满后,会触发Major GC(Full GC)
  • Full GC
    • Full GC是清理整个堆空间,包括年轻代和老年代
    • 如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常

4.3.2 Java中垃圾回收的过程

Eden区:

  • 当一个实例被创建了,首先会被存储在堆内存年轻代的Eden区中

Survivor区(S0和S1):

  • 作为年轻代GC(Minor GC)周期的一部分,存活的对象从Eden区被移动到Survivor区的S0中
    • 类似的,垃圾回收器会扫描S0,然后将存活的实例移动到S1中
  • 死亡的实例被标记为垃圾回收
    • 根据垃圾回收器选择的不同,要么被标记的实例都会不停的从内存中移除,要么回收过程会在一个单独的进程中完成

老年代(Old or tenured generation):

  • 老年代是内存中的第二块逻辑区
    • 当垃圾回收器执行Minor GC周期时,在S1 Survivor区中的存活实例将会被晋升到老年代,而未被引用的对象被标记为回收
  • 老年代GC(Major GC)
    • Major GC扫描老年代的垃圾回收过程
    • 相对于Java垃圾回收过程,老年代是实例生命周期的最后阶段
    • 如果实例不再被引用,那么它们会被标记为回收,否则它们会继续留在老年代中

内存碎片:

  • 一旦实例从堆内存中被删除,其位置就会变空并且可用于未来实例的分配
  • 这些空出的空间将会使整个内存区域碎片化,为了实例的快速分配,需要进行碎片整理
  • 基于垃圾回收器的不同选择,回收的内存区域要么被不停地被整理,要么在一个单独的GC进程中完成

4.3.3 Minor GC和Full GC的触发条件

  • Minor GC触发条件
    • 当Eden区满时,触发Minor GC
  • Full GC触发条件
    • 调用System.gc时,系统建议执行Full GC,但是不一定执行
    • 老年代空间不足
    • 方法区空间不足
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

4.3.4 JVM下发生的停顿现象

Stop The World:

  • Java中Stop-The-World机制简称STW
    • 是在执行垃圾收集算法时,Java应用程序的其他线程都被挂起(除了垃圾收集帮助器之外)
  • Java中一种全局暂停现象,全局停顿,
    • 所有Java代码停止,native代码可以执行,但不能与JVM交互,这些现象多半是由于gc引起

VM Thread(JVM里的特殊线程):

  • 专门用来执行一些特殊的VM Operation,比如分派GC,thread dump等
    • 这些任务,都需要整个Heap,以及所有线程的状态是静止的,一致的才能进行
    • 所以JVM引入了安全点(Safe Point)的概念,想办法在需要进行VM Operation时,通知所有的线程进入一个静止的安全点
  • 除了GC,其他触发安全点的VM Operation包括:
    1. JIT相关,比如Code deoptimization, Flushing code cache
    2. Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation)
    3. Biased lock revocation 取消偏向锁
    4. Various debug operation (e.g. thread dump or deadlock check)

4.3.5 于Tomcat而言

  • catalina.sh中有两个环境变量:
    • CATALINA_OPTS:仅对启动运行tomcat实例的java虚拟机有效
    • JAVA_OPTS:对本机上的所有java虚拟机有效

5. PC寄存器 & 方法区

5.1 程序计数器(PC寄存器)

  • 程序计数器(Porgram Counter Register)是一块较小的内存空间
    • 可以看做是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令
  • 线程的执行与程序计数器:
    • 一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一跳需要执行的字节码指令,从而确保线程的正确执行
    • 为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储
  • 程序计数器记录的内容:
    • 如果线程执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
    • 如果执行的是Native方法,计数器值为Undefined
  • 注意:
    • 程序计数器是线程私有的内存
    • 程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题

5.2 方法区

5.2.1 方法区(Method Area)

  • 方法区同Java堆一样是被线程共享的区域
    • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
  • 静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中
    • 该区域也被称为:永久代(或者叫非堆内存区域)
  • 常量池是方法区的一部分
  • 注意:
    • JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存
    • 元空间两个参数:
      • MetaSpaceSize:初始化元空间大小,控制发生GC阈值
      • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

5.2.2 常量池

  1. 常量池中存储编译器生成的各种字面量和符号引用
    • 字面量:字面量就是Java中常量的意思,比如文本字符串,final修饰的常量等
    • 符号引用:符号引用包括类和接口的全限定名,方法名和描述符,字段名和描述符等
  2. 常量池的作用
    • 优点:常量池避免了频繁的创建和销毁对象而影响系统性能,实现了对象的共享
  3. 基本数据类型比较的是数值,而引用数据类型比较的是内存地址

6. JVM性能监控工具

问题:

  • OutOfMemoryError:内存不足
  • 内存泄漏
  • 线程死锁
  • 锁竞争(Lock Contention)
  • Java消耗过多的CPU

6.1 jps

  • jps(java virtual machine process status tool)

作用:监控JVM进程状态信息,可以列出系统上已经启动的所有java应用进程状态

语法:jps [options] [hostid]

options参数:

  • -m   输出传入main方法的参数
  • -l     显示main类或jar文件的完全限定名称(完整类名或完整路径名称)
  • -v    显示为jvm虚拟机指定的参数
  • -q    输出类名称,JAR文件名和传递给main方法的参数的输出
  • -Joption 将选项传递给javac调用的Java启动器

hostid:目标系统的字符串,如果不写,则默认目标为本机JVM

  • 格式:[protocol:][[//]hostname][:port][/servername]

6.2 jstack

  • jstack(Stack Trace for Java)

作用:

  • 可以生成虚拟机当前时刻的线程快照
  • 一般称为threaddump或者javacore文件,即每一条线程正在执行的方法堆栈的集合
  • 查看某个java进程内的线程堆栈信息

语法:jstack [options] pid

options参数:

  • -l      long listings,输入完整的锁信息
  • -m    混合模式,会输出java堆栈和C/C++的堆栈信息
  • -F     当JVM不响应该请求时强制进行线程dump

6.3 jstat

作用:

  • jvm统计监测工具,能够收集并记录命令行选项指定的性能统计信息

语法:

  • jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
    • 其中<option>为必须提供的选项
    • 所有可用选项可使用jstat -options列出
  • jstat -gc 73618 查看gc信息
    [root@c7_node_05 ~]# jstat -gc 45937
     S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
    1856.0 1856.0 722.7   0.0   15168.0   3768.5   37640.0    23584.8   18688.0 17991.0 2048.0 1824.0    180    0.489  15      0.611    1.099

 其中各个字段的意义:

  • S0C,S1C,S0U,S1U:
    • S0:Survivor0(新生代的S0区域)
    • S1:Survivor1(新生代的S1区域)
    • C表示容量,U表示已用量
  • EC,EU:
    • Eden新生代区域的容量和已用量
  • OC,OU
    • 老年代区域的容量和已用量
  • PC,PU
    • 永久代区域的容量和已用量
  • YGC,YGT:
    • Young GC(新生代)的GC次数和耗时
  • FGC,FGCT:
    • FULL GC的次数和耗时
  • GCT:
    • 所有GC的总耗时

 6.4 jmap(JVM Memory Map)

  • 作用:
    • 查看堆内存使用细节情况
  • 语法:
    • jmap [options] pid
  • options参数:
    • heap 打印使用的GC算法、堆配置和按生成的堆使用情况,详细输出堆内存空间使用状态信息
    • histo 查看堆内存中的对象数目、大小统计结果等信息
    • permstat 打印类加载程序的Java堆永久生成的统计数据

6.5 jinfo

作用:

  • 为给定的Java进程或核心文件或远程调试服务器打印Java配置信息
  • 配置信息包括Java系统属性和Java虚拟机命令行标志

语法:

  • jinfo [ option ] pid
  • jinfo [ option ] executable core
  • jinfo [ option ] [server-id@]remote-hostname-or-IP
  • 参数说明:这些选项参数是互斥的,选项紧跟在命令名之后
    • pid 要为其打印配置信息的进程ID,jps命令输出的id
    • executable 能够产生堆内存的Java可执行文件
    • core 要为其打印配置信息的核心文件
    • remote-hostname-or-IP 远程调试服务器的主机名或IP地址
    • server-id 如果多个调试服务器在同一远程主机上运行,则为可选的唯一ID

option:

  • flags 以Key-Value的形式打印命令行标志
  • sysp rops 以Key-Value形式打印系统属性
  • h 打印帮助信息

6.6 两个GUI工具

  • jconsole
  • jvisualvm

 

 

posted @ 2020-05-14 19:27  Praywu  阅读(715)  评论(0编辑  收藏  举报