JVM基础(一)

JVM 前言

聊聊常见的面试题

1、请你谈谈你对JVM的理解? java8 虚拟机有什么更新?

答:JVM(java虚拟机)是JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。

java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。

java8对jvm的更新:

​ 撤销了永久带,引入了元空间:

​ 在HotSpot虚拟机中,jkd1.6时,设计团队把方法区设计为永久带,这样GC工作区域就可以扩展至方法区。这种策略可以可以避免为方法区单独设计垃圾回收机制,但是坏处就是,方法区的回收条件十分苛刻,而且回收效果也不好。

​ 在jdk1.7版本,设计团队也意识到这个问题,但是只将方法区中的字符串常量池移除永久带。

​ 到了最新的jdk1.8版本,就不再有永久带这个概念,并且用元空间来代替原来的永久代

​ 元空间内的规则:元空间中类及其相关的元数据和类加载器生命周期一致,每个类加载器有专门的存储空间,不会单独回收某个类,位置也是固定的,但是当类加载器不再存活时会把它相关的空间全部移除。

2、什么是OOM,请你说说OOM产生的原因?如何分析?

答:OOM:OutOfMemory 也就是所谓的内存溢出,一句话形容,你的JVM内存就这么点,结果你拼命的往里面塞东西,结果内存塞不下了,不就直接溢出了嘛

1)什么是OOM?

​ OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。看下关于的官方说明: Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。

2)为什么会OOM?

为什么会没有内存了呢?原因不外乎有两点:

1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。

2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。

内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。

内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。

在之前没有垃圾自动回收的日子里,比如C语言和C++语言,我们必须亲自负责内存的申请与释放操作,如果申请了内存,用完后又忘记了释放,比如C++中的new了但是没有delete,那么就可能造成内存泄露。偶尔的内存泄露可能不会造成问题,而大量的内存泄露可能会导致内存溢出。

而在Java语言中,由于存在了垃圾自动回收机制,所以,我们一般不用去主动释放不用的对象所占的内存,也就是理论上来说,是不会存在“内存泄露”的。但是,如果编码不当,比如,将某个对象的引用放到了全局的Map中,虽然方法结束了,但是由于垃圾回收器会根据对象的引用情况来回收内存,导致该对象不能被及时的回收。如果该种情况出现次数多了,就会导致内存溢出,比如系统中经常使用的缓存机制。Java中的内存泄露,不同于C++中的忘了delete,往往是逻辑上的原因泄露。

3)OOM的类型

JVM内存模型:

按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:

  • 程序计数器:当前线程执行的字节码的行号指示器,线程私有
  • JAVA虚拟机栈:Java方法执行的内存模型,每个Java方法的执行对应着一个栈帧的进栈和出栈的操作。
  • 本地方法栈:类似“ JAVA虚拟机栈 ”,但是为native方法的运行提供内存环境。
  • JAVA堆:对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。
  • 方法区:用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Hotspot中的“永久代”。
  • 运行时常量池:方法区的一部分,存储常量信息,如各种字面量、符号引用等。
  • 直接内存:并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。

按照JVM规范,除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。

最常见的OOM情况有以下三种:

  • java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
  • java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
  • java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

如何分析:

​ 第一种:实时监控 通过监控数据,比如一些Zabbix,Open-Falcon等监控平台,然后就可以接入一些系统异常的监控和报警,可以设置一旦发生OOM异常,就直接报警给对应的开发人员等。

第二种:发生了异常之后,使用使用一些工具来查看;

​ 比如可以使用内存快照来查看

要dump堆的内存镜像,可以采用如下两种方式:

  • 设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息。不过该方法需要JDK5以上版本。
  • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
    -XX:+HeapDumpOnOutOfMemoryError 设置当首次遭遇内存溢出时导出此时堆中相关信息
    -XX:HeapDumpPath=/tmp/heapdump.hprof 指定导出堆信息时的路径或文件名
  • 使用JDK自带的jmap命令。"jmap -dump:format=b,file=heap.bin " 其中pid可以通过jps获取。

dump堆内存信息后,需要对dump出的文件进行分析,从而找到OOM的原因。常用的工具有:

  • mat: eclipse memory analyzer, 基于eclipse RCP的内存分析工具。详细信息参见:http://www.eclipse.org/mat/,推荐使用。
  • jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言OQL,分析相关的应用后,可以通过http://localhost:7000来访问分析结果。不推荐使用,因为在实际的排查过程中,一般是先在生产环境 dump出文件来,然后拉到自己的开发机器上分析,所以,不如采用高级的分析工具比如前面的mat来的高效。

3、JVM 的常用调优参数有哪些?

答:-Xms:初始堆大小,默认是物理内存的1/64。默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到–Xmx的最大限制。例如:-Xms 20m。
-Xmx:最大堆大小。默认是物理内存的1/4 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
-XX:NewSize=n:设置年轻代大小(初始值)。
-XX:MaxNewSize:设置年轻代最大值。
**-XX:NewRatio=n:**设置年轻代和年老代的比值。
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。
-XX:PermSize(1.8之后改为MetaspaceSize) 设置持久代(perm gen)初始值,默认是物理内存的1/64。
-XX:MaxPermSize=n:(1.8之后改为MaxMetaspaceSize)设置最大持久代大小。
-Xss:每个线程的堆栈大小。

