(1)类加载器:负责加载class文件
(2)线程共享内存(堆内存):静态变量、常量、类信息、运行时常量池、实例变量(new出来的对象)
(3)线程私有内存(栈内存):虚拟机栈、本地方法栈,PC程序寄存器
(4)总结:栈管运行、堆管存储
(1)什么是栈
1.1)栈也叫栈内存,主管Java程序的运行
1.2)是在线程创建时创建,它的生命周期是跟随线程的生命期的,对于栈来说不存在垃圾回收问题
(2)栈存储什么
2.1)本地变量:输入参数和输出参数以及方法内变量
2.2)栈操作:记录出栈、入栈的操作
2.3)栈帧数据:包括类文件、方法等
(3)栈运行原理
3.1)栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,一个有关方法和运行期数据的数据集
3.2)当一个方法A被调用时就产生一个栈帧F1,并被压入到栈中
3.3)A方法又调用B方法,于是产生栈帧F2,也被压入到栈中
3.4)执行完毕后,先弹出F2,再弹出F1,栈运行是遵循 “先进后出”/ “后进先出”的原则
(4)栈异常
4.1)java.lang.StackOverflowError
4.2)在java中函数栈调用层级过多,导致当前线程的栈满了,就会出现此异常,比如:方法调用如出现死循环这种情况
内存溢出:指程序在申请内存时,没有足够的内存空间供其使用,出现OOM
内存泄漏:指分配出去的内存不再使用,但是无法回收
JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots?
内存中已经不再被使用到的空间就是垃圾
如果程序中无法再引用到该对象,这个状态称为不可达,那么该对象就可以作为回收对象被垃圾回收器回收
所谓不可达就是从根对象(GC roots)开始无法引用到该对象,该对象就是不可达的
虚拟机栈中引用的对象
方法区静态成员引用的对象
方法区常量引用对象
本地方法栈中Native方法引用的对象
公式:-XX: +或者-某个属性 (+表示开启 -表示关闭)
例子:开启打印GC收集日志 -XX:+PrintGCDetails
公式:-XX: 属性key=属性值value
例子:设置MetaspaceSize空间大小 -XX:MetaspaceSize=128m
查看当前正在运行的Java程序
查看某个Java程序当前正在使用的所有JVM参数
Non-default:表示系统根据当前机器性能自动设置的参数
Command line:表示人为添加的一些参数
查看某个Java程序,它的某个JVM参数是否开启,具体值是多少
PrintFlagsInitial :查看JVM配置的初始默认值,这个会根据系统的内存大小而变化
PrintFlagsFinal : 查看JVM修改过的配置初始参数,等号前面添加了 : 表示是修改过的值
运行时,将当前Java程序使用的JVM参数打印出来,可查看使用的是什么垃圾
java -XX:+PrintCommandLineFlags -version
初始堆内存,默认为物理内存的 1/64
等价于 -XX:InitialHeapSize
最大堆内存,默认为物理内存的 1/4
等价于 -XX:MaxHeapSize
设置线程栈大小,一般默认为512k ~ 1024k,Oracle官方文档中写到从 64位的Linux系统默认都设置为1024K
等价于 -XX:ThreadStackSize
设置元空间大小,Java默认只设置了20M,如果需要可自行通过此参数调配
JDK8元空间和JDK7永久代最大区别在于:元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小只受本地内存限制
-XX:+PrintGCDetails
设置打印GC日志
-Xloggc:/path/gc.log
日志规律:[ 名称 ] [ GC前内存占用 ] -> GC后内存占用 [ 该区内存总大小 ]
设置新生代中的占比,也就是 Eden 和 S0/S1 的比例,S0/S1相同
此值默认是 -XX:SurvivorRation=8
,表示 Eden:S0:S1 = 8:1:1
设置老年代的占比,剩下的1给新生代
此值默认是2, -XX:NewRatio = 2
,表示 新生代占1,老年代2,新生代占整个堆的 1/3
设置垃圾最大年龄,如果设置为0的话,则年轻代对象不再经过 Survivor 区,直接进入老年代。对于老年代比较多的应用,可以提高效率。
如果设置为一个较大的值,则年经代对象会在 Survivor 区进行多次复制,这样可以增加对象再年轻代的存活时间,也即增加在年轻代被回收的概率,降低 Full GC 概率。
此值只能设置为 0 到 15 之间,JDK8开始默认设置就是15,即对象在 From区 和 To 区来换复制15次,还是存活的就进入老年代
-Xms128m
-Xmx4096
-Xss1204k
-XXMetaspaceSize=512m
-XX:+PrintCommandLineFlags
-XX:+PrintGCDetails
-XX:+UseParallelGC
Java堆从GC的角度可以细分为:新生代(Eden区、From Survivor区 和 To Survivor区)和老年代
新创建的对象首先会分配在 Eden 区,当Eden区满的时候会触发第一次GC,把存活的对象复制到 From 区,当 Eden 区再次触发GC的时候,会扫描 Eden区 和 From 区,把存活的对象复制到 To 区,如果有对象的年龄已经达到标准,则直接进入老年代,否则把这些对象的年龄 + 1
然后,会清空 Eden区 和 From 区中的对象,让 To 区和 From 区进行互换,原 To区成为下一次GC时的 From区,对象在 From和 To 区交换15次(MaxTenuringThreshold参数决定,CMS默认为6次,其它垃圾收集器为15次),最终如果还是存活,就进入老入代
如果分配的新对象比较大,Eden区放不下,Old区可以放下,对象会直接被分配到Old区
因为如果没有Survivor区那每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major Gc,比较耗时
如果只有1个Survivor区,那当Eden区满了之后,就会复制对象到Survivor区,容易产生内存碎片化。严重影响性能。所以使用2个Survivor区,让它们可以来回的交换,始终保持有一个空的Survivor区,可以避免内存碎片化。
简单说 类加载器就是根据全限定名称将class文件加载到JVM内存,转为Class对象
2.1)如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类
2.2)而是把这个请求委派给父类加载器完成。每个类加载器都是如此
2.3)只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException)
2.4)子加载器才会尝试自己去加载。
如果黑客自定义一个跟java中原生的String一摸一样的类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。
比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。
此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的String类是系统的String类,导致“病毒代码”被执行。
(1)双亲委派模型,保证黑客自定义的String类永远都不会被加载进内存
(2)因为首先是最顶端的类加载器加载系统的String类,最终自定义的类加载器无法加载String类
所谓强引用就是指,只要还有一个引用指向该对象,那就算发生了OOM,JVM也不会对该对象进行回收。
工作中平时使用的大部份对象都是属于强引用,因此强引用是造成Java内存泄露的主要原因之一
软引用是一种相对强化了一些的引用,需要用 `java.lang.ref.SoftReference` 类来实现,可以让对象豁免一些垃圾收集
如果内存空间足够大,垃圾回收期就不会回收它,如果内存空间不够了,就会回收这些对象。软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收,Mybatis框架内部缓存功能就大部分使用了软引用
假如有一个应用需要读取大量的本地图片:
如果每次读取图片都从硬盘读取则会严重影响性能。
如果一次性全部加载到内存中有可能造成内存泄露。
此时使用软引用可以解决这个问题
设计思路:用一个HashMap来保存图片和相应图片路径关联的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免OOM的问题
WeakHashMap<Integer, String> map = new WeakHashMap<>();
弱引用需要用 `java.lang.ref.WeakReference` 类来实现,它比软引用的生存期更短,无论内存是否充足,只要当JVM进行垃圾回收时,都会被回收的对象就是弱引用的对象
虚引用与其他几种引用都不同,虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
它不能单独使用,也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用,虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制
对于 软引用、弱引用、虚引用三类对象,在被GC回收之前,它们都会被放到引用队列中保存下,当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收,JVM允许我们在对象被销毁后,做一些我们自己想做的事情,相当于一种通知机制,与 SpringAOP的前置通知类似。
java.lang.StackOverFlowError
在java中函数栈调用层级过多,导致当前线程的栈满了,就会出现此异常,比如:方法调用如出现死循环这种情况
java.lang.OutOfMemoryError:
堆内存溢出异常
GC回收时间过长,超过98%的时间用来做GC但只回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。
假设不抛出 GC overhead limit 错误会发生什么情况呢?
那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任何成果。
参考下方的 --> 能说说你对堆外内存的理解吗?堆外内存的优势在哪里?
导致原因
应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限。
服务器并不允许应用程序创建那么多线程,Linux系统默认允许单个进程可以创建的线程数是1024个,如果应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread
解决办法:
想办法降低应用程序创建线程的数量,分析应用是否真的需要创建那么多线程,如果不是,改代码将线程数降到最低。
对于有的应用,确实需要创建多个线程,远超过linux系统默认的1024个线程的限制,可以通过修改linux 服务器配置,扩大linux默认限制。
Java8以后metaspace并不在虚拟机中,而是使用本地内存,因此默认情况下,metaspace的大小只受本地内存限制,metaspace存储以下信息
虚拟机加载的类信息
常量池
静态变量
编译后的代码
如果程序中加载了大量的以上内容,导致元空间被占满,就会出现此错误
堆外内存指的就是JVM堆内存以外的内存,也就是OS本地内存
在写NIO Socket网络发送数据的程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道Channel与Buffer的I/O方式,可以通过 `ByteBuffer.allocateDirect(capability)` 方式来申请堆外内存,然后Java堆内有一个 DirectByteBuffer 对象会作为这块堆外内存的引用进行操作
堆内的数据,要网络IO写出去,要先拷贝到堆外内存,再写入到socket里发送出去;如果直接数据分配在堆外内存,就不需要进行一次额外的拷贝,性能是比较高的
但如果不断分配堆外内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但堆外内存可能已经使用光了,再次尝试分配堆外内存就会出现 OutOfMemoryError:Direct buffer memory 异常,那程序就直接崩溃了
Minor GC(年轻代 GC)主要就是复制算法(Copying),复制算法基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还存活的对象复制到另外一块上面去,复制算法不会产生内存碎片
缺点
耗内存、如果对象存活率高,则复制这一项工作所花费的时间就会成问题
标记:从根集合开始扫描,对存活的对象进行标记
清除:扫描整个内存空间,将未标记的对象进行回收,使用free-list记录
效率较低,因为会进行两次扫描,会产生出内存碎片
这种方式清理出来的空间内存是不连续的,内存的布局会乱七八糟,JVM就会使用上面的free-list来维持内存的空闲列表,这又会是一种开销
不直接对可回收对象进行清理,而是让所有可用的对象都向一端移动,然后直接清理掉边界以外的内存,这种算法解决了产生内存碎片的问题
但缺点是效率低,因为要标记所有存活对象,还要整理所有存活对象的引用地址
串行收集器:Serial、Serial old
并行收集器:ParNew、Parallel、Parallel Old
并发收集器:CMS、G1
一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程知道它收集结束。适合嵌入式程序
多线程的方式进行垃圾回收,在垃圾收集时,也会暂停其他所有的工作线程直到它收集结束
并行收集器其实就是串行收集器在新生代的多线程版本,最主是配合老年代的CMS GC工作,它是Java虚拟机运行在Server模式下新生代的默认垃圾收集器
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,是一种响应时间优先的的垃圾收集器
适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短
串行收集器组合:Serial(新生代) + Serial old(老年代)
并行收集器组合:Parallel(新生代) + Parallel Old(老年代)
并发收集器组合:ParNew(新生代) + CMS(老年代) + Serial Old(老年代)
整堆收集器:G1
ParNew 并行收集器
开启参数:-XX:+UseParNewGC
开启该参数后,使用组合为 ParNew(新生代)+ Serial old(老年代),在JDK1.8官方已经废弃
开启参数:-XX:+UseParallelGC
或 -XX:+UseParallelOldGC
(可互相激活)
开启该参数后,使用组合为 Parallel(新生代) + Parallel Old(老年代)
在JDK1.8默认就是该组合收集器
开启参数:-XX:+UseConcMarkSweepGC
开启该参数后会自动将 -XX:+UseParNewGC
打开
开启该参数后,使用的组合为 ParNew(新生代) + CMS(老年代) + Serial Old(老年代)
Serial Old 将作为CMS出错的后备收集器
开启参数:-XX:+UseG1GC
JDK1.9开始无须设置,默认就是G1
单CPU或小内存,单机程序
Serial(新生代) + Serial old(老年代)
多CPU,需要最大吞吐量,如后台计算型应用
Parallel(新生代) + Parallel Old(老年代)
多CPU,追求低停顿时间,需要快速响应如互联网应用
ParNew(新生代) + CMS(老年代) + Serial Old(老年代)
G1
CMS是从JDK1.5以后开始推出的一款垃圾收集器,它关注的点是尽可能的缩短用户线程的停顿时间,停顿时间越短,就越适合与用户交互的程序,良好的响应速度提升用户的体验
因为CMS是一款并发收集器,它在回收时用户线程是没有中断的,所以在CMS回收时还需要确保用户线程有足够的内存可用,并不能在空间满的时间才回收,所以当内存使用率达某一阀值时,CMS需要提前开始回收,以确保CMS回收时仍然有足够的内存供用户线程运行
因为CMS回收时用户线程是没有中断的,会继续制造垃圾,所以当CMS回收时预留的内存无法满足用户线程的需要,会出现一次 "Concurrent Mode Failure",回收失败,这时虚拟机将启动后备的方案,也就是Serial Old垃圾收集器,但是这样停顿的时间就很长了
初始标记(initial mark):相当于给所有 GC Roots 能直接关联的对象说一声,大家停一下,我要开始进行垃圾收集了 速度很快,但此步需要暂停工作线程。
并发标记(concurrent mark):开始和用户线程一起进行 GC Roots 跟踪,给每一个需要回收的对象打标记 ,此步不需要暂停工作线程。
重新标记(remark):因为第二步不会暂停程序,所以为了防止在打标记的过程中,有些对象状态发生了改变,所以需要重新确认一下,这是清理的前一步工作,很重要,所以为了清理的准确性,在确认时,需要暂停工作线程
并发清除(concurrent sweep):清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
上述的这些问题,尤其是碎片化问题,给你的JVM实例就像埋了一颗炸弹。说不定哪次就在你的业务高峰期来一次FGC。
当CMS停止工作时,会把Serial Old GC 作为备选方案,而Serial Old GC 是JVM中性能最差的垃圾回收方式,停顿个几秒钟,上十秒都有可能。
因为CMS是并发垃圾收集器,当工作时用户线程也在执行,如果用标记压缩,相当把对象的引用都改变了,那用户线程原来使用的内存还怎么用呢,所以要保证用户线程不受影响,前提是它运行的资源不受影响
G1是一款面向服务端应用的收集器,主要针对多CPU和大容量内存机器,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求
G1默认将整个堆划分为2048个大小相同的独立Region块,使用不同的Region来表示Eden、S0、S1、以及老年代等。虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,在G1中他们都是一部分的Region(不需要连续),通过Region动态分配方式实现逻辑上的连续
JDK1.9默认就是该收集器
在G1中,有一种特殊的区域,叫 Humongous(巨大的)区域。如果一个对象占用的空间超过了1.5个Region,G1收集器就认为这是一个大对象, 这些大对象默认直接会被分配在老年代(大对象不适合在Young区),但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。
为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个 Humongous 区装不下这个大对象,那么G1会寻找连续的 Humongous 分区来存储。为了能找到连续的 Humongous 区,有时候不得不启动Full GC
相比于CMS的标记清理算法,G1是将内存划分为一个个的Region,内存回收是以Region作为基本单位的,Region之间采用的是复制算法,在整理上又采用的标记压缩算法,所以能有效避免内存碎片。有利于程序长时间的运行
由于G1是将内存划分为一个个的Region,所以可以只选取部分区域进行回收,这样就缩小了回收的范围,不像原来整个Young区进行回收,或者整个Old区进行回收,对于这种全局停顿的情况发生也得到了较好的控制
G1在回收时会跟踪各个Region里面的垃圾堆积价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证G1收集器在有限的时间内可以获取尽可能高的收集效率
G1为了垃圾收集而产生的内存占用,还有程序进行时的额外执行负载都要比CMS高
G1不适合用于小内存的应用上,内存过小不利于Region的分配,导致在遇到大对象时很难找到连接的存储空间,建议是8g以上应用适合G1
初始标记:只标记GC Roots能直接关联到的对象
并发标记:进行GC Roots Tracing的过程
最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
筛选回收:根据时间来进行价值最大化的回收
开启G1收集器
设置每个Region大小,值是2的幂数,大小是1到32M之间,目标是根据最小的Java堆大小划分出2048个Region,默认是堆内存的 1/2000
也就是说 -Xmx 分配为4g,则每块Region区域大小大约就是 4 * 1024 = 4096 / 2000 = 2M 左右
设置期望达到的最大GC停顿时间指标,这是一个软性指标,JVM会尽力实现,但不保证达到,默认值是200ms
由于G1在回收时,是在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,如果这个时间设置过小,比如20ms,那么原来根据优先列表能回收5、10个Region的,现在只能先放弃列表前面较大的Region,只回收2个Region,来尽可能满足停顿时间指标
这种情况下用户线程在前面使用Region率又比较高,但是G1回收Region率又比较小,内存使用率就会越来越高,最终导致占满,占满以后那就触发 Full GC
所以这种看似设置停顿时间低,会说延迟效果会好,其实反而性能是下降了的
java -XX:+PrintCommandLineFlags -version
jps 显示出当前java进程号
jinfo -flag 进程号 查看该进程号java程序当前正在使用的所有JVM参数,就会有垃圾收集器
top -c,就可以显示进程列表,然后输入P,按照cpu使用率排序
top -Hp 43987,就是输入那个进程id就好了,然后输入P,按照cpu使用率排序
printf "%x\n" 16872
,把线程pid转换成16进制,比如0x41e8
jstack 43987 | grep ‘0x41e8’ -C5 --color
这个就是用jstack打印进程的堆栈信息,而且通过grep那个线程的16进制的pid,找到那个线程相关的东西,这个时候就可以在打印出的代码里,看到是哪个类的哪个方法导致的这个cpu 100%的问题
top 后 再按 1 可以查看每个 CPU 的情况
load average 表示系统当前平均负载值,从左到右分别是 1 分钟 、 10 分钟、15 分钟 的情况,如果三个值相加除以60再乘100%,结果高于60,表示系统负载压力重
安装命令工具:yum install -y sysstat
每隔2秒采样一次CPU的情况,共采样3次
运行等待CPU时间片的进程数,原则整个系统的运行数不能超过总核数的2倍,否则代表系统压力过大
用户进程消耗CPU时间百分比;内核进程消耗的CPU百分比;
如果这两个值过高,表示用户进程消耗CPU时间多,如果长期大于50%,优化程序;
每隔2秒,采样一次所有CPU的情况
%usr :用户使用情况 %sys:系统使用情况
%idle:CPU的空闲率,越高越好,如果低于60%,说明系统压力就大了
每秒采样1次,这个进程的具体CPU使用情况
一般 应用程序使用的总内存只要没超过 系统总内存 的 70 % 就没问题,超过了就补内存
每2秒采样1次,这个进程的具体内存使用情况
查看磁盘总体使用情况
查看磁盘IO性能
rkB/s:每秒读取数据量
wkB/s:每秒写入数据量
%util:每秒有百分之几的时间用于IO操作,接近100时,表示磁盘带宽跑满,需要优化或增加磁盘
wget http://gael.roualland.free.fr/ifstat/ifstat-1.1.tar.gz
tar xvf ifstat-1.1.tar.gz
cd ifstat-1.1/
./configure
make
make install
=========================================================
常见JAVA面试题总结<2020 java面试必备>(一)
常见JAVA面试题总结<2020 java面试必备>(二)多线程
常见JAVA面试题总结<2020 java面试必备>(四)设计模式
常见JAVA面试题总结<2020 java面试必备>(五) 网络