JVM。。。

JVM

一.运行时数据区域

JVM内存空间分为五部分:
程序计数器,虚拟机栈,本地方法栈,Java堆,方法区
(程序计数器,虚拟机栈,本地方法栈中每个线程都有自己独立的区域,线程之间不是共享的;方法区和Java堆中所有线程是共享的,所有线程都可以互相访问)

方法区:
用来存放类信息、类的静态变量、常量、运行时常量池等,方法区的大小是可以动态扩展的,

堆:
存放的是数组、类的实例对象、字符串常量池等。

Java虚拟机栈:
Java虚拟机栈会为每一个即将执行的方法创建一个叫做“栈帧”的区域,该区域用来存储该方法运行时需要的一些信息,当方法执行结束后,这个方法对应的栈帧将出栈,并释放内存空间。
(栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。)
栈中会发生的两种异常,StackOverFlowError和OutOfMemoryError,StackOverFlowError表示当线程申请栈时发现栈已经满了,但内存空间可能还有很多。 而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

本地方法栈:
结构上和Java虚拟机栈一样,只不过Java虚拟机栈是运行Java方法的区域,而本地方法栈是运行本地方法的内存模型。运行本地方法时也会创建栈帧,在本地方法执行结束后栈帧也会出栈并释放内存资源,也会发生OutOfMemoryError。

程序计数器:
程序计数器是一个比较小的内存空间,用来记录当前线程正在执行的那一条字节码指令的地址。如果当前线程正在执行的是本地方法,那么此时程序计数器为空。程序计数器有两个作用,1、字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,比如我们常见的顺序、循环、选择、异常处理等。2、在多线程的情况下,程序计数器用来记录当前线程执行的位置,当线程切换回来的时候仍然可以知道该线程上次执行到了哪里。而且程序计数器是唯一一个不会出现OutOfMeroryError的内存区域。

JVM内存模型(JMM)

heap和stack区别

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

什么情况下发生栈内存溢出

在这里插入图片描述

OOM,如何排查

OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。(StackOverFlowError表示当线程申请栈时发现栈已经满了,但内存空间可能还有很多。 )
在这里插入图片描述

JVM常量池

二.垃圾回收

GC机制
在JVM中,有一个垃圾回收线程,它是低优先级的,正常情况下不会执行,只有在虚拟机空闲或者当前堆内存不足是,才会触发执行,进行回收(将没有被任何引用的对象回收)

问:可以马上回收么,有什么办法主动通知虚拟机进行垃圾回收?
答:可以,手动执行System·gc(),通知GC运行

怎么判断对象是否可以被回收?

1.引用计数器法:
为每个对象创建一个引用计数器,有对象引用的计数器+1,引用被释放时计数器-1,计数器为0时就可以被回收
缺点就是不能解决循环引用的问题------如果我有两个对象,已经不再使用,但是他们互相引用,那他们的引用计数器就永远不为0,不会被回收)

2.可达性分析算法:
从GC Roots开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,则可以回收
(开发中遇到的内存泄漏很大一部分原因就是本该被回收的对象被GC Roots引用了)

在Java虚拟机中被规定作为GC Roots的对象有:
1.虚拟机栈中引用的对象
2.方法区中静态属性引用的对象和常量引用的对象
3.JNI引用的对象

在这里插入图片描述

内存溢出异常,Java内存泄漏的原因?

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有他得引用而导致不能回收

Minor GC和Full GC触发条件

Minor GC触发条件:
Eden区空间不足

Full GC触发条件:
老年代空间不足
方法区空间不足
调用System.gc()方法时

Java中有哪些引用类型?

1.强引用:发生gc时不会被回收
String s=new String(“xiaoming”)
2.软引用:发生内存溢出前会被回收,就是不回收就要内存溢出了
(有用但不是必须的对象)
3.弱引用:每次gc时会被回收(有用但不是必须的对象)
4.虚引用:在gc是返回一个通知

被引用的对象一定能存活么

