Java~阅读《深入理解Java虚拟机》知识点总结

文章目录

前言

  • 对于一名Java程序员,非常有必要去了解JVM虚拟机,只有在了解其内部机制,其内存分布,其运行机制,才有可能在实际的项目中去做出合适的量级优化
  • 近段时间阅读了一下《深入理解Java虚拟机》这本书,有机会也建议学Java的同志们去学习一下,此篇文章仅作为我的学习笔记,有机会大家还是阅读原书收获会更大

第一部分(Java程序内部机制)

  • 我们都知道Java的所有程序在运行前都会进行加载, 而加载其实就是把存放于存储中的编译好的字节码文件加载到JVM内存运行环境中

1.1Java运行时一个类是什么时候被加载的?

  • 一个类在什么时候开始被加载, 并没有进行强制约束,交给了虚拟机自己去自由实现, 而虚拟机的本质原则就是按需加载, 即在需要使用这个类的时候就去加载这个类,比如:
  1. 初始化这个类的实例
  2. 反射这个类
  3. 使用这个类的类对象
  4. 使用这个类的类加载器
  5. 使用这个类的静态变量
  6. 使用这个类的内部类
  • -XX:+TraceClassLoading这个参数可以帮助我们打印在运行java程序的时候帮我们显示加载了什么类
    请添加图片描述
    请添加图片描述
    请添加图片描述

1.2JVM一个类的加载过程?

  • 一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段:
    1、加载(Loading)
    2、验证(Verification)
    3、准备(Preparation)
    4、解析(Resolution)
    5、初始化(Initialization)
    6、使用(Using)
    7、卸载(Unloading)
    其中验证、准备、解析三个阶段统称为连接(Linking);
  • 加载:classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间,此阶段我们程序员可以干预,我们可以自定义类加载器来实现类的加载;
  • 验证:验证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全;
  • 准备:类变量赋默认初始值,int为0,long为0L,boolean为false,引用类型为null;常量赋正式值;
  • 解析:把符号引用翻译为直接引用, 把一个对象的class文件读进来变成一个java.long.class对象,会有一个随机的符合引用,需要在元空间给他一个属于他的直接引用;
  • 初始化:当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化… 那么这些都会触发类的初始化;
  • 使用:使用这个类;
  • 卸载
  1. 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
  2. 加载该类的ClassLoader已经被GC;
  3. 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;
  4. 该类的内部类没有被使用

1.3一个类被初始化的过程的注意点

  • 卸载一个类的条件是很苛刻的,很少去卸载一个类,而且在最终卸载的时候还会有一步操作即使检查这个类有没有实现Finalize接口,如果此时有一个强引用给到这个对象, 那这个对象还是不会被回收
  • 类的初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码;
  • 进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,才真正初始化类变量和其他资源;

1.4继承时父子类的初始化顺序是怎样的?

父类–静态变量
父类–静态初始化块
子类–静态变量
子类–静态初始化块
父类–变量
父类–初始化块
父类–构造器
子类–变量
子类–初始化块
子类–构造器

1.5究竟什么是类加载器?

在类“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的“代码”被称为“类加载器”(Class Loader),这个动作是可以自定义实现的;

  • 所以类加载器也是一段程序,一段代码,可能是java实现的,也可能是c/c++实现的,核心作用就是将class文件,读取到JRE环境的jvm内存中

1.6JVM有哪些类加载器?

站在Java虚拟机的角度来看,只存在两种不同的类加载器:
1、启动类加载器(Bootstrap ClassLoader),使用C++语言实现,是虚拟机自身的一部分;
2、其他所有的类加载器,由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader;
站在Java开发者的角度来看,自JDK 1.2开始,Java一直保持着三层类加载器架构;
在这里插入图片描述

1.7JVM中不同的类加载器加载哪些文件?

1、启动类加载器(Bootstrap ClassLoader):(根的类加载器)C++语言实现的
<JAVA_HOME>\jre\lib\rt.jar,resources.jar、charsets.jar
被-Xbootclasspath参数所指定的路径中存放的类库;

2、扩展类加载器(Extension ClassLoader):
sun.misc.Launcher$ExtClassLoader,
<JAVA_HOME>\jre\lib\ext,
被java.ext.dirs系统变量所指定的路径中所有的类库;

3、应用程序类加载器(Application ClassLoader):系统的类加载器
sun.misc.Launcher$AppClassLoader
加载用户类路径(ClassPath)上所有的类库;

在这里插入图片描述
通过这行代码就知道这个类是谁加载的,但是根的类加载器是c++实现的,我们打印不出来,所以显示null

1.8JVM三层类加载器之间的关系是继承吗?

我们先看几个图
请添加图片描述
请添加图片描述
请添加图片描述
显然,他们不是继承关系, 但是都继承了ClassLoader, 也就是说,我们自己的类加载器也是需要继承classLoader的

1.9你了解JVM类加载的双亲委派模型吗?

在这里插入图片描述
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,爆出异常由下一层类加载器捕获,然后才会尝试自己去加载;

1.10JDK为什么要设计双亲委派模型,有什么好处?

1、确保安全,避免Java核心类库被修改;
2、避免重复加载;
3、保证类的唯一性;
如果你写一个jaa.lang.String的类去运行,发现会抛出如下异常;
在这里插入图片描述

1.11可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?

可以!
想要打破这种模型,那么就自定义一个类加载器继承原有类加载器,重写其中的loadClass方法,使其不进行双亲委派即可;

1.12如何自定义自己的类加载器?

1、继承ClassLoader
2、覆盖findClass(String name)方法 或者 loadClass() 方法;
findClass(String name)方法 不会打破双亲委派;
loadClass() 方法 可以打破双亲委派;

请添加图片描述
请添加图片描述
请添加图片描述

1.13ClassLoader中的loadClass()、findClass()、defineClass()区别?

  • loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;
  • findClass() 根据名称或位置加载.class字节码;
  • definclass() 把字节码转化为java.lang.Class;

1、当我们想要自定义一个类加载器的时候,并且想破坏双亲委派模型时,我们会重写loadClass()方法;

2、如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?可以可以重写findClass方法(),findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法,这个方法只抛出了一个异常,没有默认实现;
JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中;
所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass()中实现你自己的加载逻辑即可;

