JVM笔记-java虚拟机

JVM

常见问题

什么情况下会发生栈内存溢出
谈谈你对jvm的理解?Java8的虚拟机有什么更新?
什么是ooM?什么是stackoverflowerror?
jvm的常用参数调优你知道哪些?
谈谈jvm中,对类加载器的认识?
你知道哪几种垃圾收集器,各自的优缺点
详解JVM内存模型
JVM中一次完整的GC流程是怎样的
强引用、软引用、弱引用、虚引用的区别?

走进Java


概述

java广泛应用于嵌入式系统、移动终端、企业服务器、大型机等各种场合,摆脱了硬件平台的束缚,实现了“一次编写,到处运行”的理想

java技术体系结构

按照功能来划分
  1. 包括以下几个组成部分:Java程序设计语言,各种硬件平台的java虚拟机,Java API类库,来自商业机构和开源社区的第三方Java类库,Class文件格式
  2. Java程序设计语言,java虚拟机,Java API类库统称为JDK,是用于支持java程序开发的最小环境
  3. Java API类库中的Java SE API子集和Java虚拟机统称为JRE,是支持java程序运行的基本环境
按照技术所服务的领域划分分为4个平台
  1. Java Card:支持java小程序运行在java小内存设备(如智能卡)上的平台

  2. Java ME:支持Java程序运行在移动设备上的平台

  3. Java SE:支持面向桌面级应用的平台

  4. Java EE:支持使用多层架构的企业级应用的平台

自动内存管理机制


运行时数据区域


java虚拟机所管理的内存将会包括以下几个运行都时数据区域:

在这里插入图片描述

程序计数器(Program Counter Register)

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成

java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令.

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,个条线程之间的计数器互不影响,独立存储,这类内存区域为"线程私有"的内存.

如果执行的是一个java方法,计数器记录的是正在执行的虚拟机字节码指令的地址

如果执行的是一个native方法,计数器值为空(undefined).此内存区域是唯一一个在java虚拟机规范中没有规定任何outofmemoryerror情况的区域

java虚拟机栈(Java Vitual Machine Stacks)

java虚拟机栈也是线程私有的,它的生命周期与线程相同.

每个方法执行时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每个方法从调用到直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

粗糙划分:java内存区分为堆内存(Heap)和栈内存(Stack),与对象内存分配关系紧密,这里的栈指的就是虚拟机栈或者说是虚拟机栈中的局部变量表

局部变量表存放了编译器可知的各种基本数据类型,对象引用(可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用一个,局部变量表所需的内存空间是在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如过虚拟机栈可以动态扩展(大部分都可扩展),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈(Native Method Stack)

虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈为虚拟机执行native方法服务

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如过虚拟机栈可以动态扩展(大部分都可扩展),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

栈帧是方法运行时的基础数据结构

Java堆(Java Heap)

Java堆是java虚拟机所管理的内存中最大的一块

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建

目的:存放对象实例,几乎所有对象都在这里分配内存

所有对象实例以及数组都要在堆上分配

随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有对象都在堆分配不在那么绝对

java堆是垃圾收集器管理的主要区域,因此很多时候也被叫做"GC堆"(Garbage Collected Heap)

现在的收集器基本采用分代收集法,所以java堆可以细分为:新生代和老年代,再细致点有Rden空间,From Survivor空间,To Survivor空间等,

java堆可以处于物理上的不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间,在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制x)

如果堆中没有内存完成实例分配,并且堆也无法扩展,将会抛出OutOfMemoryError异常

方法区(Method Area)


方法区也是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据

别名(Non-Heap)

很多人更愿意把方法区称为永久代,本质上不等价,只是HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说使用永久代来实现方法区

在这里插入图片描述

运行时常量池(Runtime Constant Pool)

  • 是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
  • 除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中
  • 运行时常量池相对于Class文件常量池,具有动态性,运行期间也可以将新的常量放入常量池,比如String类的intern()方法。
  • 当运行时常量池无法申请到更多的内存时,将会抛出OutOfMemoryError异常。