在这里插入图片描述

垃圾回收算法

标记清除算法
在这里插入图片描述
标记整理算法
在这里插入图片描述
复制算法
在这里插入图片描述
分代收集算法
在这里插入图片描述
在这里插入图片描述

**新生代:**复制算法
把某个空间里存活的对象复制到其他空间,把原空间可以回收的对象回收
效率高,内存利用率低(新生代中每次都会有大量对象被回收,比较频繁,因此用复制算法)

**老年代:**标记-整理算法

先标记,对存活的对象进行移动,移动到另一端,然后再清理已经死亡的

垃圾收集器

在这里插入图片描述

CMS收集过程:

想使用CMS垃圾回收器的话,只需要在JVM启动参数上加上一条

-XX:+UseConcMarkSweepGC

在这里插入图片描述

CMS缺点

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

CMS垃圾收集器会产生空间碎片。

解决:
CMS提供了两个参数,解决了这个问题
-XX:+UseCMSCompactAtFullCollection

这个参数可以开启我们的内存空间整理,使我们的空间整齐划一。

-XX:CMSFullGCsBeforeCompaction

这个参数的意思是,在CMS执行多少次full gc之后进行空间整理,最好是在每一次垃圾回收之后,都整理一次内存空间。

G1收集器

G1:
是JDK1.7提供的一个新收集器,G1基于标记-整理算法实现,不会产生内存碎片,G1回收的范围是整个Java堆。G1采用内存分区的思路,将内存划分为一个个大小相等的内存分区,回收时以分区为单位回收,存活的对象复制到另一个空闲分区中。G1的收集都是STW的,采用了混合收集的方式,这样即使堆内存很大时,也可以限制收集的范围,从而降低停顿
在这里插入图片描述

什么时候垃圾回收?

1.Eden区空间不足
2.老年代空间不足
3.永久代空间不足

垃圾回收的优点?

有效的防止了内存泄漏
有效的使用可使用的内存

JVM中一次完整的GC

在这里插入图片描述

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

Minor GC 和Fu’ll GC区别

在这里插入图片描述

四.类加载机制

把类的数据从class文件中(其内容是字节码)加载到内存,并对数据进行处理,最终形成可被直接使用的Java类型

原理:
Java中所有的类都需要类加载器装载到JVM中才能运行,工作原理就是把class文件从硬盘读取到内存上,Java类的加载是动态的,并不会一次性将所有类全部加载后在运行,而是把基础的类先加载到JVM中,其他的类什么时候用什么时候再加载

类加载的过程:
1.加载(根据路径找到相对应的class文件并导入) 静态变量是在类加载的时候分配空间的
2.验证(检查class文件的正确性)
3.准备(给类中的静态变量分配内存空间)
4.解析(将常量池中的符号引用替换成直接引用)
5.初始化 静态变量在初始化过程被赋值

注意:final类型变量会在虚拟机准备阶段给value赋值,非final类型会在对象初始化阶段设置value值

类加载器

在这里插入图片描述

五.JVM调优

JVM调优工具:visualvm(JDK自带的全能分析工具)

什么情况下要对JVM调优?

1.堆内存达到最大内存值
2.Full GC次数频繁
3.GC停顿时间过长
4.应用出现OutOfMemory内存异常
5.应用中使用本地缓存并占用大量内存空间
6.系统吞吐量与相应性能不高或下降

调优的目标

1.GC低停顿
2.GC低频率
3.低内存占用
4.高吞吐量

JVM调优步骤

1.分析GC日志及dump文件,判断是否需要优化
2.确定JVM调优量化目标
3.确认JVM调优参数(根据历史参数调整)
4.对比调优前后差异
5.分析,调整,找到最合适的参数配置

JVM调优的命令

在这里插入图片描述

六.双亲委派模型

如果一个类加载器收到了类加载的请求,他首先不会去自己加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中(只有当父加载器无法完成加载请求时,子加载器才会去加载)