加载一个类采用Class.forName()和ClassLoader有什么区别?

  • class.forName去加载会进行初始化
  • 通过classLoader去加载不会进行初始化操作,然后通过返回的class进行.newInstance的时候才会进行初始化

1.14你了解Tomcat 的类加载机制吗?

在这里插入图片描述
可以看到,在原来的Java的类加载机制基础上,Tomcat新增了3个基础类加载器和每个Web应用的类加载器+JSP类加载器;

3个基础类加载器在 conf/catalina.properties 中进行配置:
common.loader=" c a t a l i n a . b a s e / l i b " , " {catalina.base}/lib"," catalina.base/lib","{catalina.base}/lib/.jar"," c a t a l i n a . h o m e / l i b " , " {catalina.home}/lib"," catalina.home/lib","{catalina.home}/lib/.jar"
server.loader=
shared.loader=

Tomcat自定义了WebAppClassLoader类加载器,打破了双亲委派的机制,即如果收到类加载的请求,首先会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类, 我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat打破了这个规则,重写了loadClass方法,我们可以看到WebAppClassLoader类中重写了loadClass方法;

1.15为什么Tomcat要破坏双亲委派模型?

Tomcat是web容器,那么一个web容器可能需要部署多个应用程序;
1、部署在同一个Tomcat上的两个Web应用所使用的Java类库要相互隔离;
2、部署在同一个Tomcat上的两个Web应用所使用的Java类库要互相共享;
3、保证Tomcat服务器自身的安全不受部署的Web应用程序影响;
4、需要支持JSP页面的热部署和热加载;

  • 比如使用的spring的版本不一样,如果不隔离,可能会导致后面部署的项目不能正常跑起来, 如果相同版本相同对象不共享,会导致oom

1.16有没有听说过热加载和热部署,如何自己实现一个热加载?

热加载 是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于Java的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境

热部署 是指可以在不重启服务的情况下重新部署整个项目,比如Tomcat热部署就是在程序运行时,如果我们修改了War包中的内容,那么Tomcat就会删除之前的War包解压的文件夹,重新解压新的War包生成新的文件夹;
1、热加载是在运行时重新加载class,后台会启动一个线程不断检测你的class是否发生改变;
2、热部署是在运行时重新部署整个项目,耗时相对较高;
如何实现热加载呢?
在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的class文件,然后重新进行加载;
1、实现自己的类加载器;
2、从自己的类加载器中加载要热加载的类;
3、不断轮训要热加载的类class文件是否有更新,如果有更新,重新加载;

第二部分(JVM内存相关)

  • 此部分我将讲述JVM内存相关知识和垃圾回收相关知识

2.1Java代码到底是如何运行起来的?

其实运行起来一个Java程序,都是通过C:*\dev\Java\jdk1.8.0_251\bin\java 启动一个JVM虚拟机 (jvm进程,也就是一个jvm虚拟机),在虚拟机里面运行Mall.class字节码文件;
请添加图片描述

2.2来,画一下JVM整个运行时内存原理图?

在这里插入图片描述

2.3请介绍一下JVM的内存结构划分?

加载进来的.class字节码文件、代码执行创建的对象、代码执行调用方法,方法中有变量等数据需要一个地方存放,所以JVM划分出了几个区域,用于存放这些信息
在这里插入图片描述
在JDK1.8之前,元空间就是原来的方法区(永久代);

2.4JVM哪些区域是线程私有的,哪些区域是线程共享的?

1、堆、元空间(方法区)是线程共享的;
2、其他区域是线程私有的;

2.5从JVM角度剖析如下程序代码如何执行?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.6JVM运行时数据区 程序计数器 的特点及作用?

1、程序计数器是一块较小的内存空间,几乎可以忽略;
2、是当前线程所执行的字节码的行号指示器;
3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;
4、该区域是“线程私有”的内存,每个线程独立存储;
5、该区域不存在OutOfMemoryError;
6、无GC回收;

2.7JVM运行时数据区 虚拟机栈的特点及作用?

1、线程私有;
2、方法执行会创建栈帧,存储局部变量表等信息;
3、方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)
4、栈深度大于虚拟机所允许StackOverflowError;
5、栈需扩展而无法申请空间OutOfMemoryError(比较少见);hotspot虚拟机没有;
6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;
7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;
8、随线程而生,随线程而灭;
9、该区域不会有GC回收;
如图紫色区为一个虚拟机栈里的栈帧:
请添加图片描述

  • 操作数栈就是做一些简单的基本运算,将基本运算进行一次压栈进行运算
  • 动态链接一般就是指向的元空间的内存地址,方便找到下一次调用的方法

2.8JVM运行时数据区 本地方法栈的特点及作用?

1、与虚拟机栈基本类似;
2、区别在于本地方法栈为Native方法服务;
3、HotSpot虚拟机将虚拟机栈和本地方法栈合并;
4、有StackOverflowError和OutOfMemoryError(比较少见);这个oom就是创建了太多的线程,没有空间去创建虚拟机栈
5、随线程而生,随线程而灭;
6、GC不会回收该区域;
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;

2.9JVM运行时数据区 Java堆的特点及作用?

1、线程共享的一块区域;
2、虚拟机启动时创建;
3、虚拟机所管理的内存中最大的一块区域;
4、存放所有实例对象或数组;
5、GC垃圾收集器的主要管理区域;
6、可分为新生代、老年代;
在这里插入图片描述
新生代和老年代比例是1:2,新生代的三个区域是8:1:1
7、新生代更细化可分为Eden、From Survivor、To Survivor,Eden:Survivor = 8:1:1
8、可通过-Xmx、-Xms调节堆大小;
9、无法再扩展java.lang.OutOfMemoryError: Java heap space
10、如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率;

  • 多个线程往堆中放对象的时候就会产生竞争,所以会有个分配缓冲区来降低竞争,提高对象分配时的效率
    请添加图片描述

2.10JVM中对象如何在堆内存分配?

1、指针碰撞(Bump The Pointer):内存规整的情况下;

2、空闲列表(Free List):内存不规整的情况下;
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定;
因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;
而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存;

3、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):对象创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;
那么解决方案有两种:
(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;
(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;
-XX:TLABSize=512k 设置大小;

2.11JVM堆内存中的对象布局?

在 HotSpot 虚拟机中,一个对象的存储结构分为3块区域:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding);

对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit,官方称为 ‘Mark Word’;
在这里插入图片描述
第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;

实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);