-XX:+TraceClassLoading // 用于追踪类的加载信息并打印出来

4、内存快照抓取,如何分析,命令是什么?

答:命令:设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息。不过该方法需要JDK5以上版本。

分析:使用mat工具等进行分析。

5、堆里面分区:Eden、Survial(from to)、老年区

答:1.8之后 新生代(Eden,Servivor0/Servivor1) 老年代 元空间

6、GC垃圾收集算法有那个几个?谈谈利弊?

答:CMS,G1,ZGC

CMS是1.7版本主流的垃圾回收算法 ,G1是1.8版本的垃圾回收算法 ,ZGC是11版本的垃圾回收算法

BAT 难度的面试题

1、JVM 垃圾回收的时候如何确定垃圾,GC Roots?

答:没有被引用的对象就是垃圾

所谓"GC roots"或者说tracing GC的“根集合”就是一组必须活跃的引用
基本思路就是通过一系列名为“GC roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可达到的)对象就被判定为存活;没有没遍历到的就自然被判定为死亡。

GC roots对象

  1. 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(Native方法)引用的对象

2、-X、 -XX 参数你用过哪些?

-Xms:初始堆大小,默认是物理内存的1/64。默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到–Xmx的最大限制。例如:-Xms 20m。
-Xmx:最大堆大小。默认是物理内存的1/4 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
-XX:NewSize=n:设置年轻代大小(初始值)。
-XX:MaxNewSize:设置年轻代最大值。
**-XX:NewRatio=n:**设置年轻代和年老代的比值。
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。
-XX:PermSize(1.8之后改为MetaspaceSize) 设置持久代(perm gen)初始值,默认是物理内存的1/64。
-XX:MaxPermSize=n:(1.8之后改为MaxMetaspaceSize)设置最大持久代大小。
-Xss:每个线程的堆栈大小。

3、你常用的项目,发布后配置过JVM 调优参数吗?

配置过堆内存的大小,-Xms和-Xmx

4、软引用、强引用、弱引用、虚引用都是什么,请你谈谈?

答:

​ 一、引用强度排序

强引用>软引用>弱引用>虚引用

二、强引用:

\1. 正常创建的对象,只要引用存在,永远不会被GC回收,即使OOM

Object obj = new Object();

\2. 如果要中断强引用和某个对象的关联,为其赋值null,这样GC就会在合适的时候回收对象

\3. Vector类的clear()方法就是通过赋值null进行清除

三、软引用

\1. 内存溢出之前进行回收,GC时内存不足时回收,如果内存足够就不回收

\2. 使用场景:在内存足够的情况下进行缓存,提升速度,内存不足时JVM自动回收

Object obj = new Object();
SoftReference sf = new SoftReference(obj);
sf.get();//有时候会返回null

\3. 可以和引用队列ReferenceQueue联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列

四、弱引用

\1. 每次GC时回收,无论内存是否足够

\2. 使用场景:a. ThreadLocalMap防止内存泄漏 b. 监控对象是否将要被回收

Object obj = new Object();
WeakReference wf = new WeakReference(obj);
wf.get();//有时候会返回null

wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

System.gc(); //通知JVM的gc进行垃圾回收,但JVM不一定会立刻执行

wf.get();//此时会返回null

\3. 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中

五、虚引用

\1. 每次垃圾回收时都会被回收,主要用于监测对象是否已经从内存中删除

\2. 虚引用必须和引用队列关联使用, 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中

\3. 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

Object obj = new Object();
PhantomReference pf = new PhantomReference(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除

5、GC垃圾回收器和GC算法的关系?分别有哪些?

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。

串行垃圾回收器(Serial)
它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境

并行垃圾回收器(Parallel)
多个垃圾收集线程并行工作,此时用户线程是暂停的,适合于科学计算 / 大数据处理首台处理等弱交互场景

并发垃圾回收器(CMS)
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程互联网公司多用它,适合堆响应时间有要求的场景

G1垃圾回收器(G1)

G1 垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收

6、谈谈默认的垃圾回收器?

答:可以使用命令java -XX:+PrintCommandLineFlags -version来查看默认的垃圾回收器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-itzT9mE5-1583826736606)(D:\艾编程的高级课程\资料\3.8:深入理解JVM(上)\image-20200310123516615.png)]

7、G1垃圾回收器的特点?

答:1、并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
2、分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能单独管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象已获得更好的收集效果。
3、空间整合:与CMS的“标记-清除”算法不同,G1收集器从整体上看是基于“标记-整理”算法实现的,从局部(两个Region之间)上看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序的长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
4、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

8、OOM 你看过几种?

答:最常见的OOM情况有以下三种:

  • java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
  • java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
  • java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

JVM的位置

在这里插入图片描述

你刚买的电脑上java -version
在这里插入图片描述

JVM的体系架构图

在这里插入图片描述

课堂要求: 自己必须能够闭着眼睛想到这个图?

兴趣才是最好的老师

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

类加载的过程和类加载器详解

