JVM+调优(整合拉勾、图灵等课程)

作者 : admin 本文共33645个字,预计阅读时间需要85分钟 发布时间: 2024-06-16 共1人阅读

写在前面:该文档是整理了图灵学院的 【一、性能调优专题】输出的

提炼的问题点

为什么要设计双亲委派机制?

安全,避免基础类被重新加载;避免重复加载

同一个JVM内,类的全限定名一样就是一个类对象吗?

必须类加载器也要是同一个

Tomcat 如果使用默认的双亲委派类加载机制行不行?他是如何设计的?

不行,因为tomcat作为一个web服务器,需要支持加载多个web项目,肯定会有多个加载同一个类名的需求 ​ 设计上自定义类加载器,重写委派方法,使其使用自己的类加载器去加载不同/WEB-INF/下的jar

什么是内存泄漏?如何产生的?怎么解决

图方便对于JVM级缓存就简单使用一个hashmap,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁。这就是一种内存泄漏,一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。 ​ 这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。

如何判断一个类是无用的类的呢?

方法区主要回收的是无用的类,类需要同时满足下面3个条件才能算是 “无用的类” :

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  2. 加载该类的 ClassLoader 已经被回收。

  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

jdk1.7和1.8之间方法区的实现区别?

1.8将堆里方法区取消,放入元空间,元空间在堆之外,存在于本地内存。

方法区是规范,7/8实现不同而已。7是永久代,8是元空间

变化:

  1. 移除永久代,替换为元空间

  2. 永久代的类元信息转移到本地内存

  3. 永久代中字符串常量池,类静态变量转移到堆

  4. 永久代参数转为元空间参数

为什么要变?

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。

  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定困难

  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

  4. Oracle 可能会将HotSpot 与 JRockit 合二为一,JRockit没有所谓的永久代。

各种常量池解释一下?

静态常量池:也叫class文件常量池 ​ 运行时常量池:主要存编译期间生成的字面量,符号引用等。当类加载到内存里后,JVM会把class常量池里的放入运行时常量池;还有可以放入新的常量 ​ 字符串常量池: 也可以理解成运行时常量池分出来的一部分。类加载到内存的时候,字符串会存到字符串常量池里面。利用池的概念,避免大量频繁创建字符串。

字符串常量池变迁?

Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池

Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里

Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里

类加载机制

类的加载过程

JVM+调优(整合拉勾、图灵等课程)插图

其中loadClass的类加载过程有如下几步: 加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的 main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口

  • 验证:校验字节码文件的正确性

  • 准备:给类的静态变量分配内存,并赋予默认值

  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态链接

  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。 jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

源码级别过程

JVM+调优(整合拉勾、图灵等课程)插图

类加载器和双亲委派

四种类加载器
  • bootstrapLoader引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等

  • extClassloader扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包

  • appClassLoader应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类

  • 自定义加载器:负责加载用户自定义路径下的类包

类加载器初始化过程:

类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。使用了单例模式设计,保证一个JVM虚拟机内只有一个实例。

在Launcher构造方法内部,其创建了两个类加载器,分别是

  • sun.misc.Launcher.ExtClassLoader(扩展类加载器)

  • sun.misc.Launcher.AppClassLoader(应用类加载器)

JVM默认使用Launcher的 getClassLoader()方法返回的类加载器 AppClassLoader的实例加载我们的应用程序。

自定义类加载器

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法

  • loadClass(String, boolean),实现了双亲委派机制

  • findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。

双亲委派

双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改

  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

打破双亲委派机制

尝试打破双亲委派机制,用自定义类加载器加载我们自己实现的java.lang.String.class:在loadClass里做手脚,重写类加载方法时,实现自己的加载逻辑,不委派给双亲加载。此时报错:java.lang.SecurityException: Prohibited package name: java.lang

Tomcat打破双亲委派

以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?

我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。

  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。

再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行? 答案是不行的。为什么? 第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

第三个问题和第一个问题一样。

我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

Tomcat自定义加载器详解

JVM+调优(整合拉勾、图灵等课程)插图

tomcat的几个主要类加载器:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容 器本身以及各个Webapp访问;

  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不 可见;

  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有 Webapp可见,但是对于Tomcat容器不可见;

  • WebappClassLoader:⽤来加载本应⽤程序 /WEB-INF/classes 和 /WEB-INF/lib 下的类。也是各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,实现相互隔离。比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本