对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot虚拟机,任何对象的大小都是8字节的整数倍;

2.12JVM什么情况下会发生堆内存溢出?

Java堆中用于储存对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径
来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制后就会产生内存溢出;

  • 比如运行下面代码:
    请添加图片描述
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/dev/heapdump.hprof
  • 上面俩个参数表示发生oom的时候在指定路径下生成dump文件
    请添加图片描述
  • 当堆溢出的时候,将内存快照文件导出到某个路径, 然后依据MAT就可以分析这个dump文件
    请添加图片描述
    请添加图片描述
  • 可以看到哪些数据类型占用内存多少,我们可以对某些业务做出优化

2.13JVM如何判断对象可以被回收?

在JVM堆里面存放着所有的Java对象,垃圾收集器在对堆进行回收前,首先要确定这些对象之中哪些还“存活”着,哪些已经“死去”;
Java通过 **可达性分析(Reachability Analysis)**算法 来判定对象是否存活的;
该算法的基本思路:通过一系列称为“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收器回收;
在这里插入图片描述对象object 5、object 6、object 7虽然有关联,但它们到GC Roots是不可达的,所以它们将会被判定为可回收的对象;

哪些对象可以作为GC Roots呢?
1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等所引用的对象;
2、方法区/元空间中的类静态属性引用的对象;
3、方法区/元空间中的常量引用的对象;
4、在本地方法栈中JNI(即通常所说的Native方法)引用的对象;
5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;
6、所有被同步锁(synchronized关键字)持有的对象;
7、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;
8、其他可能临时性加入的对象;

2.14谈谈Java中不同的引用类型?

Java里有不同的引用类型,分别是强引用、软引用、弱引用 和 虚引用;
强引用:Object object = new Object();
软引用:SoftReference 内存充足时不回收,内存不足时则回收;MyBatis用的软引用做的缓存
弱引用:WeakReference 不管内存是否充足,只要GC一运行就会回收该引用对象;弱引用 用的很少,比如threadLocal的数据就是用的弱引用,或者我们缓存对象我们感官他不常用就可以使用这个弱引用
虚引用:PhantomReference这个其实暂时忽略也行,因为很少用,它形同虚设,就像没有引用一样,其作用就是该引用对象被GC回收时候触发一个系统通知,或者触发进一步的处理;
请添加图片描述

2.15JVM堆内存分代模型?

JVM堆内存的分代模型:年轻代、老年代;
大部分对象朝生夕死,少数对象长期存活;
新生代占1/3,老年代占2/3, 在新生代里Eden和s1和s2的比例是8:1:1
在这里插入图片描述

2.16请介绍一下JVM堆中新生代的垃圾回收过程?

JVM里垃圾回收针对的是 新生代,老年代,还有元空间/方法区(永久代)
不会针对方法的栈帧进行回收,方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉,也就是虚拟机栈不存在垃圾回收

代码里创建出来的对象,一般就是两种:
1、一种是短期存活的,分配在Java堆内存之后,迅速使用完就会被垃圾回收;
2、一种是长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用;
第一种短期存活的对象,是在Java堆内存的新生代里分配;
第二种长期存活的对象,通过在新生代S0区和S1区来回被垃圾回收15次后,进入Java堆内存的老年代中,这里的15次,我们也称为对象的年龄,即对象的年龄为15岁;
java -XX:+PrintFlagsFinal 打印jvm默认参数值, 分代年龄默认值是15;

2.17JVM对象动态年龄判断是怎么回事?

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold=15才能晋升老年代;
结论–>动态年龄判断:Survivor区的对象年龄从小到大进行累加,当累加到X年龄(某个年龄)时占用空间的总和大于50%(可以使用-XX:TargetSurvivorRatio=? 来设置保留多少空闲空间,默认值是50),那么比X年龄大的对象都会晋升到老年代;
1、Survivor区分布如下图:
在这里插入图片描述
1-3岁总和小于50%
2、此时新生代GC后,有6%的对象进入Survivor区,Survivor区分布如下图:
在这里插入图片描述
这时从1岁加到4岁时,总和51% 大于50%,但此时没有大于四岁的对象,即没有对象晋升

3、又经过一次新生代GC后,有40%的对象进入Survivor区,Survivor区分布如下图:
在这里插入图片描述
Survivor区的对象年龄从小到大进行累加,当累加到 3 年龄时的总和大于50%,那么比3大的都会晋升到老年代,即4岁的20%、5岁的20%晋升到老年代;

2.18什么是老年代空间分配担保机制

  • 就是将数据往老年代放的时候,空间够不够怎么保证。
    新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?
    1、执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;
    2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;
    3、如果执行Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC”;
    所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC
    4、如果Full GC之后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致“OOM”内存溢出 ;
    请添加图片描述

2.19什么情况下对象会进入老年代?

1、躲过15次GC之后进入老年代,可通过JVM参数“-XX:MaxTenuringThreshold”来设置年龄,默认为15岁;
2、动态对象年龄判断;
3、老年代空间担保机制;
4、大对象直接进入老年代;

大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合,大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们,而当复制对象时,大对象又会引起高额的内存复制开销,为了避免新生代里出现那些大对象,然后屡次躲过GC而进行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代;

我们可以通过JVM参数“-XX:PretenureSizeThreshold”设置多大的对象直接进入老年代,该值为字节数,比如“1048576”字节就是1MB,该参数表示如果创建一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放入老年代,而不会经过新生代;
-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,其他新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑 ParNew+CMS的收集器组合;

2.20JVM运行时数据区 元空间的特点及作用?

1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代;
2、元空间与Java堆类似,是线程共享的内存区域;
3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据;
4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;

-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;
6、元空间内存不足时,将抛出OutOfMemoryError;

2.21JVM本机直接内存的特点及作用?

1、直接内存(Direct Memory)不属于JVM运行时数据区,是本机直接物理内存;
2、像在JDK 1.4中新加入了NIO(New Input/Output)类,一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;
3、可能导致OutOfMemoryError异常出现; 使用Netty如果出现这个异常,就有可能是因为使用本地直接内存不够了