1、JVM在什么情况下会加载一个类

​ 其实类加载过程非常的琐碎复杂,但是对于我们平时从工作中实用的角度来说,主要是把握他的核心工作原理就可以。
一个类从加载到使用,一般会经历下面的这个过程:
​ 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
所以首先要搞明白的第一个问题,就是JVM在执行我们写好的代码的过程中,一般在什么情况下会去加载一个类呢?
也就是说,啥时候会从“.class”字节码文件中加载这个类到JVM内存里来。
其实答案非常简单,就是在你的代码中用到这个类的时候。
举个简单的例子,比如下面你有一个类(Kafka.class),里面有一个“main()”方法作为主入口。
那么一旦你的JVM进程启动之后,它一定会先把你的这个类(Kafka.cass)加载到内存里,然后从“main()”方法的入口代码开始执
行。

在这里插入图片描述

接着假设上面的代码中,出现了如下的这么一行代码:

在这里插入图片描述

这时可能大家就想了,你的代码中明显需要使用“ReplicaManager”这个类去实例化一个对象,此时必须得“ReplicaManager.class”字节码文件中的这个类加载到内存里来啊!是不是?
所以这个时候就会触发JVM通过类加载器,从“ReplicaManager.class”字节码文件中加载对应的类到内存里来使用,这样代码才能跑起来。
我们来看下面的图:

在这里插入图片描述

上面就是给大家举的一个例子,相信非常的通俗易懂。
简单概括一下:首先你的代码中包含“main()”方法的主类一定会在JVM进程启动之后被加载到内存,开始执行你的“main()”方法中
的代码
接着遇到你使用了别的类,比如“ReplicaManager”,此时就会从对应的“.class”字节码文件加载对应的类到内存里来。

2、从实用角度出发,来看看验证、准备和解析的过程

其实上面的类加载时机的问题,对于很多有经验的同学来说不是什么问题。
但是对于很多初学者来说,是一个非常重要的需要捋清的概念。我们重点讲解一下下面这三个概念

验证、准备、解析

(1)验证阶段
简单来说,这一步就是根据Java虚拟机规范,来校验你加载进来的“.class”文件中的内容,是否符合指定的规范。
这个相信很好理解,假如说,你的“.class”文件被人篡改了,里面的字节码压根儿不符合规范,那么JVM是没法去执行这个字节码
的!
所以把“.class”加载到内存里之后,必须先验证一下,校验他必须完全符合JVM规范,后续才能交给JVM来运行。
下面用一张图,展示了这个过程:

在这里插入图片描述

(2)准备阶段
这个阶段其实也很好理解,咱们都知道,我们写好的那些类,其实都有一些类变量
比如下面的这个“ReplicaManager”类:
在这里插入图片描述

假设你有这么一个“ReplicaManager”类,他的“ReplicaManager.class”文件内容刚刚被加载到内存之后,会进行验证,确认这个
字节码文件的内容是规范的
接着就会进行准备工作。
这个准备工作,其实就是给这个“ReplicaManager”类分配一定的内存空间
然后给他里面的类变量(也就是static修饰的变量)分配内存空间,来一个默认的初始值
比如上面的示例里,就会给“flushInterval”这个类变量分配内容空间,给一个“0”这个初始值。
整个过程,如下图所示:

在这里插入图片描述

(3)解析阶段
这个阶段干的事儿,实际上是把符号引用替换为直接引用的过程,其实这个部分的内容很复杂,涉及到JVM的底层。
所以针对这个阶段,现在不打算做过深的解读,因为从实用角度而言,对很多同学在工作中实践JVM技术其实也用不到,所以这里大家就暂时知道有这么一个阶段就可以了。
同样,我还是给大家画图展示一下:

在这里插入图片描述

(4)三个阶段的小结
其实这三个阶段里,最核心的大家务必关注的,就是“准备阶段”
因为这个阶段是给加载进来的类分配好了内存空间,类变量也分配好了内存空间,并且给了默认的初始值,这个概念,大家心里一定要有。

3、核心阶段:初始化

之前说过,在准备阶段时,就会把我们的“ReplicaManager”类给分配好内存空间
另外他的一个类变量“flushInterval”也会给一个默认的初始值“0”,那么接下来,在初始化阶段,就会正式执行我们的类初始化的
代码了。
那么什么是类初始化的代码呢?我们来看看下面这段代码:

在这里插入图片描述

大家可以看到,对于“flushInterval”这个类变量,我们是打算通过 Configuration.getInt(“replica.flush.interval”)这段代码
来获取一个值,并且赋值给他的
但是在准备阶段会执行这个赋值逻辑吗?
NO!在准备阶段,仅仅是给“flushInterval”类变量开辟一个内存空间,然后给个初始值“0”罢了。
那么这段赋值的代码什么时候执行呢?答案是在“初始化”阶段来执行。
在这个阶段,就会执行类的初始化代码,比如上面的 Configuration.getInt(“replica.flush.interval”) 代码就会在这
里执行,完成一个配置项的读取,然后赋值给这个类变量“flushInterval”。
另外比如下图的static静态代码块,也会在这个阶段来执行。