tomcat 8.5 默认改变了严格的双亲委派机制,加载过程:

  • ⾸先从 Bootstrap Classloader加载指定的类

  • 如果未加载到,则从 /WEB-INF/classes加

  • 如果未加载到,则从 /WEB-INF/lib/*.jar 加载

  • 如果未加载到,则依次从 System、Common、Shared 加载(在这最后⼀步,遵从双亲委派机制)

注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样

所以看两个类对象是否是同一个?除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。

JVM内存模型深度剖析及调优入门

内存模型

JVM+调优(整合拉勾、图灵等课程)插图

  • 本地方法栈

  • 程序计数器

  • 方法区(元空间):常量+静态变量+加载上的类信息

常见JVM参数设置

JVM+调优(整合拉勾、图灵等课程)插图

元空间参数

-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。

-XX:MetaspaceSize: 指定元空间触发Full gc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。

由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。

-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多

初见调优(JVM参数)

JVM参数大小设置并没有固定标准,需要根据实际项目情况分析,给大家举个例子:日均百万级订单交易系统如何设置JVM参数?

JVM+调优(整合拉勾、图灵等课程)插图

即可得到参数:

java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M 
-XX:MaxMetaspaceSize=512M -jar app.jar

能否再调优,让其几乎不发生FullGC

JVM+调优(整合拉勾、图灵等课程)插图

java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M 
-XX:MaxMetaspaceSize=256M -jar app.jar

通过上面这些内容介绍,大家应该对JVM优化有些概念了,就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存打小,避免新生代频繁的进行垃圾回收。

JVM对象创建与内存分配机制

对象的创建过程

对象的创建流程:

JVM+调优(整合拉勾、图灵等课程)插图

1.类加载检查

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 new指令对应到语言层面上讲是:new关键词、对象克隆、对象序列化等。

2.分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。其大小在加载完成后便可完全确定。 即为对象分配空间的任务等同于:把一块确定大小的内存从Java堆中划分出来。

这个步骤有两个问题:

  1. 如何划分内存。

  2. 在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

1.划分内存的方法:

  • “指针碰撞”(Bump the Pointer)(默认用指针碰撞) 如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

  • “空闲列表”(Free List) 如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

2.解决并发问题的方法:

  • CAS(compare and swap) 虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB) 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过­XX:+/­UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启­XX:+UseTLAB),­XX:TLABSize 指定TLAB大小。

3.初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

问题:哪些语言需要赋值才能使用呢?

4.设置对象头

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:

  • 对象头(Header)

  • 实例数据(Instance Data)

  • 对齐填充(Padding)

HotSpot虚拟机的对象头包括两部分信息:

  • 存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

  • 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

5.执行init方法

执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。

对象大小与指针压缩

对象大小可以用jol­core包查看,引入依赖

什么是java对象的指针压缩?

1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩

2.jvm配置参数:UseCompressedOops,compressed­­压缩、oop(ordinary object pointer)­­对象指针

3.启用指针压缩:­XX:+UseCompressedOops(默认开启),禁止指针压缩:­XX:­UseCompressedOops

为什么要进行指针压缩?

1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力

2.为了减少64位平台下内存的消耗,启用指针压缩功能

3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)

4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间

5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好

创建新对象时在内存分配情况

对象内存分配流程图:

JVM+调优(整合拉勾、图灵等课程)插图

对象栈上分配

JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。

为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。

若方法内创建的对象被返回出去,其作用域就不确定了

若方法内创建的对象不被返回,即方法结束对象就是无效的了,可以为其分配栈内存,同方法结束一起销毁

JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配)

JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)

  1. 标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。

  2. 标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。

开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。

对象在Eden区分配

大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Young GC。我们来进行实际测试一下。

在测试之前我们先来看看 Young GC和Full GC 有什么不同呢?

  • Minor GC/Young GC:指发生新生代的的垃圾收集动作,Young GC非常频繁,回收速度一般也比较快。

  • Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Young GC的慢10倍以上。

概念:

  1. Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC;

  2. Minor GC和Major GC是俗称,在Hotspot JVM实现的Serial GC, Parallel GC, CMS, G1 GC中大致可以对应到某个Young GC和Old GC算法组合;

    1. 老年代的垃圾收集叫做Major GC,Major GC通常是跟full GC是等价的,收集整个GC堆。

Eden与Survivor区默认8:1:1 。让eden区尽量的大,survivor区够用即可

JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy

对象可以不经过Survior直接进去老年代

创建对象时分配内存的时候eden区内存几乎已经被分配完了,没有足够空间进行分配时,虚拟机将发起一次Young GC。GC期间虚拟机又发现eden区内对象A无法存入Survior空间,所以只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放对象A,所以不会出现Full GC。

执行Young GC后,后面分配的对象如果能够存在eden区的话,还是会在eden区分配内存。

大对象直接进入老年代

为了避免为大对象分配内存时的复制操作而降低效率。

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)

JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。

对象进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。

为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Young GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 Young GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。

JVM参数 -XX:MaxTenuringThreshold 来设置对象晋升到老年代的年龄阈值。

对象动态年龄判断

当前放对象的Survivor区域里:一批对象的总大小 > 这块Survivor区域内存大小的50%

(XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。

例如:Survivor区域里现在有一批对象

【年龄1+年龄2+年龄n】多个年龄对象总和 > Survivor区域的50%

此时就会把年龄n(含)以上的对象直接放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代

对象动态年龄判断机制一般是在Young gc之后触发的。

特殊讲下,【一批对象】的定义:

  1. 深入理解JVM书里:相同的某一个年龄

  2. 知乎上纠错:认为是从小到大(1~n)的年龄。 Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。

老年代空间分配担保机制
  1. 年轻代每次Young gc之前:JVM都会计算下老年代剩余可用空间,再计算年轻代里现有的所有对象大小之和(包括垃圾对象)

  2. 判断“-XX:-HandlePromotionFailure”(jdk1.8默认设置)的参数是否设置

  3. 如果有这个参数:就会比较 老年代可用内存 是否大于 历次Young gc后进入老年代平均值

    1. 小于:那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾。如果回收完还是没有足够空间存放新的对象就会发生”OOM”。

    2. 大于:只需Young GC

  4. 如果没有这个参数:直接触发 full GC

  5. 如上,都只是young gc前的担保机制。如果Young gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc。如果full gc完之后还是没有空间放Young gc之后的存活对象,则也会发生“OOM”

如下:

JVM+调优(整合拉勾、图灵等课程)插图

对象内存回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

一、引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。

二、可达性分析算法

将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象

GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等

常见引用类型

java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用

  • 强引用:普通的变量引用

  • 软引用:将对象用SoftReference软引用类型的对象包裹。正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。 软引用可用来实现内存敏感的高速缓存。

  • 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用

  • 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

// 软引用
public static SoftReference user = new SoftReference(new User());
​
// 弱引用
public static WeakReference user = new WeakReference(new User());

finalize()方法

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

  1. 第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,对象将直接被回收。

  2. 第二次标记 如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。

如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?类需要同时满足下面3个条件才能算是 “无用的类” :

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  2. 加载该类的 ClassLoader 已经被回收。

  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集器

垃圾收集算法

4种垃圾收集算法:

  • 分代收集理论

  • 复制算法

  • 标记-清除算法

  • 标记-整理算法

分代收集理论

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

注意,“标记-清除”或“标记-整理”算法会比“标记-复制”算法慢10倍以上。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。

JVM+调优(整合拉勾、图灵等课程)插图

Serial收集器

( -XX:+UseSerialGC -XX:+UseSerialOldGC )

新生代采用复制算法,老年代采用标记-整理算法。

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。

  • 单线程” ,只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程

优点:它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

Parallel Scavenge收集器

( -XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代) )

新生代采用复制算法,老年代采用标记-整理算法。

Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。

  • Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。

  • CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。

所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。

Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。

ParNew收集器

( -XX:+UseParNewGC )

新生代采用复制算法,老年代采用标记-整理算法。

ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

CMS收集器

( -XX:+UseConcMarkSweepGC(old))

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  1. 初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。

  2. 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。

  3. 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。

  4. 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。

并发重置:重置本次GC过程中的标记数据。

JVM+调优(整合拉勾、图灵等课程)插图

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:

  • 对CPU资源敏感(会和服务抢资源);

  • 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清了);

  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理

  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是”concurrent mode failure”,此时会进入stop the world,用serial old垃圾收集器来回收

对象内存回收-底层算法实现(重要)

垃圾收集器需要标记对象是否可以回收,底层算法实现有2种:

  • 标记引用计数法:解决不了对象的循环引用

  • 可达性分析算法:目前主要在用(也叫三色标记算法)

三色标记

这里我们引入“三色标记”来给大家解释下,把GC roots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:

  • 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。

  • 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。

  • 白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。

JVM+调优(整合拉勾、图灵等课程)插图

问题:在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。

多标-浮动垃圾

产生的两种情况

  • 并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,因其之前被标记为黑色

  • 并发标记和并发清理之后产生的对象,全部是黑色

浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。

漏标-读写屏障

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案:

  • 增量更新(IncrementalUpdate)

  • 原始快照(Snapshot At The Beginning,SATB)

增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)

以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

写屏障

所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念)

  • 写屏障实现SATB 当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来

  • 写屏障实现增量更新 当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来

读屏障

现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。

对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:

  • CMS:写屏障 + 增量更新

  • G1,Shenandoah:写屏障 + SATB

  • ZGC:读屏障

工程实现中,读写屏障还有其他功能,比如写屏障可以用于记录跨代/区引用的变化,读屏障可以用于支持移动对象的并发执行等。功能之外,还有性能的考虑,所以对于选择哪种,每款垃圾回收器都有自己的想法。

为什么G1用SATB?CMS用增量更新?

我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

记忆集与卡表

在新生代做GCRoots可达性扫描过程中可能会碰到跨代引用的对象,这种如果又去对老年代再去扫描效率太低了。为此,在新生代可以引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。

事实上并不只是新生代、 老年代之间才有跨代引用的问题, 所有涉及部分区域收集(Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC和Shenandoah收集器, 都会面临相同的问题。

垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引用指针的全部细节。

hotspot使用一种叫做“卡表”(cardtable)的方式实现记忆集,也是目前最常用的一种方式。关于卡表与记忆集的关系,可以类比为Java语言中HashMap与Map的关系。

卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。

hotSpot使用的卡页是2^9大小,即512字节

一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0.

GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。

卡表的维护 卡表变脏上面已经说了,但是需要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1。Hotspot使用写屏障维护卡表状态。

垃圾收集器G1&ZGC

G1垃圾收集器优化建议

假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。

那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。 或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。

所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.

什么场景适合使用G1

  1. 50%以上的堆被存活对象占用

  2. 对象分配和晋升的速度变化非常大

  3. 垃圾回收时间特别长,超过1秒

  4. 8GB以上的堆内存(建议值)

  5. 停顿时间是500ms以内

— 总结(承上启下)–

  1. 为什么需要学习JVM?

  2. 为什么学的是JVM这几个知识点?

  3. 调优是怎么调的?

  4. 调优的标准是什么

  1. 为什么需要学习JVM?

    1. 为了面试;为了调优;为了更好的写代码;更全面的了解java(地基)

  2. 为什么学的是JVM这几个知识点?

    1. 完全是参照 周志明【深入理解JVM虚拟机】的书本制定知识点;

    2. 这些知识点串起来,就是为了更全面的调优,调的是哪里,为什么需要这么调

    3. 了解了堆内存各个区的分配,对象创建时的内存分配机制,才能知道怎么更好的制定调优参数

  3. 调优是怎么调的?

我以为学了调优之后会的东西(已被推翻的肤浅理解):

  1. JVM参数调优

  2. GC日志分析

  3. 解决线上问题

实际调优,完全就只是JVM参数调优。

GC日志是辅助工具

线上问题是分析各个参数(含内存,线程,gc次数等),然后调优JVM参数,使其参数达到指标范围内

JVM学习思路总结:

  1. JVM调优就是为了:JVM的内存 正常稳定

  2. 学习内存模型,对象创建,内存分配是了解每个部分的内存

  3. 垃圾回收器为了清理内存

  4. GC日志是为了分析 垃圾回收过程是否正常,从而优化代码或优化JVM参数

  5. 线上java进程状态查看:通过相关java的命令来查看当前进程状态,dump当前jvm的内存状态,再通过工具分析大对象等

常见内存问题解决步骤

1.内存溢出

  • 先获取dump文件(命令:jmap)

  • 根据dump文件,使用工具分析内存里各个大对象 常见的大对象:String,B(byte),C(char),List,Map等

2.线上卡顿,服务反应慢

  • 查看内存情况(参考1.内存溢出)

  • 查看线程:定位到某个对象占用内存较高,但由于使用对象的地方太多,无法定位具体代码。可以分析下占用cpu较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的cpu必然较高。可以用上面讲过的jstack或jvisualvm来定位cpu使用较高的代码

  • 分析GC日志:观察一段时间内内存回收的情况 如新生代和老年代大小分配不合理;0-100ms内 MinorGC太多等(实战部分有详解)

JVM调优工具详解

第一步是 JVM的常用指令学习

第二步是 jvm内存查看。有在线的,也可以dump下来分析,常用工具:VisualVM

第三步是 GC日志查看-分析多次内存回收情况

  • 日志含义解释

  • 日志分析工具 GCeasy等

第四步是 根据GC日志调优JVM

  • 要知道GC各个参数的标准是什么,日志的问题在哪里

详情见 拉钩课程-第二部分-优化-JVM优化

JVM常用指令(拉钩版)

  • jps : Java版的ps命令,查看java进程及其相关的信息,如果你想找到一个java进程的pid,那可以用jps命令替代linux中的ps命令了,简单而方便。

    jps  [options] [hostid]
    options
    -l : 显示进程id,显示主类全名或jar路径
    -q : 显示进程id
    -m : 显示进程id, 显示JVM启动时传递给main()的参数
    -v : 显示进程id,显示JVM启动时显示指定的JVM参数
    ​
    hostid 不填默认本机
  • jinfo:用来查看JVM参数动态修改部分JVM参数的命令

    jinfo  [option] 
    options参数解释:
    no options 输出所有的系统属性和参数
    -flag 打印指定名称的参数
    -flag [+|-] 打开或关闭参数
    -flag = 设置参数
    -flags 打印所有参数
    -sysprops 打印系统配置
    ​
    示例:
    查看JVM参数:jinfo -flags 11666
    查看打印GC日志参数:jinfo -flag PrintGC 11666
  • jstat:使用频率比较高的命令,主要用来查看JVM运行时的状态信息,包括内存状态、垃圾回收等。

    jstat [option] VMID  [interval] [count ]
    其中VMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印)
  • jstack:用来查看JVM线程快照的命令,线程快照是当前JVM线程正在执行的方法堆栈集合。使用jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环等。jstack还可以查看程序崩溃时生成的core文件中的stack信息

  • jmap:可以生成 java 程序的 dump 文件,;也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及finalizer 队列

    jmap [option] (连接正在执行的进程)
    ​
    打印heap摘要: java -heap 11666
    输出存活对象统计信息:jmap -histo:live 11666 | more
    输出dump:jmap -dump:live,format=b,file=dump.bin 11666
  • jhat:用来分析jmap生成dump文件的命令,jhat内置了应用服务器,可以通过网页[http://本机:7000]查看dump文件分析结果,jhat一般是用在离线分析上。

    很少用,用工具分析更直观

实战1:cpu占用过高问题(jstack)

  1. 本次分析是在window上,需要安装Process Explorer工具

  2. 工具里找到cpu占用率较高的线程,再进到 thread卡中找到占用最高的线程id(这个id是十进制需转为16进制)

  3. 使用jstack -l pid 查看进程的所有线程快照,在快照里找到对应的线程id

  4. 根据快照里这个线程的信息就能找到对应的代码

实战2:死锁定位(jstack)

命令 jstack -l pid 可直接打印出死锁信息:

Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007efc880062c8 (object 0x00000000ec1dc5c8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007efc88004e28 (object 0x00000000ec1dc5d8, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
   at DeadLock$Thread2.run(DeadLock.java:35)
    - waiting to lock  (a java.lang.Object)
    - locked  (a java.lang.Object)
   at java.lang.Thread.run(Thread.java:748)
"Thread-0":
   at DeadLock$Thread1.run(DeadLock.java:19)
    - waiting to lock  (a java.lang.Object)
    - locked  (a java.lang.Object)
   at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.

JVM常用分析工具

VisualVM 可视化优化工具

VisualVM 是一个工具,它提供了一个可视界面,用于查看JVM上运行的基于Java程序的详细信息。

  • 展示虚拟机中运行的应用程序的性能和资源消耗等信息

  • 可以查看远程主机上运行的java程序

  • 可以捕获有关 JVM 软件实例的数据,并将该数据保存到本地系统,以供后期查看或与其他用户共享。

功能描述:VisualVM基于NetBeans平台开发, 因此它一开始就具备了插件扩展的特性, 通过插件支持, VisualVM可以做许多事情,例如:

  • 显示虚拟机进程和进程的配置、环境信息(jps、jinfo)

  • 监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)

  • dump及分析堆转储快照(jmap、jhat)

  • 方法级的程序运行性能分析, 找出被调用最多、运行时间最长的方法

  • 离线程序快照: 收集程序的运行时配置、线程dump、内存dump等信息建立一个快照, 可以将快照发送开发者处进行bug反馈等等

GC日志分析

通过分析一段时间内的GC日志,可以观察JVM内存回收情况。如:JVM各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量等,这些指标在我们进行JVM调优的时候是很有用的。

GC日志分析前置步骤

  1. 先在启动参数中设置JVM参数,才能看到gc日志

  2. 再GC日志工具分析里查看GC时的参数是否正常

JVM+调优(整合拉勾、图灵等课程)插图

可以使用如下参数启动java进程

-Xms28m
-Xmx28m
//开启记录GC日志详细信息(包括GC类型、各个操作使用的时间),并且在程序运行结束打印出JVM的内存占用情况
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+UseGCLogFileRotation      开启滚动生成日志
-Xloggc:E:/logs/gc.log   --日志生成位置

GC 日志参数含义

JVM+调优(整合拉勾、图灵等课程)插图

YoungGC

JVM+调优(整合拉勾、图灵等课程)插图

FullGC

JVM+调优(整合拉勾、图灵等课程)插图

GC日志分析工具

GC日志可视化分析工具

  • GCeasy:在线网页(个人推荐使用)

  • GCviewer:离线 app

通过GC日志可视化分析工具,更直观的表现出JVM状况。

JVM常用指令(图灵版)

Jmap

此命令可以用来查看内存信息,实例个数以及占用内存大小

  1. 先 jps找到进程

  2. jmap -histo pid > /temp/log.txt

    • num:序号

    • instances:实例数量

    • bytes:占用空间大小

    • class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[]]]

堆信息

jmap -heap pid

JVM+调优(整合拉勾、图灵等课程)插图

堆内存dump:

jmap ‐dump:format=b,file=tmp.hprof pid

也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

  1. -XX:+HeapDumpOnOutOfMemoryError

  2. -XX:HeapDumpPath=./ (路径)

可以用jvisualvm命令工具导入该dump文件分析

Jstack

用jstack加进程id查找死锁

可以配置启动参数远程连接jvisualvm自动检测死锁

jstack找出占用cpu最高的线程堆栈信息

/**
  * 运行此代码,cpu会飙高
  */
  public int compute() { //一个方法对应一块栈帧内存区域
      int a = 1;
      int b = 2;
      int c = (a + b) * 10;
      return c;
  }