2.22JVM本机直接内存溢出问题?

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,该参数表示设置新I / O(java.nio程序包)直接缓冲区分配的最大总大小(以字节为单位);默认情况下,大小设置为0,这意味着JVM自动为NIO直接缓冲区分配选择大小;
请添加图片描述

由直接内存导致的内存溢出,无法生成Heap Dump文件,如果程序中直接或间接使用了NIO技术,那就可以重点考虑检查一下直接内存方面的原因;

2.23说几个与JVM内存相关的核心参数?

-Xms Java堆内存的大小;
-Xmx Java堆内存的最大大小;
-Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小;
-XX:MetaspaceSize 元空间大小;
-XX:MaxMetaspaceSize 元空间最大大小;
-Xss 每个线程的栈内存大小;
-XX:SurvivorRatio=8 设置eden区 和survivor 区大小的比例,默认是8:1:1;
-XX:MaxTenuringThreshold=5 年龄阈值;
-XX:+UseConcMarkSweepGC 指定CMS垃圾收集器;
-XX:+UseG1GC 指定使用G1垃圾回收器

–查看默认的堆大小及默认的垃圾收集器

java -XX:+PrintCommandLineFlags -version

第三部分(垃圾回收)

  • 这也是Java非常突出的一个优势就是开发人员不需要管理内存,一般情况下不会有内存泄漏的发生,就是因为Java有原生的垃圾回收器, 有不需要的对象,GC会帮助我们进行回收

3.1堆为什么要分成年轻代和老年代?

在这里插入图片描述
因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法;
年轻代的对象,它的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法;
老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法 ;
所以需要分成两个区域来放不同的对象;

1、绝大多数对象都是朝生夕灭的;
如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保留少量存活对象,而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;

2、熬过越多次垃圾收集的对象就越难以回收;
如果是需要长期存活的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用;

3、JVM划分出新生代、老年代之后,垃圾收集器可以每次只回收其中某一个或者某些部分的区域 ,同时也有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;
Minor GC/Young GC :新生代收集
Major GC/Old GC:老年代收集
Full GC:整堆收集,收集整个Java堆和元空间/方法区的垃圾收集;
Mixed GC:混合收集,收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;

4、针对不同的区域对象存亡特征采用不同的垃圾收集算法:
(1)复制算法
(2)标记-清除算法
(3)标记-整理算法

3.2JVM堆的年轻代为什么要有两个Survivor区?

1、如果没有Survivor区会怎么样?
此时每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major Gc/Full GC(通常伴随着MinorGC),比较耗时,所以必须有Survivor区;

2、如果只有1个Survivor区会怎么样?
刚刚创建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中存活的对象就会被移动到Survivor区,下一次Eden满了的时候,此时进行Minor GC,Eden和Survivor各有一些存活对象,因为只有一个Survivor,所以Eden区第二次GC发现的存活对象也是放入唯一的一个Survivor区域中,但此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化问题,并且由于不连续的空间会导致再分配大对象的时候,由于没有连续的空间来分配,会导致提前垃圾回收;
如果将Survivor中的所有存活对象进行整理消除碎片,然后将所有的存活对象放入其中,这样做会降低效率;
如果把两个区域中的所有存活对象都复制转移到一个完全独立的空间中,也就是第二块Survivor中,这样就可以留出一块完全空着的Eden和Survivor了,下次GC的时候再重复这个流程,所以我们便要有两个Survivor区;

  1. 如果不分的话,每次GC的区域太大,导致STW时间过长
  2. 再就是为了避免触发fullGC或者oldGC,让其来回复制达到一定年龄再进去老年代,以此来让一个对象在年轻代的停留时间加长,避免尽快进入到年代而导致老年代空间不够从而触发fullGC或者oldGC
  3. 再就是可以合理使用复制算法,来将另外一个区域的所有对象都删除,从而没有使用整理算法也能避免内存碎片。

3.3Eden区与Survivor区的空间大小比值为什么默认是8:1:1?

一个eden区 ,新生代对象出生的地方;
两个survivor区,一个用来保存上次新生代GC存活下来的对象,还有一个空着,在新生代GC时把eden+survivor中存活对象复制到这个空的survivor中;
统计和经验表明,90%的对象朝生夕死存活时间极短,每次gc会有90%对象被回收,剩下的10%要预留一个survivor空间去保存;

3.4请介绍下JVM中的垃圾回收算法?

在这里插入图片描述

3.4.3标记-清除算法

标记-清除算法是最基础的收集算法,后续的很多垃圾回收算法是基于该算法而发展出来的,它分为‘ 标记 ’和‘ 清除 ’两个阶段;
1、标记
标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记出所有存活的对象,在标记完成后,统一回收所有未被标记的对象,标记过程就是对象是否属于垃圾的判定过程,基于可达性分析算法判断对象是否可以回收;
2、清除
标记后,对所有被标记的对象进行回收;
在这里插入图片描述
优点:基于最基础的可达性分析算法,实现简单,后续的收集算法都是基于这种思想实现的;
缺点
1、执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
2、内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集;

3.4.4复制算法

复制算法是标记-复制算法的简称,将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉;
在这里插入图片描述
优点:实现简单,效率高,解决了标记-清除算法导致的内存碎片问题;
缺点
1、代价太大,将可分配内存缩小了一半,空间浪费太多了;
2、对象存活率较高时就要进行较多的复制操作,效率将会降低;
一般虚拟机都会采用该算法来回收新生代,但是JVM对复制算法进行了改进,JVM并没有按照1:1的比例来划分新生代的内存空间,因为通过大量的统计和研究表明,90%以上的对象都是朝生夕死的,所以JVM把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间,HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有另外一个Survivor空间即10%的新生代会被“浪费”;
当然,90%的对象可被回收仅仅是大部分情况下,我们无法百分百保证每次回收都只有不多于10%的对象存活,因此JVM还有一个空间担保机制的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其它内存区域(实际上就是老年代)进行空间分配担保(Handle Promotion,也就是冒险Minor GC一下);

3.4.5标记-整理算法

