1 内存模型(1.7和1.8),每个模型做什么?

JDK1.7内存模型分为:方法区(method area),堆区(heap)—(所有线程共享的数据区),虚拟机栈(VM stack),本地方法栈(native method stack),程序计数器(program counter register)—(线程私有的数据区),
image.png
程序计数器:线程私有,可以看作当前程序执行的行号指令器;
Java虚拟机栈:线程私有,生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存 模型,每个方法在执行时会形成一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,一个方法从调用到执行完毕,就是一个栈帧从进栈到出栈的过程,
image.png
本地方法栈:线程私有,作用于Java虚拟机栈类似,只不过Java虚拟机栈执行Java方法,而本地方法栈运行本地的native方法
:Java虚拟机管理的最大的一块内存区域,Java堆是线程共享的,用于存放对象实例,也就是说对象出生和回收都是在这个区域进行的。
堆分为新生代(young gen)老年代(tenured gen) ,比例默认为1:2,而新生代又分为Eden和from ,to 三个区域,默认比例为8:1:1,如图:
image.png
方法区:线程共享,用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据;
这里的永久代是hotspot虚拟机对于方法区的实现,方法的实现是不受虚拟机规范约束的,这里只是hotspot虚拟机团队是这样实现的
运行时常量池:在JDK1.7中,运行时常量池是方法区的一部分,用于存放编译期生成的各种字符变量和符号引用。其实除了运行时常量池,还有字符串常量池,class常量池,
JDK1.8内存模型:
image.png
JDK1.7与1.8最大的区别是1.8将永久代 取消,取而代之的是元空间,既然方法区是由永久代实现的,取消了永久代, 那么方法区由谁来实现呢,在1.8中方法区是由元空间来实现,所以原来属于方法区的运行时常量池就属于元空间了。元空间属于本地内存,所以元空间的大小仅受本地内存限制,但是可以通过-XX:MaxMetaspaceSize进行增长上限的最大值设置,默认值为4G,元空间的初始空间大小可以通过-XX:MetaspaceSize进行设置,默认值为20.8M,还有一些其他参数可以进行设置,元空间大小会自动进行调整,

  1. 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久 代
  2. 在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
  3. 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

