深入理解Java虚拟机(三) Java虚拟机内存管理 OOM异常、SOE异常、类加载过程

简介

java虚拟机在执行java程序过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途;

在这里插入图片描述

程序计数器

  • 作用:程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能;

特点:

  • 线程私有:在多线程中,为了线程切换后能恢复到正确的执行位置,每条线程需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。

  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果是native方法,这个计数器值则为空。

  • 是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域;

Java虚拟机栈

  • 作用:每个方法被执行时都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方发出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。方法调用时分配栈帧内存,方法调用结束,栈帧内存会被回收;

特点:

  • 与程序计数器一样,栈也是线程私有的,它的生命周期与线程相同;
  • 局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型。
  • 所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,运行期间不会改变局部变量表的大小;
  • 当线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,当虚拟机动态扩展无法申请到足够空间时会抛出OutMemoryError异常;

Java堆
当新建一个对象时,对象是用的就是堆内存,当多个线程访问堆中的对象时,需要考虑线程安全问题;
数组用的也是堆内存;
java7之后,字符串池使用的也是堆内存(之前串池用的是方法区内存);
新生代有一个特殊部分 大小约是整个新生代内存的1% TLAB 每个线程可以在新生代中申请一块缓冲区(tlab)是此线程私有的,其他线程不能访问,在tlab内创建对象不需要加锁,用来提升对象创建的效率;

  • 作用:用来存放对象实例;

特点:

  • java虚拟机所管理的内存中最大的一块,被所有线程共享;
  • 是垃圾收集器管理的主要区域,所以也被称为GC堆
  • 可以出与物理上不连续的内存空间中;
    分类:
  • 从垃圾回收角度:新生代(伊甸园、幸存区2个)、老年代;
  • Eden空间、From Survivor空间和To Survivor空间;
  • 从内存分配角度可能划分出多个线程的私有的分配缓冲区;
  • 划分是为了更好的回收内存或者更快地分配内存;

方法区

  • 作用:用于存储已被虚拟机加载的类信息(类的方法、属性、构造代码)、常量、静态变量(从java7开始使用堆内存)、即时编译器(JIT)编译后的代码等数据,也叫NonHeap(非堆);

*.java -> *.class -> 解释/热点代码编译成机器码

特点:

  • 与java堆一样,是各个线程共享的内存区域;
  • 这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载;

本地方法栈
非java语言编写的方法;

运行时常量池

常量池:用于存放编译期生成的各种字面量和符号引用,将在类加载后存放到方法区的运行时常量池,一般来说,除了保存Class文件中描述的符号外,还会把翻译出来的直接引用也存储在运行时常量池;

特点:

  • 是方法区的一部分;
  • 相对于Class文件常量池具备动态性,运行期间也可能将新的常量放入池中;

非堆内存
jdk8开始,移除了永久代,换回元空间(meta space),分配不会受到Java对大小的限制;
例如:整个机器8g内存,jvm占了500m,其他程序占500m,剩下的7g内存都可以被元空间占用;

对象访问

对象访问过程:

Object obj=new Object();
  • Object obj将会反映到java栈的本地变量表中,作为一个reference(引用)类型数据出现;
  • new Object()将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存;内存长度不固定;
  • 在java堆中还必须包含能查找到此对象类型数据的地址信息,这些类型数据则存储在方法区中。

reference类型在java虚拟机规范里面只规定了一个指向对象的引用,不同虚拟机实现的对象访问方式会有不同,主流的有两种:使用句柄和直接指针;

  • 使用句柄:在java堆中就爱那个会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息;

在这里插入图片描述

  • 直接指针访问方式:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象的地址

在这里插入图片描述
两种方式各有优势。

OOM内存溢出问题的排查以及解决方案

排查

  1. 通过设置参数-XX:+HeapDumpOnOutMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析;

  2. 安装分析工具 在eclipse中可以安装Memory Analyzer 对dump出来的快照进行分析;在idea上可以安装JProfler进行分析处理;
    在这里插入图片描述

  3. 判断到底是内存溢出还是内存泄漏;

    • 如果是内存泄漏则进一步通过工具查看泄漏对象到GC Roots的引用链。就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收的。
    • 如果不存在泄露,那就是内存溢出,可以通过调整Vm参数将堆内存调大;

具体操作定位代码

SOE栈溢出异常

栈容量只由-Xss参数设定,在单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时候,都会抛出StackOverflowError异常;如果是多线程导致的内存溢出,再不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程;

类加载过程

加载
方法区(1.7之前叫永久代,从1.8开始叫元空间)

  • 将类的字节码信息加载到元空间中
  • 并且生成一个java.lang.Class对象
  • 如果这个类还有父类没加载,则先加载父类;如果在加载过程中,父类被删除,则发生NoClassDefFoundError错误,即没有类定义被找到。
    ClassNotFoundException 类没有被找到异常,发生在类加载阶段,加载类本身不存在;而NoClassDefFoundError是第一个类找到了,跟它关联的类没找到。

链接

  • 验证类是否符合JVM规范,安全性检查;保证字节码的正确性
  • 为static变量分配内存空间;
  • 解析符号引用,常量;

初始化

在类初始化之前,会有一个特殊的构造方法,线程安全的,只会执行一次,用来收集所有静态代码块,静态变量的赋值语句操作,按顺序收集。然后以线程安全的方式来执行这个特殊构造;

初始化触发的时机:

  • 首次访问这个类的静态变量或静态方法时;
  • 子类初始化,如果父类还没有还没有,会触发;
  • main方法所在的类,首先被初始化;

不会触发初始化

  • 访问类的static final 变量不会触发初始化,甚至不会触发类的链接;
  • 类对象.class不会触发初始化;
  • 创建该类的数组不会触发初始化;

类的双亲委派加载机制

  • 启动类加载器:负责加载jdk jre/lib中核心的类,例如:rt.jar
  • 扩展类加载器:负责加载jre/lib/ext 扩展目录中的类;
  • 应用程序类加载器(系统类加载器):加载classpath下的所有class类;

注意:从上到下 父亲关系

类的加载顺序:

在这里插入图片描述

  • 先找到应用程序类加载器,调用loadClass();
  • 找父亲类加载器即扩展类加载器,看是否类已经被加载;
  • 如果没找到,再委托它的父亲(扩展)再去找启动类(扩展类加载器的父亲)加载器;
  • 如果在再找到就返回,由自己去classPath中找;

如果没有这种委托机制,就有可能造成级别低的类加载器加载更通用的类,其他使用String的代码不能正常工作,像String这种比较通用的类应该由更高级别的类加载去加载;

猜你喜欢

转载自blog.csdn.net/mashaokang1314/article/details/86766663