标记-整理算法是根据老年代的特点而产生的;
1、标记
标记过程与上面的标记-清理算法一致,也是基于可达性分析算法进行标记;
2、整理
和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理,让存活对象都向一端移动,然后直接清理掉边界以外的内存;
标记-清除算法不移动存活对象,导致有大量不连续空间,即内存碎片,而老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新所有引用这些对象的引用,这是一种比较耗时的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿我们也称为“Stop The World”即STW;
但是即便是移动存活对象是耗时的操作,但是如果不这么做,那么在充满内存碎片的空间中分配对象,又影响了对象的分配和访问的效率,所以JVM权衡两者之后,还是采用了移动存活对象的方式,也就是对内存进行了整理;
另外像cms垃圾收集器,平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间,所以像基于标记-清除算法的CMS收集器面临空间碎片过多时就会进行一次整理;
在这里插入图片描述
优点:
1、不会像复制算法那样划分两个区域,提高了空间利用率;
2、不会产生不连续的内存碎片;
缺点
效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率变低;

标记整理的标记需要STW,整理也需要STW,但是标记复制的话的复制过程不需要STW,标记清除的话,清除因为他是把有用的已经复制过去了,清除直接清除一整片区域,所以清除的STW时间也不长。所以标记整理的耗时较多.

3.4.6分代收集算法

现在一般虚拟机的垃圾收集都是采用“ 分代收集 ”算法;
根据对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,JVM根据各个年代的特点采用不同的收集算法;
新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;
老年代中,因为对象存活率较高,采用标记-清理、标记-整理算法来进行回收;

3.5请介绍一下JVM垃圾收集器?

在这里插入图片描述
如上图,一共有7种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用,垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器;
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
垃圾收集器的最前沿成果:ZGC 和 Shenandoah

3.5.3Serial收集器

新生代收集器,最早的收集器,单线程的,采用标记清除和复制算法,收集时需暂停用户线程的工作,所以有卡顿现象,
效率不高,致使java语言的开发团队一直在改进垃圾收集器的算法和实现,但Serial收集器简单,不会有线程切换的开销,是Client模式下默认的垃圾收集器,-client, -server;

client和server模式是可以通过参数设置的,JDK1.8都是server模式

参数: -XX:+UseSerialGC

在这里插入图片描述

3.5.4ParNew收集器

它是新生代收集器,就是Serial收集器的多线程版本,采用标记清除和复制算法,大部分基本一样,单CPU下,ParNew还需要切换线程,可能还不如Serial
Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,后者CMS收集老年代,
“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代垃圾收集器;
“-XX:+UseParNewGC”:强制指定使用ParNew;
“-XX:ParallelGCThreads=2”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
在这里插入图片描述

3.5.5Parallel Scavenge收集器

这个是JVM默认的垃圾收集器

简称Parallel,它是新生代收集器,基于标记清除和复制算法并行的多线程收集器(与ParNew收集器类似),侧重于达到一个可控的吞吐量,虚拟机运行100分钟,垃圾收集花1分钟,则吞吐量为99%,有时候我们也把该垃圾收集器叫吞吐量垃圾收集器或者是吞吐量优先的垃圾收集器;而且这个垃圾收集器是jvm默认的新生代的垃圾收集器;
它提供一个参数设置吞吐量:
-XX:MaxGCPauseMillis 该参数设置大于0的毫秒数,每次GC的时间将尽量保持不超过设置的值,但是这个值也不是设置得越小就越好,GC暂停时间越短,那么GC的次数会变得更频繁;

-XX:+UseAdaptiveSizePolicy 自适应新生代大小策略,默认这个参数是开启的,当这个参数被开启之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间获得最大的吞吐量,这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics);
如果我们不知道怎么对jvm调优,我们可以使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(最大停顿时间)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成,自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性;
参数:-XX:+UseParallelGC 指定使用Parallel Scavenge垃圾收集器

Parallel Scavenge垃圾收集器中的Ergonomics负责自动的调节gc暂停时间和吞吐量之间的平衡,自动优化虚拟机的性能;

查看GC日志

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/dev/gc.log
请添加图片描述

3.5.6Serial Old收集器

它是Serial收集器的老年代版本,同Serial一样,单线程,可在Client模式下使用,也可在Server模式下使用,采用标记-整理算法,Serial Old收集器也可以作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用;

就是CMS是多线程并发的垃圾收集器,但是一旦并发收集失败,就会使用SerialOld这个收集器,线上环境一般不会使用这个收集器

在这里插入图片描述

3.5.7Parallel Old收集器

是Parallel Scavenge的老年代版本,多线程,标记整理算法,它是在jdk1.6开始才提供;
在注重吞吐量和CPU资源的情况下, Parallel Scavenge新生代+ Parallel Old老年代是一个很好的搭配;
参数:-XX:+UseParallelOldGC 指定使用Parallel Old收集器;
在这里插入图片描述

3.5.8CMS收集器

CMS全称Concurrent Mark Sweep,是一款老年代的垃圾收集器,它是追求最短回收停顿时间为目标的收集器互联网B/S结构的服务器端特别适合此收集器
我们知道垃圾回收会带来Stop the World(stw)的问题,会导致系统卡死时间过长,很多响应无法处理,所以CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式(并发)来处理的,基于标记-清除和标记-整理算法
参数:-XX:+UseConcMarkSweepGC 指定使用CMS垃圾收集器;

CMS垃圾收集器的运作过程,分为4个阶段:
1、初始标记(stw,标记一下GC Roots能直接关联到的对象,那么这些对象也就是需要存活的对象,速度很快);
2、并发标记(不会stw,追踪GC Roots的整个链路,从GC Roots的直接关联对象开始遍历整个对象引用链路,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行)
3、重新标记(stw,修正并发标记期间,因用户程序继续运行而导致标记产生变化的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短,它其实就是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快的)
4、并发清除(不会stw,清理删除掉标记阶段判断的已经死亡的对象,这个阶段其实是很耗时的,但由于不需要移动存活对象,并且这个阶段也是与用户线程同时并发执行的)
其中初始标记和重新标记需要暂停用户线程(Stop The World),其它阶段都是并发执行,所以总体上暂停时间更短;