如何打破双亲委派机制?
1.自定义加载类,重写loadclass方法
重写这个方法以后就能自己定义使用什么加载器了,也可以自己定义加载委派机制,也就打破了双亲委派模型
2.使用线程上下文类加载器

七.JVM中对象访问定位(JVM栈里的方法是怎么指向堆里的对象的)

1.通过句柄方式访问
使用句柄方式访问,Java堆会划分出来一部分内存去作为句柄池来存储句柄,栈中存储的就是对象的句柄地址,而句柄中包含对象实例数据的地址和对象类型数据的具体地址信息

优点:
在对象被移动时,只会改变句柄的实例数据指针,栈中的指针不用修改,因为栈中存储的事句柄的地址

缺点:
需要二次定位,寻找两次指针,开销大

2.直接指针访问
Java栈直接与对象进行访问

优点:
速度快,节省了一次指针定位的时间开销,对象的访问在Java中非常频繁,因此可以减少执行成本

缺点:
在Java堆对象的布局中就必须考虑如何放置访问类型的相关信息(如对象类型,实现的接口,方法,父类等)因为没有了句柄

八.深拷贝和浅拷贝

浅拷贝:
只增加了一个指针指向已存在的内存地址(仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅拷贝出来的对象也会相应改变)

深拷贝:
增加了一个指针并申请了一个新的内存,使这个指针指向新的内存(释放内存是不会因为出现浅拷贝时释放同一个内存的错误)

区别:
浅拷贝是对指针进行拷贝,拷贝后两个指针指向同一块内存空间。类的复制构造函数是浅拷贝。
深拷贝不仅是对指针进行拷贝而且还对内容进行拷贝,拷贝完成后,指正指向的地址不一样,但是值是一样的。

使用自己定义的复制构造函数,使用深拷贝,构造函数将会被调用两次,析构函数会被调用两次;

如果使用类的默认赋值构造函数,就是浅拷贝,构造函数只会被调用一次,析构函数会被调用两次。

九.内存溢出场景

内存溢出的场景:
JVM运行时首先需要类加载器加载所需类的字节码文件。加载完毕交由执行引擎执行,在执行过程中需要一段空间来存储数据。这段内存空间的分配和释放过程正是我们需要关心的运行时数据区。 内存溢出的情况就是从类加载器加载的时候开始出现的,内存溢出分为两大类:OutOfMemoryError和StackOverflowError。

(一)java堆内存溢出

当出现java.lang.OutOfMemoryError:Java heap space异常时,就是堆内存溢出了。

问题描述:

1.设置的jvm内存太小,对象所需内存太大,创建对象时分配空间,就会抛出这个异常。

2.流量/数据峰值,当数据量突然增大并超过阈值时,那么就会停止正常运行的操作,并触发java . lang.OutOfMemoryError:Java堆空间错误

解决方法:

1.首先,如果代码没有什么问题的情况下,可以适当调整-Xms和-Xmx两个jvm参数,使用压力测试来调整这两个参数达到最优值。

2.其次,尽量避免大的对象的申请,像文件上传,大批量从数据库中获取,这是需要避免的,尽量分块或者分批处理,有助于系统的正常稳定的执行。

3.最后,尽量提高一次请求的执行速度,垃圾回收越早越好,否则,大量的并发来了的时候,再来新的请求就无法分配内存了,就容易造成系统的雪崩。

(二)java堆内存泄漏

问题描述:

Java中的内存泄漏是一些对象不再被应用程序使用但垃圾收集无法识别的情况。所以这些未使用的对象仍然在Java堆空间中无限期地存在。不停的堆积最终会触发java . lang.OutOfMemoryError。

将底层映射扩展到10,000个元素,而不是所有键都已经在HashMap中。然而事实上元素将继续被添加,因为key类并没有重写它的equals()方法。
随着不断使用的泄漏代码,“缓存”的结果最终会消耗大量Java堆空间。当泄漏内存填充堆区域中的所有可用内存时,垃圾收集无法清理它,java . lang.OutOfMemoryError。