​
 public static void main(String[] args) {
      Math math = new Math();
      while (true){
      math.compute();
  }
​

1,使用命令top -p ,显示你的java进程的内存情况,pid是你的java进程号,比如19663

2,按H,获取每个线程的内存情况

3,找到内存和cpu占用最高的线程tid,比如19664

4,转为十六进制得到 0x4cd0,此为线程id的十六进制表示

5,执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法

6,查看对应的堆栈信息找出可能存在问题的代码

Jinfo

查看正在运行的Java应用程序的扩展参数

查看jvm的参数:

jinfo -flags pid

查看java系统参数:

jinfo -sysprops pid

Jstat

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:

jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]

注意:使用的jdk版本是jdk8

垃圾回收统计

最常用,可以评估程序内存使用及GC压力整体情况

jstat -gc pid 

S0C:第一个幸存区的大小,单位KB S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小

S1U:第二个幸存区的使用大小 EC:伊甸园区的大小 EU:伊甸园区的使用大小 OC:老年代大小 OU:老年代使用大小 MC:方法区大小(元空间) MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间,单位s FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间,单位s GCT:垃圾回收消耗总时间,单位s

堆内存统计
jstat -gccapacity pid

NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量 S0C:第一个幸存区大小 S1C:第二个幸存区的大小 EC:伊甸园区的大小 OGCMN:老年代最小容量 OGCMX:老年代最大容量 OGC:当前老年代大小 OC:当前老年代大小 MCMN:最小元数据容量 MCMX:最大元数据容量 MC:当前元数据空间大小 CCSMN:最小压缩类空间大小 CCSMX:最大压缩类空间大小 CCSC:当前压缩类空间大小 YGC:年轻代gc次数 FGC:老年代GC次数

