JVM精进之路

一、Java虚拟机结构

1、Java Virtual Machine:JVM是运行在操作系统之上,与硬件没有直接

2、JVM体系结构

 

 3、类装载器ClassLoader

  负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Exection Engine(执行引擎负责解释命令,提交操作系统执行)决定。

  一般:Java 运行代码时,JVM 的整个处理过程如下

  ①.JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。

  ②.JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。

  ③.class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值。

  ④. 完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构造器 <clinit> 方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 <clinit>() 方法。

   类加载过程:

   

3.1 Class Loader SubSystem

3.1.1 Load

 

 双亲委派:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

3.1.2 Link

3.1.3 Init初始化

标记为常量值的字段赋值,以及执行<clinit>方法的过程。JVM会通过加锁来确保类的<clinit>方法仅被执行一次。只有当初始化完成之后,类才正式成为可执行的状态。父类型的初始化逻辑优先于当前类的逻辑。

除被final所修饰的静态字段,剩下的直接赋值操作,以及所有静态代码块中的代码,会被Java编译器至于同一方法中,命名<clinit>。

类初始化被触发的情况:

  ①当虚拟机启动时,初始化用户指定的主类;

  ②当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;

  ③当遇到调用静态方法的指令时,初始化该静态方法所在的类;

  ④当遇到访问静态字段的指令时,初始化该静态字段所在的类;

  ⑤子类的初始化会触发父类的初始化;

  ⑥如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

  ⑦使用反射API对某个类进行反射调用时,初始化这个类;

  ⑧当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。

4、Native Interface本地接口

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

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

5、Native Method Stack

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

 6、PC寄存器

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

7、栈

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

  如:Exception: java.lang.StackOverflowError

8、方法区  

  方法区是线程共享的,通常用来保存装载的类的元结构信息。比如:运行时常量池+静态变量+常量+字段+方法字节码+在类/实例/接口初始化用到的特殊方法等。

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

9、Head堆(Java 7之前)

  一个JVM实例只存在一个堆内存,堆内存大小是可以调节的。(-Xms1024m -Xmx1024m -XX:+PrintGCDetails)

  类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。

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

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

11、永久代-->元空间

jdk8,永久代已被元空间取代,区别:永久代使用JVM堆内存,元空间不在虚拟机中而是使用本地物理内存。

因此,默认情况下,元空间大小受本地内存限制。类的元数据放入native memery,字符串池和类的静态变量放入java堆中。

12、作为GC Roots的对象

① 虚拟机栈(栈帧中局部变量表)中引用的对象。

② 方法区中类静态属性引用的对象

③ 方法区中常量引用的对象

④ 本地方法栈中JNI(Native方法)引用的对象

二、内存溢出

1、stackOverflowError(栈溢出)

线程私有:

  ①Program Counter Register(PC寄存器):不存在。

  ②Java Stack:对于不可动态扩展栈大小的线程来说,不断持续的往栈里面压入栈帧,最后就会发生stackOverflowError

  ③Native Method Stack:本地方法栈调用本地方法接口,进而调用本地方法库(C或C++编写),也会发生stackOverflowError

2、OutOfMemeryError

  2.1 直接内存(Direct Memory)——JVM内存模型外:

  java.lang.OutOfMemeryError:Direct buffer Memery:元空间(jdk8)并不在虚拟机中,而是使用本地内存 。NIO程序使用ByteBuffer来读取或者写入,基于通道(Channel)与缓存区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,避免了在Java堆和Native堆中来回复制数据。

  ByteBuffer.alloteDirect(capability):分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。如果不断分配本地内存,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemeryError,那程序就直接崩溃。

  2.2 JVM内存模型内

  java.lang.OutOfMemeryError:Java heap space:堆内存溢出,最常见。

  java.lang.OutOfMemeryError:PerGen space:(jdk8之前)JVM对永久代回收非常不积极,运行时大量动态类生成、类似Intern字符串缓存占用太多。

  java.lang.OutOfMemeryError:Metaspace:(jdk8)。

  java.lang.OutOfMemeryError:GC overhead limit exceeded:GC回收时间过长,就会抛出OOM,超过98%的时间用来做GC并且回收不了2%的堆内存,连续过次GC,都回收不了2%的情况才会发生。 

  java.lang.OutOfMemeryError:unable to create new native thread:高并发请求服务器,应用创建了太多线程了,一个应用进程创建多个线程,超过系统承载极限;Linux系统默认允许单个进程可以创建的线程时1024。

    解决办法:降低应用程序创建线程的数量,不过应用不需要这么多线程,改代码将线程数量降下来。否则,修改Linux默认1024个线程的限制,扩大值。