CMS垃圾收集器的缺点:
1、并发收集会占用CPU资源,特别是cpu数量小的服务器下,会占用用户线程,导致性能下降,CMS默认启动的回收线程数是(处理器核心数量 + 3)/ 4;
2、会产生浮动垃圾,因为你并发清除的时候用户线程可能还在产生垃圾,这些垃圾没有清除,而且你不能让老年代填满了再清除,你要给用户线程留一定空间,所以jdk1.5默认是老年代68%了就触发回收,jdk1.6则提升到92%;
通过-XX:CMSInitiatingOccupancyFraction参数设置;
默认现在是取 -XX:CMSTriggerRatio 的值,默认是80%;
-XX:CMSInitiatingOccupancyFraction设置得更高,可以降低内存回收频率,获取更好的性能,但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次并发清除失败“并发失败”(Concurrent Mode Failure),如果预留老年代不够用户线程使用,则启用Serial Old收集,这就会暂停用户线程,导致性能下降;
3、CMS基于标记-清除算法,清理后会产生碎片空间,空间碎片过多时,将会导致大对象无法分配,往往会出现老年代还有很多剩余空间,但没有足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC,CMS垃圾收集器有一个**-XX:+UseCMSCompactAtFullCollection开关参数(默认是开启的),用于在CMS收集器进行Full GC时开启内存碎片的整理过程,由于这个内存整理必须移动存活对象,整理过程是无法并发的,就导致停顿时间变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction**,即CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)
在这里插入图片描述

3.5.9G1收集器

3.5.9.1G1垃圾收集器的基本介绍

G1全称Garbage First,G1垃圾回收器可以同时回收新生代和老年代,不需要两个垃圾回收器配合起来使用;
JDK 9发布时,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,如果对JDK 9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃

3.5.9.2G1垃圾收集器的基本原理

G1是一款可以让我们设置垃圾回收的预期停顿时间的垃圾收集器,设置参数是**-XX:MaxGCPauseMillis**,默认值是200ms;

其实我们对内存合理分配,优化jvm参数,就是为了尽可能减少新生代(Minor GC),或者是整个老年代(Major GC),或者是整个Java堆(Full GC),尽量减少GC带来的系统停顿,避免影响系统处理请求,G1可以指定垃圾回收导致的系统停顿时间不能超过多久,不管垃圾的多与少,垃圾回收的时间都不要超过我们设置的值(并不是绝对的),G1全权给你负责,保证达到这个目标,这相当于我们就可以直接控制垃圾回收对系统性能的影响了;
所以G1垃圾收集器是尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象,这就是G1垃圾收集器的核心原理

3.5.9.3G1垃圾收集器如何做到可预测的停顿时间?

1、这与G1垃圾收集器独特的设计有关,它最大的特点就是把Java整个堆内存拆分为多个大小相等的Region:区域、分区
在这里插入图片描述
2、G1它会追踪每个Region的回收价值,即它会计算每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?
3、G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为垃圾回收的最小单元,即每次可以选择一部分Region进行收集,避免在整个Java堆中进行全区域的垃圾收集,让G1收集器去跟踪各个Region里面的垃圾的“回收价值”,然后根据用户设定的收集停顿时间(使用参数**-XX:MaxGCPauseMillis**指定,默认值是200毫秒),然后在后台维护一个优先级列表,优先处理回收价值大的那些Region,这也是“Garbage First”名字的由来,这种使用Region划分堆内存空间,基于回收价值的回收方式,保证了G1收集器在有限的时间内尽可能收集更多的垃圾;
比如:G1通过追踪发现,1个Region中的垃圾对象有10MB,回收它需要耗费500毫秒,另一个Region中的垃圾对象有20MB,回收它需要耗费100毫秒,那么G1垃圾收集器基于回收价值计算会选择回收20MB只需要100毫秒的Region;

3.5.9.4G1垃圾收集器是否还有年代的划分?

G1也有新生代和老年代的概念,但只不过是逻辑上的概念,也就是说一个Region此时是属于新生代的Eden空间,过一会儿可能就属于老年代空间,也就是一个Region在运行过程中动态地扮演着新生代的Eden空间、Survivor空间,或者老年代空间,每个Region并不是固定属于某一个空间,另外新生代、老年代也不一定是连续空间,可能是分开的;

刚开始Region是空的,可能谁都不属于,然后系统创建对象就分配给了新生代,这个Region被新生代对象放满之后,后续垃圾回收了这个Region,然后下一次同一个Region可能又被分配了老年代,用来放老年代的长时间需要存活的对象,所以Region随时会属于新生代也会属于老年代;
新生代和老年代各自的内存区域在不停地变动,由G1自动控制,也就是Region动态分配给新生代或者老年代,按需分配,然后触发垃圾回收的时候,可以根据设定的预期系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收,保证GC对系统停顿的影响在可控范围内,同时还能尽可能回收最多的对象;

3.5.9.5G1垃圾收集器中的大对象?

Region中有一类特殊的Humongous区域,专门用来存储大对象;
G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,每个Region的大小可以通过参数**-XX:G1HeapRegionSize设定,取值范围为1MB~32MB**,而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待;

3.5.9.6G1垃圾收集器内存大小如何设置?

每个region = 1m ~32m,最多有2048个region;
G1对应的是一大堆的Region内存区域,最多可以有2048个Region,比如说堆大小是4G(4096MB),那么每个Region的大小就是2MB,Region的取值范围是1M-32M,可以通过参数“-XX:G1HeapRegionSize”指定每个Region是多少兆;
比如说堆大小是4G(4096MB),刚开始,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent"来设置新生代初始占比,一般默认值即可,因为在系统运行中,JVM会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”设置,并且一旦Region进行了垃圾回收,此时新生代的Region数量就会减少,这些都是动态的;
新生代 :老年代 = 60% :40%

G1大多数情况默认值就行

3.5.9.7G1垃圾收集器新生代还有Eden和Survivor吗?

G1垃圾收集器依然有新生代、老年代的概念,新生代里依然有Eden和Survivor的划分,G1是从CMS发展过来的,以后是要完全取代CMS垃圾收取器的,从jdk9开始G1已经是默认的垃圾收集器,之前的很多技术原理在G1中依然可用,我们知道新生代有一个参数“-XX:SurvivorRatio=8”,所以G1还是可以区分出来属于新生代的Region里哪些属于Eden,哪些属于Survivor;

比如年代占比8:1:1这些是没有变的

比如新生代刚开始初始化时有100个Region,那么可能有80个Region是Eden,10个Region分别是两个Survivor,所以G1中依然有Eden和Survivor的概念,它们会各自占据不同的Region;
只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加;
5% – 60%