类似下面的代码语义,可以理解为类初始化的时候,调用“loadReplicaFromDish()”方法从磁盘中加载数据副本,并
且放在静态变量“replicas”中:

在这里插入图片描述

那么搞明白了类的初始化是什么,就得来看看类的初始化的规则了。
什么时候会初始化一个类?
一般来说有以下一些时机:比如“new ReplicaManager()”来实例化类的对象了,此时就会触发类的加载到初始化的全过程,把这个
类准备好,然后再实例化一个对象出来;
或者是包含“main()”方法的主类,必须是立马初始化的。
此外,这里还有一个非常重要的规则,就是如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类
比如下面的代码:
在这里插入图片描述

如果你要“new ReplicaManager()”初始化这个类的实例,那么会加载这个类,然后初始化这个类
但是初始化这个类之前,发现AbstractDataManager作为父类还没加载和初始化,那么必须先加载这个父类,并且初始化这个父类。这个规则,大家必须得牢记,再来一张图,借助图片来进行理解:

在这里插入图片描述

4、类加载器和双亲委派机制

现在相信大家都搞明白了整个类加载从触发时机到初始化的过程了,接着给大家说一下类加载器的概念
因为实现上述过程,那必须是依靠类加载器来实现的
那么Java里有哪些类加载器呢?简单来说有下面几种:
(1)启动类加载器
Bootstrap ClassLoader,他主要是负责加载我们在机器上安装的Java目录下的核心类的(jre/lib/rt.jar)
相信大家都知道,如果你要在一个机器上运行自己写好的Java系统,无论是windows笔记本,还是linux服务器,是不是都得装一下
JDK?
那么在你的Java安装目录下,就有一个“lib”目录,大家可以自己去找找看,这里就有Java最核心的一些类库,支撑你的Java系统的
运行。
所以一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库。
(2)扩展类加载器
Extension ClassLoader,这个类加载器其实也是类似的,就是你的Java安装目录下,有一个“lib\ext”目录
这里面有一些类,就是需要使用这个类加载器来加载的,支撑你的系统的运行。
那么你的JVM一旦启动,是不是也得从Java安装目录下,加载这个“lib\ext”目录中的类?
(3)应用程序类加载器
Application ClassLoader,这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类
其实你大致就理解为去加载你写好的Java代码吧,这个类加载器就负责加载你写好的那些类到内存里。
(4)自定义类加载器
除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类。
(5)双亲委派机制
JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一
层是自定义类加载器。

img

然后,基于这个亲子层级结构,就有一个双亲委派的机制
什么意思呢?
就是假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载
但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。
听完了上面一大堆绕口令,是不是很迷茫?别着急,咱们用一个例子来说明一下。
比如你的JVM现在需要加载“ReplicaManager”类,此时应用程序类加载器会问问自己的爸爸,也就是扩展类加载器,你能加载到这
个类吗?
然后 扩展类加载器直接问自己的爸爸,启动类加载器,你能加载到这个类吗?
启动类加载器心想,我在Java安装目录下,没找到这个类啊,自己找去!
然后,就下推加载权利给扩展类加载器这个儿子,结果扩展类加载器找了半天,也没找到自己负责的目录中有这个
类。
这时他很生气,说:明明就是你应用程序加载器自己负责的,你自己找去。
然后应用程序类加载器在自己负责的范围内,比如就是你写好的那个系统打包成的jar包吧,一下子发现,就在这里!
然后就自己把这个类加载到内存里去了。
这就是所谓的双亲委派模型:先找父亲去加载,不行的话再由儿子来加载。
这样的话,可以避免多层级的加载器结构重复加载某些类。
最后,给大家来一张图图,感受一下类加载器的双亲委派模型。

在这里插入图片描述

IDEA设置JVM运行参数

针对某个Application设置
1、Run–>Edit Configurations
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KHnwZ2E0-1583826736617)(D:\艾编程的高级课程\资料\3.8:深入理解JVM(上)\深入理解JVM.assets\20180715125641457.png)]

  2、选中要添加JVM参数的Application,然后在Configuration里面的VM options中输入想要添加的系统参数

在这里插入图片描述

针对所有的Application设置
1、找到IDEA安装目录中的bin目录

2、找到idea.exe.vmoptions文件

3、打开该文件编辑并保存。

在这里插入图片描述

优先级关系
代码中的配置>Application中的配置>全局配置

类加载器ClassLoader

我们首先来看看一个类加载到 JVM 的一个基本结构:
在这里插入图片描述

类的加载、链接和初始化(了解)

加载:查找并加载类的二进制数据

连接:

  • 验证:保证被加载的类的正确性;

  • 准备:给类静态变量分配内存空间,赋值一个默认的初始值;

  • 解析:把类中的符号引用转换为直接引用

    在把java编译为class文件的时候,虚拟机并不知道所引用的地址;助记符:符号引用!
    转为真正的直接引用,找到对应的直接地址!
    

初始化:给类的静态变量赋值正确的值;

public class Test{
    public static int a = 1;
}
// 1、加载   编译文件为 .class 文件,通过类加载,加载到JVM

// 2、连接   
	  验证(1)  保证Class类文件没有问题
      准备(2)int类型分配内存空间,a = 0;
      解析(3)  符号引用转换为直接引用