案例:软引用解决OOM

目前应用需要读取大量本地图片:如果每次读取图片都从硬盘读取会严重影像性能,如果一次性全部加载到内存中,又会造成OOM;

设计思路:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效避免了OOM的问题。 

  

Map<String,SoftReference<Bitmap>> imageCache = new HashMap<String,SoftReference<Bitmap>>();

 

三、JVM标配、X和XX参数

https://blog.csdn.net/longgeqiaojie304/article/details/93851827

以下具体参数来自《深入理解Java虚拟机-第二版》-附录C HotSpot虚拟机主要参数表(以jdk1.6为基础)

使用-XX:+PrintFlagsFinal参数可以输出所有参数的名称及默认值。

参数使用方式有如下3种:

  ① -XX:+<option>开启option参数

  ② -XX:-<option> 关闭option参数

  ③-XX:<option>=<value> 将option参数的值设置为value。

在idea种,在Terminal使用JDK工具查看是否配置JVM参数

       jps -l 表示查看java运行的进程号

       jinfo -flag PrintGCDetails 表示查看JVM是否配置PrintGCDetails参数

       -XX:-PrintGCDetails 中减号表示没有配置PrintGCDetails参数
  然后配置JVM参数。。。 -XX:+PrintGCDetails。。。重启。。。重复前面操作。。。发现。。。-XX:+PrintGCDetails 中加号表示配置了PrintGCDetails参数

  jinfo查看当前运行程序配置

         公式:jinfo -flag 配置项 进程号

         案例:查看JVM所有配置项(默认+人工配置)

         jinfo -flags 进程号

         Non-default VM flags表示JVM默认参数

         Command line表示人工配置参数

NO1 :内存管理参数

---采用①

UseSerialGC---Clinet模式的虚拟机默认开启,其他模式默认关闭---使用Serial + SerialOld的收集器组合进行内存回收

UseParNewGC---默认关闭--使用ParNew+Serial Old

UseConcMarksSweepGC--默认关闭--ParNew+CMS+Serial Old(CMS出现 Concurrent Mode Failure)后背收集器

UseParallelGC---默认开启---Parallel Scavenge + Serial Old (Jdk1.7 jdk1.8亦是,如果是jdk1.9 就是G1)

UseParalleOldGC---默认关闭--Parallel Scavenge + Parallel  Old

---采用③

SurvivorRatio---默认8---Eden/Survivor比值

MaxTenuringThreshold---默认15---晋升到到老年代对象年龄。每个对象坚持过一次Minor GC,年龄增加1,操作值就进入老年代

NO2:调优参数

---采用①

HeapDumpOnOutOfMemeryError---默认关闭---在发生内存溢出异常时是否生成堆转储快照,关闭则不生成

PrintGC---默认关闭---打印GC信息

PrintGCDetails---默认关闭---打印GC详细信息

PrintGCTimeStamps---默认关闭---打印GC停顿时间

四、JVM调优

11、MAT(Eclipse Memory Analyzer)

  Eclipse Markerplace中搜索安装Memory Analyzer。

  运行时:调出Run Configurations 在Arguments的VM arguments中设置-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError。。。。。。Apply 。。。Run

  刷新项目,出现java_pidxxxxx.hprof。。。。

 

原文地址:https://www.cnblogs.com/gzhcsu/p/12500054.html