直接内存(Direct Memory)

  • 并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但被频繁使用,而且也可能导致OutOfMemoryError异常
  • JDK 1.4加入的NIO(New Input/Output), 它可以使用Native函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在java堆和native堆中来回复制数据
  • 本机直接内存的分配不会受到Java堆大小的限制,会受到本机总内存(包括Swap以及RAM区)的大小以及处理器寻址空间的限制

HotSpot虚拟机对象探秘

对象的创建
  • 当遇到new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,先判断这个类是否被加载、解析、初始化过,如果没有,先执行相应类的加载过程。
  • 类加载检查通过后,为新生对象分配内存,如果Java堆内存是规整连续的,采用“指针碰撞”的分配方式,如果是不连续规整的,采用“空闲列表”分配方式。内存是否规整取决于垃圾收集器是否带有压缩整理功能。
  • Serial,ParNew等带有Compact过程的收集器,采用的分配算法是“指针碰撞”。而CMS这种基于Mark-Sweep算法的收集器,通常采用“空闲列表”分配方式。
  • 创建对象涉及到分配内存和指针指向两个操作,不是原子性的,不是线程安全的。针对这个问题,有两个解决办法:1是采用CAS加上失败重试来保证操作的原子性。2是采用TLAB(Thread Local Allocation Buffer)策略,在Java堆中预先为每一个线程分配一小块内存,称为TLAB(Thread Local Allocation Buffer),哪个线程要分配内存就在各自的TLAB上进行内存的分配,只有TLAB用完进行新的TLAB的分配时才需要同步锁定,虚拟机是否使用TLAB,可以通过 -XX:+/- UseTLAB
  • 内存分配完成后,需要将分配到的内存空间都初始化为零值,如果使用TLAB,这一工作过程也可以提前到TLAB分配时进行,保证了对象实例字段在代码中可以不赋初值就直接使用
  • 接下来,需要对对象进行设置,包括这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存在对象的对象头
  • 最后执行init方法,把对象按照程序员的意愿进行初始化。这样一个真正可用的对象s上面工作做完后,从虚拟机角度看,一个新的对象已经产生,但从Java
  • 程序的视角看,对象的创建才刚刚开始—方法还没有执行
  • 执行new指令之后会接着执行方法,一个真正可用的对象才算完全生产出来。
对象的内存布局

分为三块区域,对象头(Header)、实例数据(Instance Data)、对齐补充(Padding)。

  • 对象头包括两部分信息:

    1. 存储对象自身的运行时数据,如哈希码、对象的GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit,官方称为"MarK Word"。

    2. 另一个部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个对象来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是说,查找元数据信息并不一定要经过对象本身.如果对象是Java数组,那在对象头中必须有一块用于记录数组长度的数据

在这里插入图片描述

  • 实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容

  • 对齐填充并不是必然存在的,仅仅起着占位符的作用

    HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分刚好是8字节的整数倍,所以实例数据部分要通过填充对齐

对象的访问定位**

Java程序需要通过栈上的reference数据来操作堆中的具体对象,具体实现流行的方式有两种方式:使用句柄和直接指针两种。

  • 使用句柄:Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包括了对象的实例数据和类型数据各自的地址信息。
    在这里插入图片描述

  • 使用直接指针:reference中存储的直接就是堆中的对象地址,堆对象的布局中需要考虑如何放置访问类型数据的相关信息。

在这里插入图片描述
对比:

使用句柄:最大好处是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时对象移动是很普遍的)时只会改变句柄中的数据指针,reference本身不需要修改

使用直接指针;最大好处是速度更快,节省了一次指针定位的开销,HotSpot就采用的直接指针方式。

OutOfMemoryError异常

设置虚拟机启动参数

在这里插入图片描述

Java 堆溢出
  • 不断创建对象,保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,达到最大堆的容量限制后就会产生内存溢出异常。

  • -Xms20m 堆的最小值20m;-Xmx20m 堆的最大值20m;设置一样的参数可以避免堆自动扩展

  • XX:+HeapDumpOnOutOfMemoryError 内存溢出异常时Dump出当前的内存堆转储快照以便日后分析

    //VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
    public class HeapOOM {
    	static class OOMObject {
    	}
    	
    	public static void main(String[] args) {
    		List<OOMObject> list = new ArrayList<OOMObject>();
    		
    		while(true) {
    			list.add(new OOMObject());
    		}
    	}
    }
    

    可通过内存映像分析工具(ieda可以用JProfiler)对Dump出来的堆转储快照进行分析,确认对象是否必要,是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