新生代垃圾回收统计
jstat -gcnew pid

S0C:第一个幸存区的大小 S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小 TT:对象在新生代存活的次数 MTT:对象在新生代存活的最大次数 DSS:期望的幸存区大小 EC:伊甸园区的大小 EU:伊甸园区的使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间

新生代内存统计

老年代垃圾回收统计

老年代内存统计

JVM调优标准(常见指标范围)

如何选择垃圾收集器

  1. 优先调整堆的大小让服务器自己来选择

  2. 如果内存小于100M,使用串行收集器

  3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择

  4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选

  5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器

  6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

下图有连线的可以搭配使用

JVM+调优(整合拉勾、图灵等课程)插图

JDK 1.8默认使用 Parallel(年轻代和老年代都是) JDK 1.9默认使用 G1

什么场景适合使用G1

  1. 50%以上的堆被存活对象占用

  2. 对象分配和晋升的速度变化非常大

  3. 垃圾回收时间特别长,超过1秒

  4. 8GB以上的堆内存(建议值)

  5. 停顿时间是500ms以内

常见JVM参数标准值

JVM+调优(整合拉勾、图灵等课程)插图

元空间参数

  • -XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。

  • -XX:MetaspaceSize: 指定元空间触发Full gc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M。 达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。

由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整