// 3、初始化
      经过这个阶段的解析,把1 赋值给 变量 a;

类的加载 static

package com.coding.classloader;
public class Demo02 {
    public static void main(String[] args) {
        System.out.println(MyChild1.str2);
    }
}
class MyParent1{
    public static String str = "hello,world";
    static {
        System.out.println("MyParent1 static");
    }
}

class MyChild1 extends MyParent1{
    public static String str2 = "hello,str2";
    static {
        System.out.println("MyChild1 static");
    }
}
//最后的输出结果:在VM options中加上 -XX:+TraceClassLoading 这样可以查看类的加载信息
[Loaded com.icoding.test.MyParent1 from file:/D:/code/IcodingDemo/target/classes/]
[Loaded com.icoding.test.MyChild1 from file:/D:/code/IcodingDemo/target/classes/]
MyParent1 static
MyChild1 static
hello,str2
由此可以看出,最先加载的是 MyParent1这个父类,然后再去加载的 MyChild1这个子类的,同时加载父类的时候因为static在类加载的时候就已经被加载进去了,所以会先打印出来父类的static静态块中的东西,然后同时子类在加载的时候也会加载static静态块中的东西,所以头两行是父类和子类中的静态块中的东西,然后才根据代码输出子类的str2中的内容。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D71Xy7vL-1583826736618)(D:\艾编程的高级课程\资料\3.8:深入理解JVM(上)\深入理解JVM.assets\设置jvm参数.png)]

// JVM 参数:
// -XX:+TraceClassLoading // 用于追踪类的加载信息并打印出来
// 分析项目启动为什么这么慢,快速定位自己的类有没有被加载!
// rt.jar jdk 出厂自带的,最高级别的类加载器要加载的!

常量 final 在编译期间可以确定的常量是在类加载之前就初始化好的,而在编译期间不能确定的常量,是在类加载之后才初始化的

package com.coding.classloader;

// 常量
public class Test {
    public static void main(String[] args) {
        System.out.println(MyParent02.str);
    }
}
class MyParent02{
    public static final String str = "hello world";

    static {
        System.out.println("MyParent02 static"); // 这句话会输出吗?
    }
}
//最后结果:
[Loaded sun.nio.cs.US_ASCII$Decoder from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded com.icoding.test.Test from file:/D:/code/IcodingDemo/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
hello,str2
可见,上面的那些类只有Test类执行了,其余的类都没有执行,但是str的值还是输出来了,由此可见,常量,必须是固定的那种常量是在类加载之前就已经初始化好了的,所以可以在不执行类的情况下输出来
package com.coding.classloader;

import java.util.UUID;

/**
 * 当一个常量的值并非编译期间可以确定的,那这个值就不会被方法调用类的常量池中!
 * 程序运行期间的时候,回主动使用常用所在的类
 */
public class Test {
    public static void main(String[] args) {
        System.out.println(MyParent04.str);
    }
}

class MyParent04{
	//    public static final String str = new String("aaaa"); 这句话的话,static静态块的值也会执行,因为这个也需要去加载才能知道值,这个值是需要加载开辟内存块的,而不是肉眼可见的叫做确定的值
    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("MyParent04 static"); // 这句话会输出吗?会的
    }

}
//最后输出结果:
[Loaded com.icoding.test.MyParent04 from file:/D:/code/IcodingDemo/target/classes/]
[Loaded java.util.UUID from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded java.util.UUID$Holder from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded java.util.Random from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded java.security.SecureRandom from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.security.jca.Providers from C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar]
......
MyParent04 static
fac95502-a09d-4bea-9d77-e3d9d881bc1c
由此可见,上面的程序运行的顺序是首先初始化MyParent04这个类,然后在加载UUID这个类,所以静态代码块中的程序是会被执行的,因为final修饰的变量在编译期间不能确定,需要加载完UUID这个类才能确定,所以程序会首先执行final变量所在的类,然后才加载UUID这个类,然后才确定常量的值

ClassLoader 分类

1、java虚拟机自带的加载器

  • BootStrapClassLoader 根加载器 (加载系统的包,JDK 核心库中的类 rt.jar)<java_home>/lib/rt.jar
  • Extension ClassLoader 扩展类加载器 (加载一些扩展jar包中的类)<java_home>/lib/ext/
  • Sys/ApplicationClassLoader 系统(应用类)加载器 (我们自己编写的类) classpath

2、用户自己定义的加载器

  • ClassLoader,只需要继承这个抽象类即可,自定义自己的类加载器
package com.coding.classloader;

// Demo01
public class Demo01 {