虚拟机栈和本地方法栈溢出
  • -Xss 栈容量
  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出StackOverflowError
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError
  • 栈深度达到1000-2000完全没有问题
  • 建立多线程导致内存溢出,通过减少最大堆和减少栈容量来换取更多的线程

在这里插入图片描述

方法区和运行常量池溢出

String.intern()是一个native方法,作用是:如果字符串常量池中已经包含一个等于此对象的字符串,则返回常量池中这个字符串的String对象,否则,就将此对象包含的字符串添加到常量池中,并且返回此对象的引用

  • 多次调用String.intern()方法可以S产生内存溢出异常。

  • JDK 1.6之前,由于常量池被分配到了永久代,可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制方法区大小,从而间接限制常量池的容量

    //VM Args: -XX:PermSize=10M -Xx:MaxPermSize=10m
    public class RuntimeConstantOllOOM {
    	public static void main(String[] args) {
    		List<String> list = new ArrayList<>();
    		int i = 0;
    		while(true) {	
    			list.ad(String.valueOf(i++).intern());
    			}
    	}
    }
    

    关于String.intern()返回引用的测试

    public class RuntimeConstantPoolOOM {
    	public static void main(String[] args) {
    		String str1 = new stringbuilder("计算机").append("软件").toString;
    		System.out.print(str1.intern() == str);
    				String str2 = new stringbuilder("计ja").append("va").toString;
    		System.out.print(str2.intern() == str);
    	}
    }
    
    • 1.6:intern()方法会把首次遇到的字符串实例复制到永久代,返回的是永久代的字符串实例,而StringBuilder创建的字符串实例在堆上,所以不是一个引用
    • 1.7: intern()不再复制实例,只是在常量池中记录首次出现的实例引用
    • 方法区用于存放Class信息,如类名,访问修饰符,常量池,字段描述,方法描述等

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7FVGcrdh-1582378076228)(C:\Users\kris\AppData\Roaming\Typora\typora-user-images\image-20191217012931257.png)]

大量动态生成类的应用,可能会方法区溢出

本地直接内存溢出
  • 可以通过 -XX:MaxDirectMemorySize 指定。如果不指定,则默认和Java堆最大值(-Xmx 指定)一样

垃圾收集器和内存分配策略

1、对象已死吗?如何确定对象是否还“活着”

引用计数器方法

  • 给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减1。
  • 优点是判定简单,效率也很高。缺点是无法解决相互循环引用的问题

可达性分析方法

  • 通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,说明这个对象是可回收的。

  • Java语言中,可作为GC Roots的对象包括以下几种:

    虚拟机栈中引用的对象,

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

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

    本地方法栈中JNI引用的对象。

在这里插入图片描述

再谈引用

  • JDK1.2 之后把引用分为了四种:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)
  • 强引用:只要强引用还存在,就不会被垃圾回收器回收。类似 Object o=new Object()
  • 软引用:指一些有用但并非必须的对象,在系统将要发生内存溢出的时候,会将这部分对象回收。SoftReference 类来实现软引用
  • 弱引用:被弱引用关联的对象只能生存到下一次垃圾回收。WeakReference 类来实现弱引用
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间造车影响,也无法通过虚引用取得对象的引用。一个对象设置虚引用的唯一目的是在被垃圾回收的时候收到一个系统通知

对象被回收的过程

  • 当对象进行可达性分析没有与GC Roots相连的引用链,将会被第一次标记,并根据是否需要执行finalize()方法进行一次筛选,对象没有重写finalize()或者虚拟机已经调用过finalize(),都被视为不需要执行
  • 如果对象有必要执行finalize,会被放入到F-Queue队列中,并在稍后由虚拟机自动创建的低优先级的Finalizer线程去触发它,并不保证等待此方法执行结束。
  • 如果对象在finalize()方法执行中,重新和GC Roots产生了引用链,则可以逃脱此次被回收的命运,但finalize()方法只能运行一次,所以并不能通过此方法逃脱下一次被回收
  • 笔者不建议使用这个方法,建议大家完全忘掉这个方法的存在。