3.5.9.8G1垃圾收集器的新生代垃圾回收

G1的新生代也有Eden和Survivor,其触发垃圾回收的机制也是类似的,随着不停在新生代Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%
假设堆 4G,最大2048个region,每个region为2M,新生代最大60%=2.4G;
一旦新生代达到了设定的占据堆内存的最大大小60%,按照上面的数据大概就是有1200个Region,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区满了,此时触发新生代的GC,G1就会依然用复制算法来进行垃圾回收,进入一个“Stop the World”状态,然后把Eden对应的Region中的存活对象复制到S0对应的Region中,接着回收掉Eden对应的Region中的垃圾对象;

但这个过程与之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms,那么G1就会通过对每个Region追踪回收它需要多少时间,可以回收多少对象来选择回收一部分Region,保证GC停顿时间控制在指定范围内,尽可能多地回收对象;

所以G1的伊甸园区垃圾回收时会按域,将一个域的所有垃圾进行回收,而且不会有内存碎片。

3.5.9.9G1垃圾收集器的老年代垃圾回收

1、初始标记需要Stop the World,不过仅仅标记一下GC Roots直接能引用的对象,这个过程速度很快,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿;
2、并发标记不需要Stop the World,这个阶段会从GC Roots开始追踪所有的存活对象,初始标记阶段仅仅只是标记GC Roots直接关联的对象,而在并发标记阶段,就会进行GC Roots追踪,从这个GC Root对象直接关联的对象开始往下追踪,追踪全部的存活对象,这个阶段是很耗时的,但可以和系统程序并发运行,所以对系统程序的影响不大;
3、重新标记(最终标记),需要Stop the World,用户程序停止运行,最终标记一下有哪些存活对象,有哪些是垃圾对象;
4、筛选回收需要Stop the World,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的;

从整体上看,G1垃圾收集像是一种标记-整理算法,它不存在内存碎片问题,实际上它是一种复制算法,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 所以它并不是纯粹地追求低延迟,而是给它设定暂停目标,使其在延迟可控的情况下获得尽可能高的吞吐量;

3.5.9.10G1垃圾收集器的混合垃圾回收?

混合垃圾收集即mixed gc,它不是一个old gc,除了回收整个young region,还会回收一部分的old region,是回收一部分老年代,而不是全部老年代,可以选择部分old region进行收集,从而可以对垃圾回收的耗时时间进行控制;
G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,它的默认值是45%,即如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段;
比如堆内存有2048个Region,如果老年代占据了其中45%的Region,也就是接近1000个Region的时候,就会开始触发一个混合回收;

3.5.9.11G1回收失败时的Full GC

在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,把各个Region中存活的对象复制到其他空闲的Region中;
如果万一出现复制时没有空闲Region可以存放存活对象了,就会停止系统程序,然后采用单线程进行标记清除和压缩整理,空闲出来一批Region,这个过程很慢;
与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫暂停用户线程,导致Full GC而产生长时间Stop The World;
1、新生代垃圾回收
2、老年代垃圾回收
3、混合回收
4、Full GC

3.5.9.12查看G1日志信息

查看G1日志信息的JVM参数

-Xmx50m
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xl1oggc:D:/dev/gc.log
-XX:+UseG1GC
  • 第一行是虚拟机信息,第二行是堆信息
    请添加图片描述
  • 第三行是JVM堆运行信息的参数
    请添加图片描述
    请添加图片描述
  • 过程及结果
    请添加图片描述
    请添加图片描述
    请添加图片描述
    请添加图片描述
  • FULLGC回收,新生代老年代元空间都会回收
    请添加图片描述
3.5.9.13什么时候使用G1垃圾收集器?

1、针对大内存、多处理器的机器推荐采用G1垃圾收集器,比如堆大小至少6G或以上;
2、超过50%的堆空间都被活动数据占用;
3、在要求低延迟的场景,也就是GC导致的程序暂停时间要比较少,0.5-1秒之间;
4、对象在堆中分配频率或者年代升级频率变化比较大,防止高并发下应用雪崩现象的场景;

3.5.10ZGC收集器

ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器,它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器,在JDK 11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms;

第四部分(实战知识)

  • 这部分我讲介绍一些线上环境易出现的问题和优化案例

4.1什么是内存泄漏?什么是内存溢出?

内存溢出:OutOfMemory
它是指程序在申请内存时,没有足够的内存空间供其使用,抛出OutOfMemory错误;
比如申请了一个8MB空间,但是当前内存可用空间只有5MB,那么就是内存溢出;
即:OutOfMemoryError,是指没有空闲内存,垃圾收集器回收后也不能提供更多的内存空间;
在这里插入图片描述
内存泄露:Memory Leak
它是指程序运行后,没有释放所占用的内存空间,一次内存泄漏可能不会有很大的影响,但长时间的内存泄漏,堆积到一定程度就会产生内存溢出;
在这里插入图片描述
(1)单例对象,生命周期和应用程序一样长,如果单例对象持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会产生内存泄露;
(2)一些资源未闭也会导致内存泄漏,比如数据库连接,网络连接socket和IO流的连接都必须在finally中close,否则不能被回收的;

4.2线上环境的JVM都设置多大?

线上:4核8G机器;
JVM:栈、堆、元空间;
1、栈 1m,xss512k,一个线程是1m,一个线上项目Tomcat可能有300个线程,300m;
2、堆:大概把机器的一半内存给堆,4G(新生代、老年代);
CMS:1/3 、2/3 G1: 6:4
3、元空间: 一般512M肯定够了;
此时JVM参数如下:

-Xms4096M -Xmx4096M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:+UseG1GC

4.3线上Java服务器内存飙升怎么回事?

  • top命令查看CUP情况

VIRT:virtual memory usage 虚拟内存
1、进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
2、假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量

RES:resident memory usage 常驻内存
1、进程当前使用的内存大小,但不包括swap out
2、包含其他进程的共享
3、如果申请100m的内存,实际使用10m,它只增长10m,与VIRT相反
4、关于库占用内存的情况,它只统计加载的库文件所占内存大小

SHR:shared memory 共享内存
1、除了自身进程的共享内存,也包括其他进程的共享内存
2、虽然进程只使用了几个共享库的函数,但它包含了整个共享库的大小
3、计算某个进程所占的物理内存大小公式:RES – SHR
4、swap out后,它将会降下来