[

](https://blog.csdn.net/Hollake/article/details/92762180)

2 堆内存组成(新生代和老年代)?

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:EdenFrom SurvivorTo Survivor
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
image.png
从图中可以看出: 堆大小 = 新生代 + 老年代
默认情况下:
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
Eden : from : to = 8 : 1 : 1
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。

Minor GC

Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。

新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。

当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。

回收过程如下:

  当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳(上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是最大 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。

但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 )或者Suvivor区已满,则是直接进入到老年代。

Full GC 是发生在老年代的垃圾收集动作,通常也会伴随着新生代的垃圾回收,所采用的是标记-整理算法。

现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长,一般是Minor GC的 10倍以上。
[

](https://blog.csdn.net/qq_42370146/article/details/105569520)
3Java中类加载的过程
4类加载器有哪些?
Java的类加载器有四种
Bootstrap ClassLoader:根类加载器,负责加载Java的核心类,它不是java.lang.ClassLoader的子类,而是由JVM自身实现
Extension ClassLoader:扩展类加载器,扩展类加载器的加载路径是JDK目录下 jre/lib/ext,通过扩展类加载器的getParent() 方法返回的是null,实际上扩展类加载器的父类是根加载器
System ClassLoader:系统(应用)类加载器,它负责在JVM启动时加载来自java 命令的 -classpath选项,或者通过CLASSPATH环境变量所指定的jar包和类路径。
User ClassLoader:自定义类加载器,加载用户创建的自定义类
[

](https://blog.csdn.net/weixin_43884884/article/details/107719529)
5双亲委派模型?如何打破?
什么是双亲委派机制
当一个类收到了类的加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一层的类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派机制的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托顶层的启动类加载器进行加载,这样保证了使用不同的类加载器最终得到的都是同一个Object对象。
image.png
工作原理
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
image.png
双亲委派机制举例
当我们加载jdbc.jar 用于实现数据库连接的时候,首先我们需要知道的是 jdbc.jar是基于SPI接口进行实现的,所以在加载的时候,会进行双亲委派,最终从根加载器中加载 SPI核心类,然后在加载SPI接口类,接着在进行反向委派,通过线程上下文类加载器进行实现类 jdbc.jar的加载。
image.png
沙箱机制
我们创建一个自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
双亲委派机制的优势
通过上面的例子,我们可以知道,双亲机制可以
避免类的重复加载
保护程序安全,防止核心API被随意篡改
自定义类:java.lang.String
自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
为什么要打破双亲委派机制?
打破双亲委派机制的场景有很多:JDBC、JNDI、Tomcat等,我们以Tomcat为例来说明
Tomcat为什么要打破双亲委派机制
首先tomcat是一个web容器,主要是需要解决以下问题
一个web容器可能要部署两个或多个应用程序,不同的应用程序之间可能会依赖同一个第三方类库的不同版本,因此要保证每个应用程序的类库都是独立的、相互隔离的
部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM中
web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离
web容器支持jssp文件修改后不用重启,jsp文件也要编译成.class文件的,支持HotSwap功能
Tomcat使用Java默认加载器的问题
默认的类加载器无法加载两个相同类库的不同版本,它只在乎类的全限定类名,并且只有一份,所以无法解决上面的问题1和问题3,也就是相关隔离的问题。
同时在修改jsp文件后,因为类名一样,默认的类加载器不会重新加载,而是使用方法区中已经存在的类,所以需要每个jsp对应一个唯一的类加载器,当修改jsp的时候,直接卸载唯一的类加载器,然后重新创建类加载器,并加载jsp文件。

Tomcat的类加载机制

image.png
tomcat有多个自定义类加载器
CommonClassLoader:tomcat最基本的类加载器,加载路径中class可以被tomcat和各个webapp访问
CatalinaClassLoader:tomcat私有类加载器,webapp不能访问其加载路径下的class,即对webapp不可见
SharedClassLoader:各个webapp共享的类加载器,对tomcat不可见
WebappClassLoader:webapp私有的类加载器,只对当前webapp可见
JasperClassLoader:JSP的类加载器
每个web应用程序都对应一个WebappClassLoader,每一个jsp文件对应一个JasperClassLoader,所以这两个类加载器有多个实例。

工作原理
CommonClassLoader能加载的类都可以被CatalinaClassLoader使用,从而实现了公有类库的共用。
CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离
WebappClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离,多个WebAppClassLoader是同级关系。
JspClassLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.class文件,它出现的目的就是为了被丢弃;当web容器检测到JSP文件被修改时,会替换掉目前的JasperClassLoader实例,并通过在创建一个Jsp类加载器来实现JSP文件的HotSwap功能
tomcat目录结构,与上面的类加载器对应
/common/
/server/

/shared/
WEB-INF/

默认情况下,conf目录下的catalina.properties文件,没有指定server.loader以及shared.loader,所以tomcat没有建立CatalinaClassLoader和SharedClassLoader实例,这两个都会使用CommonClassLoader来代替。Tomcat6之后,把common、shared、server目录合成一个lib目录,所以我们服务器里看不到common、shared、server目录。
Tomcat应用的默认加载顺序
先从JVM的BootStrapClassLoader中加载。
加载Web应用下/WEB-INF/classes中的类。
加载Web应用下/WEB-INF/lib/.jap中的jar包中的类。
加载上面定义的System路径下面的类。
加载上面定义的Common路径下面的类。
Tomcat类加载过程
先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则继续下一步
让系统类加载器(ApplicationClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续
前两步均没有加载到目标列,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续
前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步
最后还是加载不到的话,则委托父类父类加载器(Common ClassLoader)去加载
Tomcat打破双亲委派
image.png
如上图所示,上面的橙色部分还是和原来一样,采用的双亲委派机制,
黄色部分是tomcat第一部分自定义的类加载器,这部分主要是加载tomcat包中的类,这一部分依然采用的是双亲委派机制
而绿色部分是tomcat第二部分自定义类加载器,正是这一部分*,打破了类的双亲委派机制

Tomcat第一部分自定义类加载器(黄色部分)
这部分类加载器,在tomcat7及以前是tomcat自定义的三个类加载器,分别在不同文件加载的jar包,而到了tomcat8及以后,tomcat将这三个文件夹合并了,合并成一个lib包,也就是我们现在看到的lib包
image.png
我们来看看这三个类加载器 的主要作用

CommonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat容器本身和各个webapp访问
CatalinaClassLoader:tomcat容器中私有的类加载器,加载路径中的class对webapp不可见
SharedClassLoader:各个webapps共享的类加载器,加载路径中的class对所有的webapp都课件,但是对tomcat容器不可见
这一部分类加载器,依然采用的是双亲委派机制,原因是它只有一份,如果有重复那也是以这一份为准

Tomcat第二部分自定义类加载器(绿色部分)
绿色是Java项目在打war包的时候,tomcat自动生成的类加载器,也就是说,每一个项目打成war包,tomcat都会自动生成一个类加载器,专门用来加载这个war包,而这个类加载器打破了双亲委派机制,我们可以想象一下,加入这个webapp类没有打破双亲委派机制会怎么样?

如果没有打破,它就会委托父类加载器去加载,一旦加载到了,紫烈加载器就没有机会加载了,那么Spring4和Spring5的项目就没有可能共存了。

所以,这一部分它打破了双亲委派机制,这样一来webapp类加载器就不需要在让上级类去加载,它自己就可以加载对应的war里的class文件,当然了,其它的项目文件还是要委托上级加载的。

举例
我们首先列举一个场景,比如现在我有一个自定义类加载器,加载的是 /com/lxl/jvm/User1.class类,而在应用程序的target目录下也有一个 com/lxl/jvm/User1.class,那么最终User1.class这个类将被哪个类加载器加载呢?根据双亲委派机制,我们知道它一定是被应用程序类加载器AppClassLoader加载,而不是我们自定义的类加载器,为什么呢?因为他要向上寻找,向下委托,当找到以后,便不再向后执行了。

而我们要打破双亲委派机制,就是要让自定义类加载器来加载我们的User1.class,而不是应用程序类加载器来加载。

接下来分析,如何打破双亲委派机制?双亲委派机制是在那里实现的呢?

双亲委派机制是在ClassLoader类的loadClass()中实现的,如果我们不想使用系统自带的双亲委派模型,只需要重新实现ClassLoader的loadClass()方法即可,下面是ClassLoader中定义的loadClass()方法,里面实现了双亲委派机制
image.png
下面给DefinedClassLoaderTest.java增加一个loadClass方法, 拷贝上面的代码即可. 删除掉中间实现双亲委派机制的部分
image.png
这里需要注意的是,com.lxl.jvm是自定义的雷暴,只有我们自己定义的类才可以从这里加载,如果是系统类,依然使用双亲委派机制来加载,下面来看看运行结果
image.png
现在User1方法确实是由自定义类加载器加载的了,源码如下
image.png
image.png
image.png
image.png
image.png

6内存溢出和内存泄漏

1、内存泄漏memory leak :
是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
2、内存溢出 out of memory :
指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
3、二者的关系:
内存泄漏的堆积最终会导致内存溢出
内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错。
4、内存泄漏的分类(按发生方式来分类)
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4.隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
5、内存溢出的原因及解决方法:
(1) 内存溢出原因:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小

(2)内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。
3.检查是否有大循环重复产生新对象实体。
4.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况
[

](https://blog.csdn.net/mashuai720/article/details/79557670)

7jvm相关参数-调优(会3个参数)

8jvm相关的命令调优(jps top jstack)