    public static void main(String[] args) {
        Object o = new Object(); // jdk 自带的
        Demo01 demo01 = new Demo01();  // 实例化一个自己定义的对象

        // null 在这里并不代表没有,只是Java触及不到!
       System.out.println("Object被"+o.getClass().getClassLoader()+"加载"); // null
        System.out.println("demo01被"+demo01.getClass().getClassLoader()+"加载"); // AppClassLoader
        System.out.println("demo01的父类被"+demo01.getClass().getClassLoader().getParent()+"加载"); // ExtClassLoader
        System.out.println("demo01的父类的父被"+demo01.getClass().getClassLoader().getParent().getParent()+"加载"); // null
  输出结果:
        Object被null加载  null表示的不是没有,是java触及不到的意思,因为Object类属于java.lang包下,这个包存在于rt.jar包里面,而这个jar包属于根加载器加载的范围,是系统本身自带的,而不是java的,所以java触及不到是null
		demo01被sun.misc.Launcher$AppClassLoader@18b4aac2加载 因为demp01是我们自己写的类,位于classpath下面的,所以是应用类加载器加载这个类
		demo01的父类被sun.misc.Launcher$ExtClassLoader@2f0e140b加载 这个表示的是demo01的加载器的父类加载器是哪一个了类,demo01被appclassLoader加载,那么这个的父加载器就是ExtensionClassLoader类了
		demo01的父类的父类被null加载  这个表示父类的父类就是根加载器,这个无法被java所感知到

        // 思考:为什么我们刚才自己定义的 java.lang.String 没有生效?

        // jvm 中有机制可以保护自己的安全;
        // 双亲委派机制 : 一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推
        // Demo01
        // AppClassLoader       03
        // ExtClassLoader       02
        // BootStrap (最顶层)   01  java.lang.String  rt.jar

        // 双亲委派机制 可以保护java的核心类不会被自己定义的类所替代
    }
}

双亲委派机制

双亲委派机制 可以保护java的核心类不会被自己定义的类所替代

一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RiSnJhlb-1583826736619)(D:\艾编程的高级课程\资料\3.8:深入理解JVM(上)\深入理解JVM.assets\20190822162827501.jpeg)]

// Demo01
// AppClassLoader       03
// ExtClassLoader       02
// BootStrap (最顶层)   01  java.lang.String  rt.jar

我们可以查看一个类就知道了这个顺序了 classLoader.java

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
1:先调用parent.loadClass(name, false)
2:如果父亲加载失败,则调用 loadClass(name, false) 方法 查找"当前类加载器"对应的"目录"来加载

Native方法

public class Test {
    public static void main(String[] args) {
        // java 真的可以开启线程吗?
        // private native void start0();
        new Thread().start();
    }
}

native : 只要是带了这个关键字的,说明 java的作用范围达不到,只能去调用底层 C 语言的库!

闲谈:Robot 按键精灵 ! 默认鼠标键盘操作!可以通过这个类实现一个简单的自动化脚本!

JNI : Java Native Interface (Java 本地方法接口)

为什么会有 Native 这个东西?

JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

程序计数器

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

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOxbOT7T-1583826736619)(深入理解JVM.assets/image-20200308211203137.png)]

分支、循环、跳转、异常处理!都需要依赖于程序计数器来完成!

bipush 将 int、float、String、常量值推送值栈顶;

istore 将一个数值从操作数栈存储到局部变量表;

iadd

imul

方法区渊源

Method Area 方法区 是 Java虚拟机规范中定义的运行是数据区域之一,和堆(heap)一样可以在线程之间共享!

天上飞的理念都会有落地的实现!

JDK1.7之前

永久代:用于存储一些虚拟机加载类信息,常量,字符串、静态变量等等。。。。这些东西都会放到永久代中;

永久代大小空间是有限的:如果满了 OutOfMemoryError:PermGen

JDK1.8之后

彻底将永久代移除 HotSpot jvm ,Java Heap 中或者 Metaspcace(Native Heap)元空间;

元空间就是方法区在 HotSpot jvm 的实现;

方法区重要就是来存:类信息,常量,字符串、静态变量、符号引用、方法代码。。。。。。

元空间和永久代,都是对JVM规范中方法区的实现。

元空间和永久代最大的区别:元空间并不在Java虚拟机中,使用的是本地内存!

-XX:MetasapceSize10m

对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

栈Stack

什么是栈?栈中存哪些东西?

我们的程序在运行时本质上就是启动线程在运行,比如main函数就是一个主线程。一个main主线程可以有很多的子线程。

线程在工作就是执行我们的各种方法。方法里面肯定有我们自己定义了一些局部的变量,比如我们在方法里面new了一个对象,对象肯定是放在堆里面的,但是对堆的引用我们就放在栈里面。那么栈就有问题了,我们一个程序有很多的线程,如果把所有的线程里面的变量存放在一起,肯定有会有变量是重复的,冲突。所有不能放在一起。所以栈里面是分线程来存放的。每一个线程都是自己的栈空间,线程私有的。堆是线程共享的。

栈里面有一个细节:

就是说一个栈空间是以不同的线程区分开来。每个线程有自己栈,每个线程里面又会执行很多的方法,每一个方法对应一个栈帧:

每个方法执行时都会创建一个栈帧(Stack Frame)用语存储局部变量表、操作数栈、动态链接、方法出口等信息。从下图从可以看到,每个线程在执行一个方法时,都意味着有一个栈帧在当前线程对应的栈帧中入栈和出栈。

Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。

Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个Java栈的模型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OaHXEupa-1583826736620)(D:\艾编程的高级课程\资料\3.8:深入理解JVM(上)\深入理解JVM.assets\20180516201213321.png)]

