参考链接:https://www.cnblogs.com/yrxing/p/14464799.html
https://github.com/YaoChuanbiao/Java-Summarize
https://juejin.cn/post/7143112298628907044
JVM(Java Virtual Machine)是Java虚拟机的缩写,它是Java程序运行的环境和平台。
JVM是Java语言的核心组成部分,它负责解释和执行Java字节码(Bytecode)指令,将Java源代码编译的字节码转换为机器可以执行的指令。JVM提供了一种与具体硬件平台无关的执行环境,使得Java程序具有跨平台的特性,可以在不同的操作系统和硬件架构上运行。
JVM主要包含以下几个组件:
JVM的设计目标是提供一种可移植、高性能和安全的执行环境,使得开发人员能够编写一次代码,多平台运行。通过使用JVM,Java程序可以在不同的操作系统(如Windows、Linux、macOS等)和硬件架构(如x86、ARM等)上运行,无需针对每个平台编写不同的代码。这是Java语言的一个重要特性,也是其广泛应用于各种领域的原因之一。
类加载器用于Java类的加载。
分为启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。
1、启动类加载器(Bootstrap ClassLoader)(引导类加载器),加载java核心类库(<JAVA_HOME>/jre/lib/rt.jar),无法被java程序直接引用,是用C++编写的,用来加载其他的类加载器(类加载器本质就是类),是所有加载器的父类。
2、拓展类加载器(Extension ClassLoader),用来加载java的拓展库<JAVA_HOME>/jre/lib/ext
。
3、系统类加载器(System ClassLoader)(应用程序类加载器),用来加载类路径下的Java类
4、用户自定义类加载器,继承java.lang.ClassLoader类的方式实现。
官方文档中将类加载器分为引导类加载器和自定义类加载器
隐式装载:由加载器加载
显式装载:
自定义加载,比如使用反射Class.forName(类路径),类加载器ClassLoader.getSystemClassLoader().loadClass("test.A");
使用当前进程上下文的使用的类装载Thread.currentThread().getContextClassLoader().loadClass("test.A")
======类加载是动态的====,它不会一次性加载所有类然后运行,而是保证程序运行的基础类(核心类库一部分的类)完全加载到JVM中就运行,这是为了节省内存开销。==
全盘负责、双亲委派机制、缓存机制、可见性。
全盘负责:当一个 Class 类被某个类加载器所加载时,该 Class 所依赖引用的所有 Class 都会由这个加载器负责载入,除非显式的使用另一个 ClassLoader。(当然只是这个加载器负责,并不一定就是由这个加载器加载,这是由于双亲委托机制的作用)
缓存机制:当一个 Class 类加载完毕后,会放入缓存,在其他类需要引用这个类时就会从缓存中直接使用,这也是为什么我们在修改了文件后需要重启服务器才能使修改生效。
双亲委托机制:当一个类加载器收到了类加载的请求时,它首先会将这个请求委派给父类,父类不能执行再自己尝试执行,父类如果存在父类,也会委派给父类,这样传到了启动类加载器加载,当启动类加载器不能读取到类时才会传给子类加载器,然后子类加载器再尝试加载。
好处:1、防止自定义的类篡改核心类库中的代码 2、防止同一个类被重复加载
可见性:子类加载器可以访问父类加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没办法利用类加载器去实现容器的逻辑。后续有更新,
程序计数器
概述:较小的内存空间,是当前线程执行的字节码的行号指示器
作用:通过改变计数器的值来指定下一条需要执行的字节码指令,来恢复中断前程序运行的位置
特点:线程私有化,每个线程都有独立的程序计数器、无内存溢出、
JVM唯一 一个没有规定任何 OOM 的区域。也不存在GC
Java虚拟机栈
方法执行流程:进行压栈操作,开始方法的执行,如果此方法中调用了其他方法,那么会将调用的这个方法对应的栈帧压入栈,等到这个方法执行完之后,如果方法包含返回值,将这个返回值返回给上一个方法,然后这个被调用的栈帧出栈,随后继续执行上一个栈帧
概述:每个方法从调用直到执行的过程,对应着一个栈帧在虚拟机栈的入栈和出栈的过程
作用:每个方法执行都创建一个“栈帧”来存储局部变量表、操作数栈、动态链接、方法出口等信息
特点:线程私有化、生命周期与线程执行结束相同
参考链接:https://www.cnblogs.com/mengxinJ/p/14251272.html#_label1_0_2
堆
创建时间:JVM启动时创建该区域
占用空间:Java虚拟机管理内存最大的一块区域
作用:用于存放对象实例及数组(所有new的对象)
特点:垃圾收集器作用该区域,回收不使用的对象的内存空间、各个线程共享的内存区域、该区域的大小可通过参数设置
方法区
作用:用于存储类信息、常量、静态变量、即时编译器编译后的代码缓存,是各个线程共享的内存区域
特点:线程共享
ps:方法区的实现在 1.8 之前是永久代,使用的是 JVM 的内存,在1.8开始实现变成元空间,使用的是本地内存。之所以这样改变,是因为原来的方法区很容易发生 OOM
之所以容易OOM是因为方法区的类信息被回收的条件非常苛刻,必须满足以下三点:
1、该类的所有对象都被回收;
2、加载该类的类加载器被回收;
3、该类对应的 Class 对象没有在任何地方被引用(无法在任何地方通过反射访问该类的方法)。
关于第三点的 Class 对象,在一个类被加载时,会在堆中创建一个用于用于访问这个类的类信息 Class 对象。而在成为元空间后,使用的是本地内存,所以方法区发生 OOM 的情况会极大改善
1)运行时常量池
当 Class 文件被类加载器加载到 JVM 中时,存储的位置就是在方法区,而在 Class 文件信息中包括着 class 文件的常量池,当 JVM 开始执行时,就会将文件常量池中的数据加载到 方法区内部的运行时常量池,变成运行时状态,并将符号引用转成直接引用。
2)字符串常量池
在 JDK 1.7 开始,字符串常量池就由方法区移入了堆中,字符串常量池是专门存放字符串常量的,至于为什么移入堆中,这是因为字符串的创建和对象一样频繁,销毁也就变得尤其频繁,而方法区的 GC 是伴随着 full gc 的, 因为 full gc 会造成 STW,在 full gc 期间其他程序都会停止,所以都会避免 full gc,而字符串常量池放在方法区中就减少了 字符串被回收的频率,提高了 OOM 的概率。
本地方法栈与native(本地)方法
本地方法栈(也就是最上面图中的本地接口)是 JVM 与底层交互的接口,用于调用 native 方法。作用与 Java 虚拟栈差不多,只不过是为 native 方法服务的,是由非 Java 语言编写的
垃圾回收器是JVM(Java Virtual Machine)的重要组成部分,其主要作用是自动管理内存并回收不再使用的对象。以下是垃圾回收器的主要作用:
总的来说,垃圾回收器的作用是帮助开发人员管理内存,自动回收不再使用的对象,解决内存泄漏问题,减少内存碎片,提高应用程序的性能和可靠性。
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
1、引用计数法(很少使用)
2、可达性算法
通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系链向下搜索,如果某个对象无法被搜索到,则说明该对象无引用执行,可回收。相反,则对象处于存活状态,不可回收。
3、标记-清除算法
为每个对象存储一个标记位,记录对象的生存状态。
缺点:两次扫描严重浪费时间;会产生内存碎片,如果Java堆中包含大量对象,而且大部分是需要被回收的,这时必须记性大量标记及清除动作,导致标记和清除两个过程执行效率都随对象数量增长而降低
优点:不需要额外的空间
5、复制算法
复制算法主要发生在年轻代(幸存0区和幸存1区)
坏处:浪费内存空间(浪费幸存区一半的空间);对象存活率较高的场景下,需要复制的东西太多,效率会下降。
最佳使用环境:对象存活率较低的时候,也就是年轻代。
4、标记-整理算法
这个是标记-清除算法的一个改进版,又叫做标记-清除-压缩算法。不同的是在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存货的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。可以进一步优化,在内存碎片不太多的情况下,就继续标记清除,到达一定量的时候再压缩。
优点:
1、弥补了“标记-清除”算法,内存区域分散的缺点
2、弥补了“标记-复制”算法内存减半的代价
缺点:
1、效率不高,对于“标记-清除”而言多了整理工作。
内存效率:复制算法>标记清除算法>标记压缩算法
内存整齐度:复制算法 = 标记压缩算法>标记清除算法
内存利用率:标记压缩算法= 标记清除算法 >复制算法
当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下。具体如下:
根据各个年代的特点采取最适当的收集算法
1、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
2、老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
轻GC(Minor GC)针对年轻代进行垃圾回收,回收短命对象,保持年轻代的空间可用性。
重GC(Major GC)对年轻代和部分老年代进行垃圾回收,回收较大的对象和年龄较大的存活对象。
完全GC(Full GC)对整个堆进行垃圾回收,清除整个堆中的不再被引用的对象。完全GC的过程较长,可能导致较长的停顿时间。
Minor GC触发条件:
当Eden区满时,触发Minor GC。
Full GC触发条件
(1) System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。
(2) 老年代空间不足
老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
(3) 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC
(4) 对象太大,年轻代容不下
分配的对象大小大于eden space。适合所有收集器。
eden space剩余空间不足分配,且需要分配对象内存大小不小于eden space总空间的一半,直接分配到老年代,不触发Minor GC。适合-XX:+UseParallelGC、-XX:+UseParallelOldGC,即适合Parallel Scavenge。
大对象直接进入老年代,使用-XX:PretenureSizeThreshold参数控制,适合-XX:+UseSerialGC、-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC,即适合Serial和ParNew收集器。
因为Object Header采用4个bit位来保存年龄,4个bit位能表示的最大数就是15
内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存
主要有两点:
分配的少了:比如虚拟机本身可使用的内存太少。
应用用的太多,并且用完没释放
举例如下:
处理方法
1、先把内存镜像dump出来,有两种方法
设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息
使用jmap命令。"jmap -dump:format=b,file=heap.bin " 其中pid可以通过jps获取
2、得到内存信息文件后就使用工具去分析,也有两个工具
mat: eclipse memory analyzer, 基于eclipse RCP的内存分析工具
jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等
解决办法:
逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。
逃逸分析可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配,由于该对象一定是局部的,所以栈上分配不会有问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。