DATA
1、数据占用的内存。如果top没有显示,按f键可以显示出来。
2、真正的该程序要求的数据空间,是真正在运行中要使用的。

请添加图片描述

  • 查看指定java进程里占用内存的对象有哪些
jmap -histo 1576 > jvm.txt

(1576为pid,需换成对应自己的进程信息)

  • 然后使用sz把jvm.txt下载下来本地打开,或者在服务器上打开也行, 可以信息哪个全限定名下的对象比较多

请添加图片描述

  • 使用jmap -heap 1576 (1576为pid,需换成对应自己的进程信息)可以查看JVM配置信息新生代和老年代的使用情况
    请添加图片描述
    请添加图片描述
  • 使用jmap -dump:format=b,file=heap.hprof 1576(1576为pid,需换成对应自己的进程信息)将堆的暂存文件导出,会占用服务器内存资源,这个文件会很大,谨慎操作
  • 用MAT分析dump这个文件,IVM可以监测堆的情况
    请添加图片描述
    请添加图片描述
    请添加图片描述
    请添加图片描述

jmap -histo pid
jmap -heap pid
jmap -dump:format=b,file=heap.hprof pid

4.4线上Java项目CPU飙到100%怎么排查?

  • 使用top命令看到java进程CPU使用率很高
    请添加图片描述
  • 然后使用top -H -p pid 查看具体进程下线程的情况,可以看到具体是哪个线程的问题
    请添加图片描述
  • 然后使用jstack pid 就可以看到java进程具体的使用CPU情况,最后在里面找到对应线程的16进制的pid的信息,就可以定位到问题
    请添加图片描述
    请添加图片描述

top
top -H -p pid
printf ‘%x’ tid 8ef
jstack pid

4.5JVM堆溢出后,其他线程是否可以继续工作?

分情况,如果是局部变量导致的oom,那个这个栈帧销毁后,对应的对象也会释放,那么其他线程可以正常进行工作,但是如果导致oom的对象是单例的,全局存在的,或者被一些元空间对象的引用,那么其他的线程就不能正常运行了。

4.6线上Java项目OOM了,怎么回事?

这个得具体问题具体分析,比如使用jmap -histor 看那些对象占用内存比较大

设置printGcdetails 和 gcLog 和dump文件这些参数,然后使用MAT分析这个dump文件,可以看到具体问题

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/heap.hprof

4.8亿级流量的订单系统JVM优化

1、系统预估
每个用户访问20次/天,500万日活用户,流量 = 500万 * 20 = 10000万 = 1亿;
购买率15%,每人1单,每天订单量 = 500万 * 15% * 1 = 75万订单/天;
二八原则,下单集中在一天4小时内,洪峰下单量 = 75万 / 4小时 = 18.75万单/小时
= 18.75万单/60分/60秒 = 52单/秒;
52k2010=52*200=10400 = 10MB/秒,每秒52单基本上JVM没有压力;
2、流量洪峰场景
普通4核8G服务器,一台机器抗300-400并发下单请求比较合理;
583000 / 300 = 1943台机器
300KB * 20 * 10 = 60MB的内存开销,一秒后60MB对象就成为垃圾;
3、内存分配
4核8G的机器,JVM给4G,剩下几个G会留给操作系统;
堆3G(新生代1.5G,老年代1.5G)
栈1MB,JVM里大概会有300-500个线程,大概300-500MB;
元空间/永久代512MB;

-Xms3072M -Xmx3072M -Xmn1536M -Xss1M
-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M” 

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/heap.hprof
上面三个是堆的日志信息,辅助我们去优化,下面俩个是当发生oom时候获取转存文件,辅助我们排查问题

元空间也可以不设置,默认使用系统内存,但是一般给512MB也够用了.

4、内存占用动态推算
每秒处理300个订单,占据新生代60MB的内存空间,新生代总共有1.5G的内存空间;
1.5G * 1024MB / 60MB = 25秒 新生代Eden占满,触发Minor GC;
一般情况下一次可以回收掉90%的新生代对象,存活对象 = 1.5G * 1024MB * 10% = 150MB;
如果“-XX:SurvivorRatio”参数默认值为8,那么:
新生代Eden=1.2GB、S0 = 150MB、S1 = 150MB
5、如何调优?
(1)1次Minor GC后,可能Survivor不足或者触发动态年龄判断,对象进入老年代,明显是Survivor空间不足;
新生代调整为2G,老年代为1G,此时Eden:1.6G,每个Survivor:200MB;
解决可能的Survivor不足或者触发动态年龄判断,降低新生代对象进入老年代的概率;
此时JVM参数:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M” 

(2)一般系统里的@Service、@Controller之类的注解需要长期存活,这些对象一般也不会很多,可能几十兆,应该让它们尽快进入老年代;

对 对象年龄的一个调整

此时JVM参数:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5

(3)一般情况下,大对象可能需要长期存活和使用,让它直接进入老年代;(根据项目实际情况来确定)

比如缓存的一些大的map对象,还有一些工具类

此时JVM参数如下:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M” 

(4)指定合适的垃圾回收器;
此时JVM参数 :没有使用G1垃圾收集器;

G1一般在大堆大内存的时候使用,或者有强制地停顿要求的时候才会使用这个垃圾回收器

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC” 

(5)大概每隔几分钟Minor GC之后有大概200MB左右对象进入老年代,推算可能差不多1小时后,才会有接近1GB的对象进入老年代,触发Full GC,然后高峰期一过,可能需要几个小时才会一次Full GC;

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:-UseCompressedClassPointers -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSCompactAtFullCollection 
-XX:CMSFullGCsBeforeCompaction=0

最后俩个参数表示每次发生FULLGC后是否需要进行内存整理,此时表示需要,并且是每次进行FULLGC后都进行整理

(6)优化思路
1、尽可能让对象在新生代里分配和回收,避免对象频繁进入老年代导致老年代频繁垃圾回收;
2、给系统充足的内存空间,避免新生代频繁的垃圾回收;
3、指定合适的垃圾收集器;

一般25秒左右进行一次年轻代GC,一小时左右进行一次FULLGC是一个比较好的情况

  • 34
    点赞
  • 84
    收藏
    觉得还不错? 一键收藏
  • 25
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值