栈和队列

程序 = 数据结构 + 算法(IT行业流传的术语)

程序 = 业务逻辑 + 框架(真的IT人员做的)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YkI073hJ-1583826736620)(深入理解JVM.assets/image-20200308213052089.png)]

栈和队列都是基本的数据结构;

队列:FIFO(First Input First OutPut)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OB9EbPpr-1583826736620)(深入理解JVM.assets/image-20200308213347820.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xB8p58Se-1583826736621)(深入理解JVM.assets/image-20200308213533428.png)]

吃多了垃就是队列,喝多了吐就是栈

栈的存储和优势

栈就是管理程序运行的

存储一些基本类型的值,对象的引用,方法等…

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDiBPBZM-1583826736621)(深入理解JVM.assets/image-20200308213854553.png)]

存储:编译器可知的各种基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(引用指针,并非对象本身)

Car car = new CAR(); 其中car就存储在栈中,new Car()这个就放在堆中

优势:存取速度比堆快,仅次于寄存器。 栈是线程私有的

栈是java 方法执行的内存模型:是管理程序运行的

每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。

每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

(局部变量表:存放了编译器可知的各种基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(引用指针,并非对象本身),

其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间);

栈的垃圾和OOM

public class Demo01 {
    public static void main(String[] args) {
        a();
    }
    // main  a  a  a   a a  a a a  a a  a  满
    // Exception in thread "main" java.lang.StackOverflowError
    private static void a() {
        a();
    }
}

所以说,栈里面是一定不会存在垃圾回收的问题的,只要线程一旦结束,该栈就Over了。生命周期和线程一致;但是栈中仍然会有OOM异常的风险,因为一旦栈设置的太小,栈中调用的方法太多的话, 那么这时候栈同样也会出现java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小

Stack 原理

java栈的组成元素–栈帧

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tLbfV8cM-1583826736621)(深入理解JVM.assets/image-20200308214858179.png)]

栈(存什么)+ 堆 + 方法区的交互图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLnvKYCf-1583826736621)(深入理解JVM.assets/image-20200308215247211.png)]

我们的这个栈主要是 HotSpot (指针)

假设你的公司用的 JVM 不是HotSpot

问题:请你谈谈你认识几种 JVM? (3种)

  • SUN 公司 HotSpot (掌握即可)
  • BEA 公司 JRockit
  • IBM 公司 J9VM

堆(heap)

Java7之前:

Heap 堆,一个JVM实例中只存在一个堆,堆的内存大小是可以调节的。

可以存的内容:类、方法、常量、保存了类型引用的真实信息;

分为三个部分:

  • 新生区:Young (Eden-s0-s1)
  • 养老区:Old Tenure
  • 永久区:Perm

堆内存在逻辑上分为三个部分:新生、养老、永久(JDK1.8以后,叫元空间)

物理上只有 新生、养老;元空间在本地内存中,不在JVM中!

GC 垃圾回收主要是在 新生区和养老区,又分为 普通的GC 和 Full GC,如果堆满了,就会爆出 OutOfMemory;

新生区

新生区 就是一个类诞生、成长、消亡的地方!

新生区细分: Eden、s(from to),所有的类Eden被 new 出来的,慢慢的当 Eden 满了,程序还需要创建对象的时候,就会触发一次轻量级GC;清理完一次垃圾之后,会将活下来的对象,会放入幸存者区(),… 清理了 20次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收!这时候就会将这个对象送入养老区!运行了几个月之后,养老区满了,就会触发一次 Full GC;假设项目1年后,整个空间彻彻底底的满了,突然有一天系统 OOM,排除OOM问题,或者重启;

Sun HotSpot 虚拟机中,内存管理(分代管理机制:不同的区域使用不同的算法!)

Eden from to

99% 的对象在 Eden 都是临时对象;

养老区

15次都幸存下来的对象进入养老区,养老区满了之后,触发 Full GC

默认是15次,可以修改!

永久区(Perm)

放一些 JDK 自身携带的 Class、Interface的元数据;

几乎不会被垃圾回收的;

OutOfMemoryError:PermGen 在项目启动的时候永久代不够用了?加载大量的第三方包!

JDK1.6之前: 有永久代、常量池在方法区;

JDK1.7:有永久代、但是开始尝试去永久代,常量池在堆中;

JDK1.8 之后:永久代没有了,取而代之的是元空间;常量池在元空间中;

闲聊:方法区和堆一样,是共享的区域,是JVM 规范中的一个逻辑的部分,但是记住它的别名 非堆

元空间:它是本地内存!

堆内存调优(初识)

我的环境:HotSpot、JDK1.8;

测试一:

package com.coding.oom;


/**
 * 默认情况:
 * maxMemory : 1808.0MB (虚拟机试图使用的最大的内存量  一般是物理内存的 1/4)
 * totalMemory : 123.0MB (虚拟机试图默认的内存总量 一般是物理内存的 1/64)
 */
// 我们可以自定堆内存的总量
// -XX:+PrintGCDetails; // 输出详细的垃圾回收信息
// -Xmx: 最大分配内存; 1/4
// -Xms: 初始分配的内存大小; 1/64