回收方法区

  • 主要包括废弃常量和无用类的回收。判断类无用:

    类的实例都被回收,

    类的ClassLoader被回收,

    类的Java.Lang.Class对象没有在任何地方引用。

    满足这三个条件,类才可以被回收(卸载)

  • HotSpot虚拟机通过 -Xnoclassgc 参数进行控制是否启用类卸载功能。在大量使用反射、动态代理、CGLib等框架,需要虚拟机具备类卸载功能,避免方法区发生内存溢出

垃圾回收算法

标记-清除
  • 先标记出所有要回收的对象,在标记完成后统一进行对象的回收。有两个不足:

1 是效率问题,标记和清除的效率都不高。

2 是空间问题,会产生大量不连续的内存碎片,碎片太多会都导致大对象无法找到足够的内存,从提前触发垃圾回收。

复制算法
  • 新生代分为一个Eden,两个Survival空间,默认比例是8:1。回收时,将Eden和一个Survival的存活对象全部放入到另一个Survival空间中,最后清理掉刚刚的Eden和Survival空间
  • 当Survival空间不够时,由老年代进行内存分配担保
标记-整理
  • 根据老年代对象的特点,先标记存活对象,将存活对象移动到一端,然后直接清理掉端边界以外的对象
分代收集
  • 新生代采用复制算法,老年代采用标记-删除,或者标记-整理算法。

HotSpot算法实现

枚举根节点实现

  • 可达性分析时会进行GC停顿,停顿所有的Java线程。
  • HotSpot进行的是准确式GC,当系统停顿下来后,虚拟机有办法得知哪些地方存在着对象引用,HotSpot中使用一组称为OopMap的数据结构来达到这个目的

安全点

  • HotSpot没有为每个指令都生成OopMap,只在特定的位置记录这些信息,这些位置称为安全点。安全点的选定不能太少,也不能太频繁,安全点的选定以“是否让程序长时间执行”为标准
  • 采用主动式中断的方式让所有线程都跑到最近的安全点上停顿下来。设置一个标志,各个程序执行的时候轮询这个标志,发现中断标志为真时自己就中断挂起

安全区域

  • 解决没有分配Cpu时间的暂时不执行的程序停顿。

垃圾收集器

​ 如果两个收集器之间有连线,说明可以搭配使用。没有最好的收集器,也没有万能的收集器,只有对应具体应用最合适的收集器。

Serial 收集器

  • 新生代收集器,单线程回收。优点在于,简单而高效,对于运行在Client模式下的虚拟机来说是一个很好的选择(比如用户的桌面应用)
  • 参数 -XX:UseSerialGC,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收

ParNew收集器

  • 新生代收集器,Serial的多线程版本,除了Serial收集器之外,只有它能与CMS收集器配合工作。
  • -XX:+UseConcMarkSweepGC 选项后默认的新生代收集器,也可以使用 -XX:+UseParNewGC 选项来强制指定它
  • ParNew收集器在单CPU的环境中,效果不如Serial好,随着CPU的增加,对于GC时系统资源的利用还是很有效的。
  • 默认开启的收集线程数和CPU数相等,可以使用 -XX:ParallelGCThreads 指定

Parallel Scavenge收集器

  • 新生代收集器,并行收集器,复制算法,和其他收集器不同,关注点的是吞吐量(垃圾回收时间占总时间的比例)。提供了两个参数用于控制吞吐量。
  • -XX:MaxGCPauseMillis,最大垃圾收集停顿时间,减少GC的停顿时间是以牺牲吞吐量和新生代空间来换取的,不是设置的越小越好
  • -XX:GCTimeRatio,设置吞吐量大小,值是大于0小于100的范围,相当于吞吐量的倒数,比如设置成99,吞吐量就为1/(1+99)=1%。
  • -XX:UseAdaptiveSizePolicy ,这是一个开关参数,打开之后,就不需要设置新生代大小(-Xmn)、Eden和Survival的比例(-XX:SurvivalRatio)、 晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,收集器会自动调节这些参数。