基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。

-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多

GC日志各项指标标准值

JVM当前运行情况预估

内存GC情况

用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数:

  • 比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。

年轻代对象增长的速率

可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。

Young GC的触发频率和每次耗时

知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。

每次Young GC后有多少对象存活和进入老年代

这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。

Full GC的触发频率和每次耗时

知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

内存中大对象定位

使用jvm命令,找出内存中大对象,判断其是否有优化空间

优化思路

其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

说白了还是调教JVM参数

面试准备

对于jvm调优总是在面试中被问到,想知道jvm调优这个事情,到底什么情况下会去jvm调优,jvm调优的话调的是什么,感觉没有实战经验面试的时候就算知道一些理论只是也不敢说,想知道怎么去回答这个jvm调优的问题?

== 平时可以找一些网上的面试题目练练,jvm调优,我觉得 主要就是调整

  1. 内存分配

  2. gc调优

内存分配主要是堆 栈 元数据

  • 堆里面 就是 老年代 新生代 from to

  • 栈 就是线程占用内存

再就是和 gc相关的,用什么算法,一些gc阈值

给你个网站:HeapDump – Java虚拟机参数分析。里面每个参数都有很详细的解释,你可以贴上你的参数 ,多用这个分析分析。参数、堆、栈三种分析,多看看就有感觉了

如果心里发虚 就多找一些实际的面试题,先自己答,再看答案。开始会答漏几次,等开始答全的时候,就不虚了

JVM调优实战

系统频繁Full GC导致系统卡顿是怎么回事

机器配置:2核4G JVM内存大小:2G 系统运行时间:7天 期间发生的Full GC次数和耗时:500多次,200多秒 期间发生的Young GC次数和耗时:1万多次,500多秒

大致算下来每天会发生70多次Full GC,平均每小时3次,每次Full GC在400毫秒左右; 每天会发生1000多次Young GC,每分钟会发生1次,每次Young GC在50毫秒左右。

JVM参数设置如下:

‐Xms1536M ‐Xmx1536M ‐Xmn512M ‐Xss256K ‐XX:SurvivorRatio=6 ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=75 ‐XX:+UseCMSInitiatingOccupancyOnly

JVM+调优(整合拉勾、图灵等课程)插图

大家可以结合对象挪动到老年代那些规则推理下我们这个程序可能存在的一些问题

经过分析感觉可能会由于对象动态年龄判断机制导致full gc较为频繁

对于对象动态年龄判断机制导致的full gc较为频繁可以先试着优化下JVM参数,把年轻代适当调大点:扩大一倍(‐Xmn1024)

JVM+调优(整合拉勾、图灵等课程)插图

优化完发现没什么变化,full gc的次数比Young gc的次数还多了

我们可以推测下full gc比Young gc还多的原因有哪些?

1、元空间不够导致的多余full gc

2、显示调用System.gc()造成多余的full gc,这种一般线上尽量通过­XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果

3、老年代空间分配担保机制

最快速度分析完这些我们推测的原因以及优化后,我们发现young gc和full gc依然很频繁了,而且看到有大量的对象频繁的被挪动到老年代,这种情况我们可以借助jmap命令大概看下是什么对象

JVM+调优(整合拉勾、图灵等课程)插图

查到了有大量User对象产生,这个可能是问题所在,但不确定,还必须找到对应的代码确认,如何去找对应的代码了?

1、代码里全文搜索生成User对象的地方(适合只有少数几处地方的情况)

2、如果生成User对象的地方太多,无法定位具体代码,我们可以同时分析下占用cpu较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的cpu必然较高。可以用上面讲过的jstack或jvisualvm来定位cpu使用较高的代码

最终定位到的代码:一次查询出500M的对象出来,明显不合适,要根据之前说的各种原则尽量优化到合适的值,尽量消除这种朝生夕死的对象导致的full gc

内存泄露到底是怎么回事

再给大家讲一种情况,一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。

这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。

JVM参数调优实战(ParNew+CMS)

大型电商系统后端现在一般都是拆分为多个子系统部署的,比如,商品系统,库存系统,订单系统,促销系统,会员系统等等。我们这里以比较核心的订单系统为例:

JVM+调优(整合拉勾、图灵等课程)插图

对于8G内存,我们一般是分配4G内存给JVM,正常的JVM参数配置如下:

 ‐Xms3072M ‐Xmx3072M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M  ‐XX:SurvivorRatio=8

上节课说过,这样设置可能会由于 动态对象年龄判断原则导致频繁full gc。

于是我们可以更新下JVM参数设置:将新生代调大

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M  ‐XX:SurvivorRatio=8

JVM+调优(整合拉勾、图灵等课程)插图

将新生代调大:降低了因为对象动态年龄判断原则导致的对象频繁进入老年代的问题。

其实很多优化无非就是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc。

进入老年代的两个条件

  • 年龄,调小提前进入

  • 对象大小,一般1M,一般就是缓存对象:大list map

对于JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),如果内存较大(超过4个G,只是经验值),系统对停顿时间比较敏感,我们可以使用ParNew+CMS(XX:+UseParNewGC -XX:+UseConcMarkSweepGC)

CMS参数设置

对于老年代CMS的参数如何设置我们可以思考下,首先我们想下当前这个系统有哪些对象可能会长期存活躲过5次以上minor gc最终进入老年代。无非就是那些Spring容器里的Bean,线程池对象,一些初始化缓存数据对象等,这些加起来充其量也就几十MB。还有就是某次minor gc完了之后还有超过一两百M的对象存活,那么就会直接进入老年代。

此时突然某一秒瞬间要处理五六百单,那么每秒生成的对象可能有一百多M,再加上整个系统可能压力剧增,一个订单要好几秒才能处理完,下一秒可能又有很多订单过来。我们可以估算下大概每隔五六分钟出现一次这样的情况,那么大概半小时到一小时之间就可能因为老年代满了触发一次Full GC,Full GC的触发条件还有我们之前说过的老年代空间分配担保机制,历次的minor gc挪动到老年代的对象大小肯定是非常小的,所以几乎不会在minor gc触发之前由于老年代空间分配担保失败而产生full gc,其实在半小时后发生full gc,这时候已经过了抢购的最高峰期,后续可能几小时才做一次FullGC。

对于碎片整理,因为都是1小时或几小时才做一次FullGC,是可以每做完一次就开始碎片整理,或者两到三次之后再做一次也行。

综上,只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值。

每秒几十万并发的系统如何优化JVM

Kafka类似的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息很正常的,一般来说部署kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理。

这里就涉及到一个问题了,我们以前常说的对于eden区的young gc是很快的,这种情况下它的执行还会很快吗? 很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消息,显然是不行的。

那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。

G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。

实战:通过GC日志优化线上JVM参数(针对tomcat服务器)

tomcat服务器在JavaEE项目中使用率非常高,所以在生产环境对tomcat的优化也变得非常重要了。 对于tomcat的优化,主要是从2个方面入手

  • tomcat自身的配置

  • tomcat所运行的jvm虚拟机的调优。

前置条件:使用jmeter工具压测并输出结果

JVM+调优(整合拉勾、图灵等课程)插图

平均响应时间,单位毫秒: 1845 错误率: 0.18% 吞吐量:459.5/sec

如下:tomcat所运行的jvm虚拟机的调优 — 开始

1.设置并行垃圾回收器

#年轻代、老年代均使用并行收集器,初始堆内存64M,最大堆内存512M
JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms64m -Xmx512m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"

查看gc日志文件 将gc.log文件上传到gceasy.io查看gc中是否存在问题。

2.查看GC日志文件

将gc.log 文件上传到gceasy.io查看gc中是否存在问题

问题一: 年轻代和老年代空间大小分配不合理, 具体如下图

JVM+调优(整合拉勾、图灵等课程)插图

问题二: 0-100ms时间范围内执行MinorGC 太多