解决办法:
解决方案比较简单:重写equals方法即可

(三)垃圾回收超时内存溢出

问题描述:

当应用程序耗尽所有可用内存时,GC开销受到了限制,而GC多次未能清除垃圾,这时便会引发java.lang.OutOfMemoryError。
(当JVM花费大量的时间执行GC,而收效甚微,而一旦整个GC的过程超过限制便会触发错误默认的jvm配置GC的时间超过98%,回收堆内存低于2%)。

解决方法:

要减少对象生命周期,尽量能快速的进行垃圾回收。

(四)元空间内存溢出

问题描述:

元空间的溢出,出现这个异常的问题的原因是 系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法 ,导致元空间的内存占用很大。

解决方法:

1.优化参数配置,避免影响其他JVM进程

-XX:MetaspaceSize:初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整 ,如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize:最大空间,默认是没有限制的

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 。
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

2.谨慎引用第三方包

对第三方包,一定要慎重选择,不需要的包就去掉。这样既有助于提高编译打包的速度,也有助于提高远程部署的速度。

3.关注动态生成类的框架

对于使用大量动态生成类的框架,要做好压力测试,验证动态生成的类是否超出内存的需求会抛出异常。

(五)栈内存溢出

问题描述:

当一个线程执行一个Java方法时,JVM将创建一个新的栈帧并且把它push到栈顶。此时新的栈帧就变成了当前栈帧,方法执行时,使用栈帧来存储参数、局部变量、中间指令以及其他数据。

当一个方法递归调用自己时,新的方法所产生的数据(也可以理解为新的栈帧)将会被push到栈顶,方法每次调用自己时,会拷贝一份当前方法的数据并push到栈中。因此,递归的每层调用都需要创建一个新的栈帧。这样的结果是,栈中越来越多的内存将随着递归调用而被消耗,如果递归调用自己一百万次,那么将会产生一百万个栈帧。这样就会造成栈的内存溢出。

解决办法:

如果程序中确实有递归调用,出现栈溢出时,可以调高-Xss大小,就可以解决栈内存溢出的问题了。递归调用防止形成死循环,否则就会出现栈内存溢出。

十.卡表

JVM堆空间通常被划分为新生代和老年代,由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么,需要跟踪从老年代到新生代的所有引用,费时又麻烦。
对于HotSpot JVM, 使用了卡标记技术来解决老年代到新生代的引用问题。具体是,使用卡表和写屏障来进行标记并加快对GC Roots的扫描。

卡表通常将堆空间划分为一系列2次幂大小的卡页。卡表标记卡页的状态,每个卡表项对应一个卡页。HotSpot JVM的卡页大小为512字节,卡表被实现为一个简单的字节数组,卡表的每个标记项为1个字节。
当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty。
具体方法: 首先,计算对象引用所在卡页的卡表索引号。将地址右移9位,相当于用地址除以512(2的9次方)。也可以这么理解,假设卡表卡页的起始地址为0,那么卡表项0、1、2对应的卡页起始地址分别为0、512、1024(卡表项索引号乘以卡页512字节)。其次,通过卡表索引号,设置对应卡标识为dirty。)

带来的2个问题
1.无条件写屏障带来的性能开销

每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作。显然,这会增加一些额外的开销。但是,与扫描整个老年代相比较,这个开销就低得多了。

不过,在高并发环境下,写屏障又带来了虚共享问题。

2.高并发下虚共享带来的性能开销
在高并发情况下,频繁的写屏障很容易发生虚共享(false sharing),从而带来性能开销。
假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行。HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB。如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能。

一个简单的解决方案,就是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表项未被标记过才将其标记为dirty。这就是JDK 7中引入的解决方法,引入了一个新的JVM参数-XX:+UseCondCardMark,在执行写屏障之前,先简单的做一下判断。如果卡页已被标识过,则不再进行标识。