Serial Old 收集器

  • 单线程收集器,老年代,主要意义是在Client模式下的虚拟机使用。在Server端,用于在JDK1.5以及之前版本和Parallel Scavenge配合使用,或者作为CMS的后备预案。
    在这里插入图片描述

Palallel Old 收集器

  • 是Parallel Scavenge的老年代版本。在注重吞吐量的场合,都可以优先考虑Parallel Scavenge 和Palallel Old 配合使用
    在这里插入图片描述

CMS 收集器

  • Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器,尤其重视服务的响应速度。基于标记-清除算法实现。

  • 分为四个步骤进行垃圾回收:

    初始标记,

    并发标记,

    重新标记,

    并发清除。

    只有初始标记和重新标记需要停顿。

  • 初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记就是进行GC Roots的Tracing。

  • 重新标记为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,远比并发时间短。

  • 耗时最长的并发标记和并发清除过程中,处理器可以与用户线程一起工作。
    在这里插入图片描述

  • 它并不是完美的,有如下三个比较明显的缺点:

    1. 垃圾回收时会占用一部分线程,导致系统变慢,总吞吐量会降低。
    2. 无法处理浮动垃圾,需要预留足够的内存空间给用户线程使用,可以通过 -XX:CMSInitiatingOccupancyFraction 参数控制触发垃圾回收的阈值。

如果预留的内存无法满足程序需要,就会出现“Concurrent Mode Failure”失败,这时将启动应急预案,启用Serial Old 进行垃圾回收,停顿时间会变长

所以-XX:CMSInitiatingOccupancyFraction 参数的值设置的太高,会导致频繁“Concurrent Mode Failure”失败,性能反而降低。

3、标记-清理,容易产生内存碎片。-XX:+UseCMSCompactAtFullColletion 开启碎片整理功能,默认开启,-XX:CMSFullGCsBeforeCompaction,控制多少次不压缩的FullGC之后来一次带压缩的

G1 收集器

  • 包括新生代和老年代的垃圾回收。和其他收集器相比的优点:并行和并发,分代收集,标记-整理,可预测的停顿。垃圾回收分为以下几个步骤:

  • 初始标记:标记GC Roots能够直接关联到的对象,这阶段需要停顿线程,时间很短

  • 并发标记:进行可达性分析,这阶段耗时较长,可与用户程序并发执行

  • 最终标记:修正发生变化的记录,需要停顿线程,但是可并行执行

  • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来执行回收计划

    理解GC日志

20191228015047320

20191228015109281

在这里插入图片描述

垃圾收集器的参数总结