JVM+调优(整合拉勾、图灵等课程)插图

从图中可以看到0-100 100-200毫秒的gc 发生了9次和4次, 时间短,频率高,说明年轻代空间分配不合理,我们可以尝试多给年轻代分配空间,减少Minor GC 频率, 降低Pause GC事件,提高吞吐量.

问题三:下图中我们也能看到问题, Minor GC 发生了 14 次, Full GC 发生了2次。 Pause time 事件也较长。

JVM+调优(整合拉勾、图灵等课程)插图

3.调整年轻代大小

对比下之前的配置,将初始堆大小,年轻代大小均进行提升

JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms512m -Xmx512m -XX:NewRatio=2 -
XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -
XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"

4.调整后查看吞吐量

JVM+调优(整合拉勾、图灵等课程)插图

5.调整后再次校验GC日志

JVM+调优(整合拉勾、图灵等课程)插图

6.设置G1垃圾收集器

理论上而言,设置为G1垃圾收集器,性能是会提升的。但是会受制于多方面的影响,也不一定绝对有提升。

#设置使用G1垃圾收集器最大停顿时间100毫秒,初始堆内存512m,最大堆内存512m
JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms512m -Xmx512m -XX:+PrintGCDetails -
XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"

JVM+调优(整合拉勾、图灵等课程)插图

JVM相关重要概念

安全点与安全区域

安全点

就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。 这些特定的安全点位置主要有以下几种:

  1. 方法返回之前

  2. 调用某个方法之后

  3. 抛出异常的位置

  4. 循环的末尾

大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和安全点是重合的。

安全区域又是什么?

Safe Point 是对正在执行的线程设定的。 如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。 因此 JVM 引入了 Safe Region。 Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

方法区详解(永久代or元空间)

方法区是定义,是接口。实现有:

  • 永久代(1.7)

  • 元空间(1.8)

区别如下

  • 存储位置不同:

    • 永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的

    • 元空间属于本地内存

  • 存储内容不同:

    • 永久代用来存放:类的元数据信息、静态变量以及常量池等。

    • 元空间:只有 类的元信息 而静态变量和常量池等并入堆中(jdk1.8)。相当于原来的永久代中的数据,被分到元空间和堆内存。

为什么要废弃永久代,引入元空间?

相比于之前的永久代划分,Oracle为什么要做这样的改进呢?

  • 不好指定大小:在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出。

  • 历史版本原因:移除永久代是为融合HotSpot VM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代

  • GC上考虑:永久代会为GC带来不必要的复杂度,并且回收效率偏低。

废除永久代的好处
  • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在 时的内存溢出错误。

  • 将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。

  • 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。

常量池详解

常量池vs运行时常量池

字节码文件中,内部包含了常量池(也叫静态常量池)

方法区中,内部包含了运行时常量池

  • 常量池:存放编译期间生成的各种字面量与符号引用

  • 运行时常量池:常量池表在运行时的表现形式

编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。

可理解为字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时常量池中取数据,被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。

它们不是一个概念,存放的位置是不同的。一个在字节码文件中,一个在方法区中。

静态常量池

也叫Class常量池,可以理解为是Class文件中的资源仓库。 Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)。

当然我们一般不会去人工解析这种16进制的字节码文件,我们一般可以通过javap命令生成更可读的JVM字节码指令文件:javap -v Math.class

主要存放两大类常量:字面量和符号引用。

字面量

字面量就是指由字母、数字等构成的字符串或者数值常量;只可以右值出现,右值是指等号右边的值

符号引用

符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:

  • 类和接口的全限定名

  • 字段的名称和描述符

  • 方法的名称和描述符

运行时常量池

  • 当类加载到内存中后,JVM就会将class常量池中的内容存放到运行时常量池中;运行时常量池里面存储的主要是编译期间生成的字面量、符号引用等等。

  • 类加载在链接环节的解析过程,会符号引用转换成直接引用(静态链接)。此处得到的直接引用也是放到运行时常量池中的。

  • 运行期间可以动态放入新的常量。

字符串常量池

也可以理解成运行时常量池分出来的一部分。类加载到内存的时候,字符串会存到字符串常量池里面。利用池的概念,避免大量频繁创建字符串。 字符串常量池的设计思想:

  1. 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能

  2. JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化

  • 为字符串开辟一个字符串常量池,类似于缓存区

  • 创建字符串常量时,首先查询字符串常量池是否存在该字符串

  • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

字符串常量池位置变迁

  • Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池

  • Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里

  • Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里。

字符串常量池设计原理
String对象创建个数

  字符串常量池底层是hotspot的C++实现的,底层类似一个 HashTable, 保存的本质上是字符串对象的引用。看一道比较常见的面试题,下面的代码创建了多少个 String 对象?

String str1 = "abc";  // 在常量池中
// 当直接赋值时,字符串“abc”会被存储在常量池中,只有1份。如果常量池中已经存在了“abc”,直接将引用赋值给str1;如果常量池中没有“abc”,那么创建一个对象,并将引用赋值给str1。

String str2 = new String("abc"); // 在堆上+常量池上
// 答案是1个或2个。当JVM遇到上述代码时,会先检索常量池中是否存在“abc”,如果不存在“abc”这个字符串,则会先在常量池中创建这个一个字符串。
// 然后再执行new操作,会在堆内存中创建一个存储“abc”的String对象,对象的引用赋值给str2。此过程创建了2个对象。当然,如果检索常量池时发现已经存在了对应的字符串,那么只会在堆内创建一个新的String对象,此过程只创建了1个对象。

new String(“xx”) 必定会创建一个string对象new出来的都是在Heap中,堆是用于存储对象的。

1.6

JVM+调优(整合拉勾、图灵等课程)插图

1.7

JVM+调优(整合拉勾、图灵等课程)插图

intern() 方法 1.6与1.7的区别(字符串池在不在堆里)
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
 