与原来的实现相比,只是简单的增加了一个判断操作。
虽然开启-XX:+UseCondCardMark之后多了一些判断开销,但是却可以避免在高并发情况下可能发生的并发写卡表问题。通过减少并发写操作,进而避免出现虚共享问题。

也用于CMS GC
CMS在并发标记阶段,应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:

新生代的对象晋升到老年代;
直接在老年代分配对象;
老年代对象的引用关系发生变更;
等等。
对于这些对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,并发标记阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续阶段就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

十一.内存分配策略

分配原则:

1.优先分配到Eden

2.大对象可能直接被分配到老年代

为什么大对象直接被分配到老年代?
答:大对象一般是大的字符串或数组,它的存活时间比较长。因为新生代中垃圾回收算法一般采用复制算法,Eden区域执行gc频繁,若大对象存在于新生代中,每次执行gc都需要移动大对象,性能低,所以大对象放在老年代中,因为老年代gc次数比较少,性能好一些。

3.长期存活的对象可能被分配到老年代

//设置到达老年代存活的年龄阀值
//Survivor中有Age年龄计数器,每经过一次gc,存活下来的对象年龄+1,达到年龄阀值进入老年代
//jdk6之前默认年龄阀值为15,jdk6后长期存活年龄未必是15,也可能第2、3次就进入老年代了

-XX:MaxTenuringThreshold 15

4.创建对象时Eden区域内存不够,则会向老年代去借内存,即空间分配担保
*1、先检查老年代内存是否足够容纳下新生代的所有内存,若可以全部容纳下新生代的内存,则可以担保。
*2、检查参数是否开启 -XX:+HandlePromotionFailture +号开启 开启才能担保。
*3、验证老年代最大的可用连续空间是否大于历次竞争的老年代对象的平均值大小,大于平均值则担保。
*/

-XX:+HandlePromotionFailture

5.动态对象年龄判断

十二.对象头

Mark Word
锁标志位:用于JVM判定对象是否被锁住,以及锁膨胀优化。包括
无锁:新建一个对象的默认状态
偏向锁:只需比较 Thread ID,适用于单个线程重入
轻量级锁:CAS自旋,速度快,但存在CPU空转问题
重量级锁:需调用系统级互斥锁(mutex/monitor),效率低
GC标记:由markSweep使用,标记一个对象为无效状态
分代年龄:即在young区存活的次数,默认达到15次后进入old区(CMS默认6次),4bits最大值为15(32位/64位一样)
HashCode:调用 System.identityHashCode(…) 获得,HotSpot使用xor-shift算法
Thread ID:偏向锁偏向的线程id
Epoch:用于保存偏向时间戳

Klass Word:
即class指针(指向内存中对象的class类,例如通过getClass拿到类信息)

数组长度:
64位进程中占用64bits(8字节)的空间, 只有当对象是一个数组对象时才会有这部分 (这就解释了为什么数组对象的最大长度小于int最大值(32位:Integer.MAX_VALUE - 3;64位:Integer.MAX_VALUE - 2))

十三.停顿时间有两个概念,安全点和安全区域

安全点: 程序执行时,确定线程的状态
程序执行时并非在所有地方都能停顿下来开始GC , 只有在特定的位置才能停顿下来开始GC。如果安全点太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题

如何选择安全点:
可以选择一些执行时间较长的指令作为safe Point
循环的末尾
方法临返回前
调用方法之后
抛异常的位置

为什么选定这些位置作为安全点:
主要的目的就是避免程序长时间无法进入 Safe Point。比如 JVM 在做 GC 之前要等所有的应用线程进入安全点,如果有一个线程一直没有进入安全点,就会导致 GC 时 JVM 停顿时间延长。比如这里,超大的循环导致执行 GC 等待时间过长。

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
抢先式中断:(目前没有虚拟机采用了)
首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
主动式中断:
设置一个中断标志,各个线程运行到Safe Point的时 候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

安全区域
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,不能再运行到 Safe Point 上。
安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值