在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xRu5umJC-1582378076235)(C:\Users\kris\AppData\Roaming\Typora\typora-user-images\image-20191228015901565

内存分配和回收策略

  • 对象优先在Eden分配,当新生区没有足够的内存是,通过分配担保机制提前转移到老年代中去
  • 大对象直接进入老年代。大对象是指需要大量连续内存空间的对象,虚拟机提供了参数 -XX:PretenureSizeThreshold(只对Serial,PerNew两个回收器起效),令大于这个值得对象直接在老年代分配,避免了Eden和两个Survival之间发生大量的内存复制。
  • 长期存活的对象将进入老年代。虚拟机给每个对象定义了对象年龄计数器(Age),如果对象在Eden出生,经过第一次Minor GC后依然存活,并且能被Survival容纳的话,将被移动到Survival,对象年龄设为1。对象在Survival中每熬过一次Major GC,年龄就增加1,达到一定程度(默认是15),就会被晋升到老年代。对象晋升老年代的阈值,可以通过参数-XX:MaxTenuringThreShold 指定
  • 动态对象年龄判断。如果在Survival空间中相同年龄所有对象的大小综合超过了Survival空间的一半,年龄大于等于这个年龄的对象都会被晋升到老年代。无需等待年龄超过MaxTenuringThreShold指定的年龄
  • 空间分配担保。只要老年代的连续空间大于新生代对象总和或者历次晋升的平均大小,就进行Major GC,否则进行Full GC。

虚拟机性能监控与故障处理工具

jps

命令用法: jps [options] [hostid]

功能描述: jps是用于查看有权访问的hotspot虚拟机的进程. 当未指定hostid时,默认查看本机jvm进程

常用参数:-lmvV

详细说明:JAVA JPS 命令详解

jstat。监视JVM内存工具。

语法结构:

Usage: jstat -help|-options

​ jstat - [-t] [-h] [ []]

例子: jstat -gcutil 25444 1000 5

详细说明:JDK之jstat的用法

jinfo。查看和修改JVM运行参数

java -XX:+PrintFlagsFinal -version|grep manageable 【查看JVM中哪些参数可以被jinfo动态修改】

jinfo -flag +PrintGCDetails 105704 【修改参数 PrintGCDetails 的值】

4、jmap。命令用于生成heap dump文件

如果不使用这个命令,还可以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候自动生成dump文件。

jmap不仅能生成dump文件,还可以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。

详细使用:JVM调优命令-jmap

jstack。Java堆栈跟踪工具

详细使用:使用jstack精确找到异常代码jstack 工具使用性能调优

注意:dead lock问题,占用cpu时间最多的线程,频繁GC

入手点总结:

wait on monitor entry: 被阻塞的,肯定有问题,等待synchronized锁

runnable : 注意IO线程,IO阻塞的线程

in Object.wait(): 注意非线程池等待,调用Object.wait()的对象

常见JVM配置说明

1、JVM配置

CPU 核数:8;内存(GB):16;磁盘(GB):400

-Xms10g

-Xmx10g

-Xss512k

-XX:MetaspaceSize=256m

-XX:MaxMetaspaceSize=256m

-XX:MaxDirectMemorySize=128M
-XX:+UseG1GC

-XX:MaxGCPauseMillis=150

-XX:+ParallelRefProcEnabled

-XX:+UnlockExperimentalVMOptions

-XX:G1MaxNewSizePercent=70

-XX:ParallelGCThreads=8

-XX:ConcGCThreads=2

1.2、*CMS+ParNew*

CPU 核数:4;内存(GB):8;磁盘(GB):200

-Xmx4g
-Xms4g
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:SurvivorRatio=8
-XX:NewRatio=4
-XX:+HeapDumpOnOutOfMemoryError
-XX:+DisableExplicitGC
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:ParallelGCThreads=4
-XX:+CMSClassUnloadingEnabled
-XX:CMSFullGCsBeforeCompaction=1
-XX:CMSInitiatingOccupancyFraction=72

JVM调优案例分析与实践

Minor GC、Major GC和Full GC之间的区别

  • 每次 Minor GC 会清理年轻代的内存
  • Major GC 是清理老年代,Full GC 是清理整个堆空间—包括年轻代和老年代。大部分时候Major GC和Full GC区分的不是很明显
  • 不仅仅Major GC和Full GC会stop-the-world。所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。

常用命令

查看java进程号

两种方式都可以查看tomcat进程号
ps -ef | grep java

jps -lmvV |grep java

结果如下:2556

查看进程内线程情况
top -Hp 2556(2556为上一步查询出来的进程号)
找到占用cpu时间最长的线程号:3345

得到线程号的十六进制数
printf “%x\n” 3345(输出为a05)

使用jstack定位问题
jstack 2556 | grep a05

查看内存和swap使用情况。参考:https://www.cnblogs.com/coldplayerest/archive/2010/02/20/1669949.html

free -h

问题排查

内存和SWAP问题

虚拟机类加载机制

虚拟机把表示类的class文件加载到内存,经过校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制

类加载的时机

  • 使用new关键字实例化对象的时候、读取一个类的静态字段的时候、调用类的静态方法的时候
  • 使用java.lang.reflect包的方式对类进行反射调用的时候
  • 初始化类,发现其父类还未初始化,需要对父类进行初始化

类加载的过程

  • 加载。通过类的全限定名获取到定义此类的二进制字节流。将字节流所代表的静态存储结构转化成方法区的运行时数据结构。在方法区生成这个类的java.lang.Class对象。加载阶段和连接阶段的部分内容是交叉进行的。用户可以通过自己写的类加载器去控制字节流的获取方式(重写类加载器的loadClass()方法),
  • 验证。是连接阶段的第一步。目的是确保class文件中的二进制字节流符合虚拟机的要求,不会危及虚拟机自身安全。包括文件格式验证、元数据验证、字节码验证
  • 准备。是连接阶段的第二步。是正式为类变量分配内存空间和设置初始值的阶段。这个初始值和初始化阶段的赋值不同,这里指的是变量的默认初始值。另外,如果时final修饰的变量,那么会在准备阶段赋予代码里指定的初始值
  • 解析。是连接阶段的第三步。是虚拟机将符号引用替换为直接引用的过程
  • 初始化。根据程序代码去初始化类变量和其他资源

类加载器

  • 被不同类加载器加载的同名类,也认为是不同的类。
  • 双亲委派模型。分为两种类加载器: 1 是启动类加载器 ,是虚拟机自身的一部分;2 是所有的其他类加载器,这些类加载器都由java语言实现。独立于虚拟机外部,全部继承自java.lang.ClassLoader抽象类。类加载器具体层次关系:启动类加载器->扩展类加载器->系统类加载器->自定义类加载器。每一个类的加载,会优先由父加载器来加载。这种方式就称为双亲委派,双亲委派保证了java基本类的不会被破坏和替代

高效并发

Java内存模型与线程

硬件的效率与一致性

  • 完成计算任务,处理器必须和内存交互才能完成,比如读取运算数据,写入计算结果等。这个I/O操作是很难消除的。计算的处理器和存储设备的运算速度有几个数量级的差距。所以现代计算机加入了一层读写速度尽可能接近处理器的高速缓存
  • 高速缓存解决了处理器和内存的速度矛盾,却引入了新的问题:内存一致性。多处理器系统中,各个处理器都有自己的高速缓存,又同时共用内存。为了解决这一问题,在读写内存时需要遵循缓存一致性协议
  • 处理器会对输入的代码进行乱序执行优化,类似的,Java虚拟机也存在着指令重排序优化。

Java内存模型

Java内存模型规定,所有的变量(这个变量和java编程中的变量有区别,它包括了实例字段、静态字段。不包括局部变量和方法参数,因为后者是线程私有的)都存储在主内存,每条线程有自己的工作内存,工作内存中保存了该线程使用到的变量的拷贝副本,线程对变量的所有操作都必须在工作内存中进行,线程间变量值得传递需通过主内存来完成

主内存和工作内存间交互协议,8种原子操作:

  • lock(锁定主内存)
  • unlock(解锁主内存)
  • read(读取主内存,为load准备)
  • load(载入主内存至工作内存)
  • use(执行引擎使用工作内存)
  • assign(接受执行引擎计算后的值赋值给工作内存)
  • store(存储工作内存至主内存,为write准备)
  • write(把工作内存写入主内存)

volatile是java虚拟机提供的轻量级的同步机制,对于volatile变量的特殊规则:

  • 保证了变量对所有线程的可见性,当一个线程修改了这个变量的值,修改后的值对其他线程来说是立即可见的。普通变量,需要通过把新值会写到主内存,其他线程从主内存读取之后才可以看到最新值
  • 禁止指令重排序优化。
  • 无法保证符合操作的原子性,比如i++
  • 通过内存屏障实现的可见性和禁止重排序。不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这些差异,由JVM来为不同的平台生成相应的机器码来完成。X86 处理器只会对写-读进行指令重排序,写volatile变量时,会加lock总线锁,将cpu缓存写入主存,其他cpu的读都会被阻塞,然后其他核的缓存某些对应数据会被标记为失效,那么其他核下次读的时候先读缓存发现失效了,然后去主存读

关于long和double类型变量的特殊规则:允许虚拟机将没有被volatile变量修饰的64位数据的读写操作划分为两次32位的操作来进行。这点就是long和double的非原子性协定

Java与线程

Java虚拟机实现线程,有三种方式:

(1)通过内核线程实现。jvm中的一个线程对应一个轻量级进程,一个轻量级进程对应一个内核线程。CPU通过调度器对线程进行调度。缺点:

  • 由于基于内核线程实现,各种线程操作需要系统调用,系统调用代价较高,需要在用户态和内核态之间来回切换

  • 每个线程都需要一个内核线程的支持,因此轻量级进程会消耗内核资源,一个系统支持的轻量级进程是有限的

  • 2)使用用户线程实现。不需要切换回内核态,也可以支持规模更大的线程数量。部分高性能数据库的多线程就是使用用户线程实现的。缺点是没有系统内核的支援,所有问题需要自己考虑,程序实现比较复杂

    (3)内核线程和用户线程结合