System.out.println(s1 == s2);
 // 在 JDK 1.6 下输出是 false,创建了 6 个对象
 // 在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象
 // 当然我们这里没有考虑GC,但这些对象确实存在或存在过

为什么输出会有这些变化呢?主要还是字符串池从永久代中脱离、移入堆区的原因, intern() 方法也相应发生了变化: 1、在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例。如下 6个对象

JVM+调优(整合拉勾、图灵等课程)插图

2、在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。如下5个对象

JVM+调优(整合拉勾、图灵等课程)插图

  

由上面两个图,也不难理解为什么 JDK 1.6 字符串池溢出会抛出 OutOfMemoryError: PermGen space ,而在JDK 1.7 及以上版本抛出 OutOfMemoryError: Java heap space 。

String常量池问题的几个例子
1 String s0="zhuge";
2 String s1="zhuge";
3 String s2="zhu" + "ge";
4 System.out.println( s0==s1 ); //true
5 System.out.println( s0==s2 ); //true

分析:因为例子中的 s0和s1中的”zhuge”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true; 而”zhu”和”ge”也都是字符串常量,当一个字 符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所 以s2也同样在编译期就被优化为一个字符串常量”zhuge”,所以s2也是常量池中” zhuge”的一个引用。所以我们得出s0==s1==s2;

1 String s0="zhuge";
2 String s1=new String("zhuge");
3 String s2="zhu" + new String("ge");
4 System.out.println( s0==s1 );  // false
5 System.out.println( s0==s2 );  // false
6 System.out.println( s1==s2 );  // false s1和s2都是新的引用,指向了同一个字符串

分析:用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。s0还是常量池 中”zhuge”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象”zhuge”的引用,s2因为有后半部分 new String(”ge”)所以也无法在编译期确定,所以也是一个新创建对象”zhuge”的引用;明白了这些也就知道为何得出此结果了。

课上问题

  1. 类加载机制和热部署?

  2. sync锁升级?

    1. AQS;自旋锁

  3. 四种引用类型平时用吗?refecence

  4. CMS

  5. 跟可达算法是思想,三色标记是实现

  6. 默认新老是1:2;可调为2:1

  7. 本地方法可以通过引擎去掉,为啥

  8. 如何找到跟可达的跟:找整个内存

    1. 跟可达节点都是变量

思维导图创建

  1. 突出标题和模块,字不要太多

  2. 难点重点处贴附件图

  1. 架构

  2. 对象创建

  3. 类加载机制

  4. jvm内存模型

  5. gc

  6. 双亲委派机制打破:tomcat;spi

  7. 调优,实战

老年代空间不足时,会尝试触发MinorGC. 如果空间还是不足,则触发Major GC

如果Major GC , 内存仍然不足,则报错OOM

Major GC的速度比Minor GC慢10倍以上.

要把知识点动起来,一味的罗列没有意义。jvm整个就是:

  1. 编译出.class文件

  2. 加载文件到内存里,引出内存

  3. 内存的分配,各个区

  4. 然后内存满了,如何回收垃圾,聊聊垃圾回收算法,映像深的

  5. 最后当然是调优

积极发言,争取发言,大家钱花的都是一样的,多说就是赚到

方法区1.7到1.8的变更

  • 从linux进程出发,原来的

  • 即时编译

不同的区使用什么样的算法,对应什么样的收集器?

元空间是否会存在内存溢出?

  • 操作系统内存用完了一样会内存溢出

这个问题涉及到linux原理,就是一个linux进程

本地方法栈存在linux堆栈哪里?虚拟机栈存在linux的堆栈哪里?

垃圾标记的触发时机?

在所有线程都达到安全点,安全区域这个安全区域在并发编程里也是一个基础知识点。锁升级过程中,持有锁的线程会在运行到安全区域时CAS锁有没有升级。

新生代内存不足,一定会进行minor GC吗?

空间担保机制,可能会进行一次Full GC

阿尔萨斯直接替换class文件,为什么会直接顶掉? 高皓

使用阿尔萨斯的类加载器顶替原来的APP类加载,自定义实现加载规则,在实现方法里判断类是不是被顶替的,如果是就打破双亲委派不是就走双亲。

Spring boot里的热部署也是,监听到targe下编译的文件被改后,触发自定义的类加载去重新加载该类。

打破双亲委派就是需要一个类被反复加载。那如果直接重复加载,而原来的类什么时候卸载呢?

第四组演讲需要对照PPT看

常量池主要存放两大类常量:字面量、符号引用。

字面量比较接近于Java语言层面的常量概念。

符号引用属于编译原理概念,主要包括几种常量:

•类和接口的全限定名

•字段的名称和描述符

•方法的名称和描述符

因为class文件不会存储字段、方法的最终内存布置信息,所以要存储相关的符号引用在动态链接时,找到相应的内存地址。

栈针,局部变量需要多少内存,都是在加载是分配好的,类加载后会放在常量池里

JVM+调优(整合拉勾、图灵等课程)插图

总结

训练学习思维

·

知识梳理目标:

  1. 关键指标明确:

  2. 知识架构体系的分工:充分讨论

学习方法和学习方式

  1. 先收集领域内知识

  2. 找主线的两种方式:1.知识分层;2. 问题驱动,核心问题引出细节问题

    1. 如并发编程。核心是解决线程安全

    • 硬件,多核多级缓存

    • 诞生各种协议和概念:如原子性,可见性,顺序一直性

    • 然后就是锁

    • 最后是java的实现AQS

    • 线程池原理,JUC

    • 最最后是实战解决生产问题

  3. 最后是系统的压缩的输出

把知识变的立体感,所谓的立体感就是非知识点的填鸭

  • 以虚拟机为例,就是静态和动态两块理解

解耦的方式就是加一层中间层

本站无任何商业行为
个人在线分享 » JVM+调优(整合拉勾、图灵等课程)
E-->