// -Xmx1024m -Xms1024m -XX:+PrintGCDetails
public class Demo01 {
    public static void main(String[] args) {
        // 获取堆内存的初始大小和最大大小
        long maxMemory = Runtime.getRuntime().maxMemory();
        long totalMemory = Runtime.getRuntime().totalMemory();

        System.out.println("maxMemory="+maxMemory+"(字节)、"+(maxMemory/1024/(double)1024)+"MB");
        System.out.println("totalMemory="+totalMemory+"(字节)、"+(totalMemory/1024/(double)1024)+"MB");

    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8mmacW0-1583826736622)(深入理解JVM.assets/image-20200308223310875.png)]

测试二 OOM

package com.coding.oom;

import java.util.Random;

/*
 * -Xmx8m -Xms8m -XX:+PrintGCDetails
 *
 * 分析GC日志:
 *
 *  [Times: user=0.00 sys=0.00, real=0.00 secs]
 * 1、GC 类型  GC:普通的GC,Full GC :重GC
 * 2、1536K 执行 GC之前的大小
 * 3、504K  执行 GC之后的大小
 * 4、(2048K) young 的total大小
 * 5、0.0012643 secs 清理的时间
 * 6、user 总计GC所占用CPU的时间   sys OS调用等待的时间   real 应用暂停的时间
 *
 * GC :串行执行 STW(Stop The World)  并行执行   G1
*/

public class Demo02 {
    public static void main(String[] args) {
        System.gc(); // 手动唤醒GC(),等待cpu的调用
        String str = "ilovecoding";
        while (true){
            str += str
                    + new Random().nextInt(999999999)
                    + new Random().nextInt(999999999);
        }
        // 出现问题:java.lang.OutOfMemoryError: Java heap space
    }
}

恭喜你,入门JVM 了;

Dump内存快照

在java程序运行的时候,想测试运行的情况!

使用一些工具来查看;

1、Jconsole

2、idea debug

3、Eclipse(MAT插件)

4、IDEA(Jprofiler插件)

Jprofiler 插件

一款性能瓶颈分析插件

安装 Jprofiler

1、IDEA安装 JProfiler 插件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-knp8ZdoI-1583826736622)(深入理解JVM.assets/image-20200308225541172.png)]

2、window上安装 JProfiler (无脑下一步即可:注意路径中不能有中文和空格)

3、激活

注册码仅供大家参考

L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215
L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674

4、在IDEA 中绑定 JProfiler

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Ozsen2d-1583826736622)(深入理解JVM.assets/image-20200308230004229.png)]

快速体验

package com.coding.oom;

import java.util.ArrayList;
import java.util.List;

// -Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError
public class Demo03 {

    byte[] bytes = new byte[1*1024*1024]; // 1M

    public static void main(String[] args) throws InterruptedException {
        // 泛型:约束!
        List<Demo03> list = new ArrayList<Demo03>();

        int count = 0;

        try {
            // Error
            while (true){
                list.add(new Demo03());
                count = count + 1;
            }
        } catch (Throwable e) { // Throwable 或者 Error
            System.out.println("count="+count);
            e.printStackTrace();
        }
    }
}

分析dump出来的快照,查看异常对象;分析定位到具体的类和代码问题!

jvm启动参数设置OOM异常时,自动生成dump文件,然后使用jprofiler工具分析即可

设置如下2个参数即可:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
-XX:+HeapDumpOnOutOfMemoryError 设置当首次遭遇内存溢出时导出此时堆中相关信息
-XX:HeapDumpPath=/tmp/heapdump.hprof 指定导出堆信息时的路径或文件名

如:

java -Xms750m -Xmx750m -Xmn512m -Xss1024k -XX:MaxPermSize=128m
-XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/tmp/jvm.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -Dfile.encoding=utf-8 -jar /data/app/test.jar

-Xms750表示堆内存初始值为750M
-Xmx750m表示堆内存最大值750M
-Xmn512m 设置年轻代大小为512m。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-Xss1024k 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-XX:MaxPermSize=128m 设置永久代的大小,此属性在jdk8之后不再提供。
注:如果1.7之前为-XX:PermSize=64m -XX:MaxPermSize=128m ,1.8需要变成
-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m 否则起不来

-XX:+UseConcMarkSweepGC 设置年老代为并发收集。测试配置这个参数以后,参数-XX:NewRatio=4就失效了,所以,此时年轻代大小最好用-Xmn设置,因此这个参数不建议使用
-XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此参数设置运行次FullGC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection 打开对年老代的压缩。可能会影响性能,但是可以消除内存碎片
-XX:+PrintGC 每次GC时打印相关信息
-XX:+PrintGCDetails 每次GC时打印详细信息
-XX:+PrintGCTimeStamps 打印每次GC的时间戳
-Xloggc:/tmp/jvm.log 设置垃圾回收日志打印的文件,文件名称可以自定义
-XX:+HeapDumpOnOutOfMemoryError 设置当首次遭遇内存溢出时导出此时堆中相关信息
-XX:HeapDumpPath=/tmp/heapdump.hprof 指定导出堆信息时的路径或文件名

GC详解

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值