4)JVM,对于Sun JDK来说,在Windows和LInux系统下,都是使用的一对一的线程模型实现的。

Java线程调度

  • 协同式线程调度。线程的执行时间由自己控制,线程执行完毕,会主动通知系统
  • java使用的是抢占式调度。每个线程有系统分配执行时间,线程的切换也有系统来决定,线程的执行时间是可控的。线程可以设置优先级,来争取更多的执行时间。Java一共设置了10个优先级,操作系统的优先级数量可能和java定义的不一致,另外操作系统还可以更改线程的优先级,所以Java中优先级高的线程并不一定被优先执行。

Java线程状态转换

线程的状态
  • 新建: 创建后尚未启动的线程处于这种状态

  • 运行: 运行包括了操作系统线程状态中的running和Ready,处于这种状态的线程可能正在执行,也可能正在等待着cpu为它分配执行时间

  • 无限期等待: 处于这种状态的线程不会被分配cpu执行时间,她们要等待被其他线程显式地唤醒

    以下方法会让线程陷入无限期的等待状态

    • 没有设置Timeout参数的wait()
    • 没有设置Timeout参数的join()
    • park()方法
  • 限期等待: 处于这种状态的线程也不会被分配cpu执行时间,不过无需等待被其他线程显式唤醒,在一定时间之后她们会由系统自动唤醒

    以下方法会使线程陷入限期等待状态

    • sleep()方法
    • Timeout参数的wait()
    • Timeout参数的join()
    • parkNanos()方法
    • parkUtil()方法
  • 阻塞: 等待获取到一个排他锁

  • 结束: 已终止线程的线程状态

线程安全与锁优化

程调度**

  • 协同式线程调度。线程的执行时间由自己控制,线程执行完毕,会主动通知系统
  • java使用的是抢占式调度。每个线程有系统分配执行时间,线程的切换也有系统来决定,线程的执行时间是可控的。线程可以设置优先级,来争取更多的执行时间。Java一共设置了10个优先级,操作系统的优先级数量可能和java定义的不一致,另外操作系统还可以更改线程的优先级,所以Java中优先级高的线程并不一定被优先执行。

Java线程状态转换

线程的状态
  • 新建: 创建后尚未启动的线程处于这种状态

  • 运行: 运行包括了操作系统线程状态中的running和Ready,处于这种状态的线程可能正在执行,也可能正在等待着cpu为它分配执行时间

  • 无限期等待: 处于这种状态的线程不会被分配cpu执行时间,她们要等待被其他线程显式地唤醒

    以下方法会让线程陷入无限期的等待状态

    • 没有设置Timeout参数的wait()
    • 没有设置Timeout参数的join()
    • park()方法
  • 限期等待: 处于这种状态的线程也不会被分配cpu执行时间,不过无需等待被其他线程显式唤醒,在一定时间之后她们会由系统自动唤醒

    以下方法会使线程陷入限期等待状态

    • sleep()方法
    • Timeout参数的wait()
    • Timeout参数的join()
    • parkNanos()方法
    • parkUtil()方法
  • 阻塞: 等待获取到一个排他锁

  • 结束: 已终止线程的线程状态

线程安全与锁优化

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值