JVM内存结构
JVM共享和隔离区域
线程共享:定义一个变量或者一个方法,多线程都可以同时访问、修改这个方法或者变量
线程隔离:就是数据不能被多个线程同时访问,某些数据只属于一个线程
运行时数据区分为:方法区,堆,虚拟机栈、本地方法栈、程序计数器。
线程共享区域:方法区、堆、
线程隔离区域:虚拟机栈、本地方法栈、程序计数器
方法区
类加载:JDK编译java文件后的class文件从磁盘加载到内存中,类一开始从磁盘加载到内存的时候先加载到方法区中
Class文件中除了有类的版本、字段、方法、接口等描述信息
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据,即永久代
jdk1.8中不存在方法区了,被元数据区(元空间)替代了,原方法区被分成两部分; 1: 加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,
运行时常量池保存在堆中;
字符串常量池
1、在JDK1.7前,运行时常量池+字符串常量池是存放在方法区中,HotSpot VM对方法区的实现称为永久代。
2、在JDK1.7中,字符串常量池从方法区移到堆中,运行时常量池保留在方法区中。
3、在JDK1.8中,HotSpot移除永久代,使用元空间代替,此时字符串常量池保留在堆中,运行时常量池保留在方法区中,只是实现不一样了,JVM内存变成了直接内存。
运行时常量区
运行时常量池是方法区的一部分
方法区是一个大数组,常量区是数组的一部分,类加载后的常量池表的内容存放到方法区的运行时常量池中
虚拟机栈
java中的栈通常是指虚拟机栈,更多情况只是指虚拟机栈中的局部变量,
虚拟机栈:线程私有的,每个方法执行的时候都会创建一一个栈帧, 用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的
栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
堆
创建对象是在堆中
堆: java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
本地方法栈
本地方法栈:线程私有的,保存的是native方法的信息,当-个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用调用操作系统提供的某些方法;
就是java的执行,最终需要把我们的方法翻译成操作系统可识别的语言,本地方法栈就是存放翻译后的方法
程序计数器
记录线程执行到哪一步,什么时候入栈,什么时候出栈、什么时候进入循环、跳转、异常,线程恢复等等由程序计数器来进行记录。也就是记录
记录着下一条JVM的指令地址
如果正在执行本地方法,这个计数器的值则应该为空
直接内存(堆外内存)
JVM占的内存是我们自己写的C和汇编所占据的内存,不属于JVM数据区一部分,属于本机运行内存。执行JVM的时候还需要调用操作系统内核
提供的方法,操作系统也需要消耗内存,这时就用到了直接内存,一般程序都消耗直接内存
知识扩展:操作系统调取内核提供的方法,内核连接驱动,驱动硬件只识别汇编语言,硬件中加了存储功能有一套翻译,支持C语言的识别
JVM内存模型(JMM)
Java内存模型(下文简称JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。
这一组规则被称为Happens-Before, JMM规定,要想保证B操作能够看到A操作的结果(无论它们是否在同一个线程),那么A和B之间必须满足
Happens-Before关系:(写后读)
●单线程规则:一个线程中的每个动作都happens-before该线程中后续的每个动作
●监视器锁定规则:监听器的解锁动作happens-before后续对这个监听器的锁定动作
●volatile变量规则:对volatile字段的写入动作happens-before后续对这个字段的每个读取动作
●线程start规则:线程start()方法的执行happens-before一个启动线程内的任意动作
●线程join规则:一个线程内的所有动作happens-before任意其他线程在该线程join(成功返回之前
●传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
所以说,Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性,另外,还确保正确同步的Java代码可以在不同体系结构的处理器上正确运行。
对Happens-Before的理解
如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1)线程A把本地内存A中更新过的共享变量刷新CPU高速缓存然后再刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量
这仍然遵循的是写后读思想
对象
1、在堆中分配对象选择哪种分配方式:
指针碰撞
假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,指针指的就是边界,内存的分配就是通过调整边界,删除就是通过调整边界规定为无效。
空闲列表
但如果Java堆中的内存并不是规整的,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;
而当使用CMS这种基于清除 算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存
对象的组成
对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据和对齐填充(8bit倍数)
对象头:两类信息
1、是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,
2、类型指针
对象的第三部分是对齐填充,
空指针问题NPE
空指针就是空引用,java空指针异常就是引用本身为空,却调用了方法,这个时候就会出现空指针异常。 常常是因为我们调用的对象是空的而抛出的异常。
1、最常用的一种就是直接对 对象进行判断,就是对代码的规范,参数校验之类的
比如if(Object == null)来对所有用到的对象进行判断,或者是用equals进行判断,常量放前面,变量放equals后面
比如在写servlet项目的时候
out.println(request.getParameter(``"username"``));
如果某个用户在输入数据时并没有提供表单 域"username" 的值,就会抛出空指针异常
如果返回是集合类型。而且是空的,不要返回null值,而是要返回一个空的集合,如果返回类型是对象的话,我们可以抛出异常。
2、
String userName = session.getAttribute("session.username").toString();
在一般情况下,如果在用户已经进行某个会话,则不会出现什么问题;但是,如果此时应用服务器重新启动,而用户还没有重新登录,(也可能是用户关闭浏 览器,但是仍打开原来的页面。)那么,此时该session的值就会失效,同时导致session中的session.username的值为空。对一个 为 null的对象的直接执行toString()操作,就会导致系统抛出空指针异常。
3、
public static void main(String args[]){
Person p=null;
p.setName("张三");
System.out.println(p.getName());
}
这个时候你的p就出现空指针异常,因为你只是声明了这个Person类型的对象并没有创建对象,所以它的堆里面没有地址引用,切忌你要用对 象掉用方法的时候一定要创建对象。
4、使用instanceof 操作符
即使对象的引用为null,instanceOf操作符可使用。当引用为null时,instanceof操作符返回false,而且不会抛出NullPointerException,比如
溢出
内存溢出OOM
内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,出OutOfMemoryError
整个栈或堆溢出
除了程序计数器,其他内存区域都有0OM的风险。
●栈一般经常会发生StackOverflowError, 一般是递归创建线程
●Java8常量池移到堆中,溢出会出java.lang.OutOfMemoryError: Java heap space, 设置最大元空间大小参数无效;
●堆内存溢出,报错同上,这种比较好理解,GC之后无法在堆中申请内存创建对象就会报错;
●方法区OOM, 经常会遇到的是动态生成大量的类、jsp等;
●直接内存00M,涉及到-XX:MaxDirectMemorySize参数和Unsafe对象对内存的申请。
排查0OM的方法:
●增加两个参数-XX:+HeapDumpOnOutOfMemoryError -
XX:HeapDumpPath=/tmp/heapdump.hprof,当0OM发生时自动dump堆内存信息到指定目
录;
●同时jstat 查看监控JVM的内存和GC情况,先观察问题大概出在什么区域;
●使用MAT工具载入到dump文件,分析大对象的占用情况,比如HashMap做缓存未清理,时间长了就会内存溢出,可以把改为弱引用。
栈溢出
虚拟机栈溢出 方法不断压栈,线程栈空间满了就会溢出 StackOverflowError,一般都是递归导致的
-Xss:规定了每个线程虚拟机栈及堆栈的大小,一般情况下,256k是足够的,此配置将会影响此进程中并发线程数的大小。
但是,如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
堆溢出
堆的内存不够创建对象了
**堆溢出分析方法:**对Dump出来的堆转储快照进行分析。第一步首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏还是内存溢出
-Xms:表示初始化JAVA堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。
**-Xmx:**表示java堆可以扩展到的最大值,在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。
内存泄露
一个对象已经没用了,但是还是有句柄指向它,利用不上
内存够,但是由于我们申请数组的内存空间是连续的,存在着和页和块一样大小的4KB的碎片空间,这样的话碎片空间是无法使用的,利用不上内存就是内存泄漏。
**排查内存泄露:**top-c先查看一下应用进程,c:显示进程完整的路径与名称。
再free命令用来显示内存的使用情况,以MB显示内存大小
df-h查看磁盘盘剩余空间
CPU占用率特别高,达到了900%。
我们的Java进程,并不做大量CPU运算,正常情况下,CPU应该在100~200%之间,出现这种CPU飙升的情况,要么走到了死循环,要么就是在做大量的GC。
使用jstat -gc pid [interval]命令查看了java进程的GC状态,果然,FULL GC达到了每秒一次。gcXXX各区域GC的详细信息,如-gcold
这么多的FULL GC,应该是内存泄漏没跑了,于是使用jstack pid > jstack.log保存了线程栈的现场,使用jmap -dump:format=b,file=heap.log pid保存了堆现场,然后重启了探测服务,报警邮件终于停止了。
分析栈
栈的分析很简单,看一下线程数是不是过多,多数栈都在干嘛。。
才四百多线程,并无异常
线程状态好像也无异常,接下来分析堆文件。
下载堆dump文件
堆文件都是一些二进制数据,在命令行查看非常麻烦,Java为我们提供的工具都是可视化的,Linux服务器上又没法查看,那么首先要把文件下载到本地。
由于我们设置的堆内存为4G,所以dump出来的堆文件也很大,下载它确实非常费事,不过我们可以先对它进行一次压缩。
gzip是个功能很强大的压缩命令,特别是我们可以设置-1-9来指定它的压缩级别,数据越大压缩比率越大,耗时也就越长,推荐使用-67,-9实在是太慢了,且收益不大,有这个压缩的时间,多出来的文件也下载好了。
使用MAT分析jvm heap
MAT是分析Java堆内存的利器,使用它打开我们的堆文件(将文件后缀改为 .hprof), 它会提示我们要分析的种类。
方法区和运行时常量池溢出
方法区和运行时常量池内存不够
HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代
在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,JDK7开始放在堆中
*String::intern() 高频率面试点
是一个本地方法,就是String A= “Java”,如果Java这个字符串一直存在于字符串常量池,它就返回字符串常量池中Java这个字符串的String对象的引用。如果String A= “renzilu” renzilu这个字符串不存在于常量池中,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用
public static void main(String[] args) {
String str1 = new StringBuilder(“计算机”).append(“软件”).toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder(“ja”).append(“va”).toString();
System.out.println(str2.intern() == str2);
}
这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。
因为在JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,但是字符串常量池在栈中,StringBulider对象创建需要在堆中
而在JDK7中,字符串常量池已经移到Java堆,只需要在常量池里记录一下首次出现的实例引 用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。而对str2比较返回false,这是因为“java”是内置的常用字符串,这个字符串在执行String-Builder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。
本机直接内存溢出
(堆外内存):除了消耗JVM内存,也要消耗操作系统内存
java变慢:栈、堆内存满了,或者堆外内存满了
怎么调用堆外内存:DirectByteBuffer类直接通过反射获取Unsafe实例进行内存分配,虽然使用DirectByteBuffer分配内存也会抛出内存溢 出异
常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存
的方法是Unsafe::allocateMemory()
栈堆的几个问题
0、基本类型存在内存的哪个区域?*
在方法中声明的基本类型的变量,调用时在栈中分配空间,结束时释放栈
在类中声明的成员变量,在堆中分配空间,不会因为方法销毁失效
对于引用变量,是对象的存储地址,存放在栈中
2、栈内存分配越大越好吗?
都是1024KB
windows根据虚拟内存分配
假设物理内存500MB 一个线程占用的栈内存越大 就会减少其他线程的数量
栈内存越大 只是可以进行更多次的方法调用 不一定越大越好
3、方法内的局部变量是否线程安全?
看多个线程对变量是共享的 还是一个线程私有的
如果方法体内的局部变量没有逃离方法的作用范围 就是线程安全的
如果局部变量引用了对象,并且逃离了作用范围,就需要考虑线程安全
常量池
JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类
对象常量池。
●Class文件常量池。在java代码的编译期间, 我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
●**运行时常量池:**方法区内
字符串常量池:在HotSpot VM中,它是一个叫做StringTable的全局表。string”或通常所说的"进入了字符串常量池的字符串”。存放在堆中
- 在JDK1.7前,运行时常量池+字符串常量池是存放在方法区中,HotSpot VM对方法区的实现称为永久代。
- 在JDK1.7中,字符串常量池从方法区移到堆中,运行时常量池保留在方法区中。
- 在JDK1.8中,HotSpot移除永久代,使用元空间代替,此时字符串常量池保留在堆中,运行时常量池保留在方法区中,只是实现不一样了,JVM内存变成了直接内存。
●基本类型包装类对象常量池: 这些类是Byte, Short,Integer,Long,Character,Boolean,。另外上面这5种整型的包装类也只是在对应值小于等于
127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
引用计数、可达性分析
引用计数:在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一,任何时刻计数器为0的对象就是不可能再被使用了。
但是如果有两个对象再无任何引用,但他们相互引用,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
**可达性分析:**这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
固定可作为GC Roots的对象包括以下几种:
1、·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
2、·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
3 、·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
4、·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
5、·所有被同步锁(synchronized关键字)持有的对象。
6、·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没 有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
finalize() 方法
finalize()方法是对象逃脱死亡的最后一次机会,稍后收集器将会对队列里的对象进行第二次小规模标记,二次标记后将会被回收。
对象的回收需要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件就是此对象是否有必要执行finalize() 方法。当对象没有覆盖finalize() 方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize() 方法,那么这个对象将会放置一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。finalize() 方法是对象逃脱被回收的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize() 方法中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,比如把自己赋值给某个类变量或者对象的成员变量,那么第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
引用–强软弱虚
加上关键字修饰可以代表不同的引用
-
强引用:gc时不会回收 当内存空间不足,JVM宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题
String s=“abc”
// 变量s = 字符串对象"abc"的一个强引用
// 注:只要给强引用对象赋空值null,该对象就可被垃圾回收器回收
// 即:只要给强引用对象s赋空值null,该对象就可以被垃圾回收器回收。因为该对象此时不再含有其他强引用 -
软引用:只有在内存不够用时,gc才会回收
内存敏感的数据缓存机制,如图片、网页缓存等
- 弱引用: 一旦发现弱引用对象,无论内存足否,都会进行回收
1.防止内存泄漏,保证内存被JVM回收
2.保护对象引用
-
虚引用:任何时候都可能被垃圾回收器回收
跟踪对象被GC回收的活动
触发垃圾回收的途径有哪些?
1、堆中内存已满 2、人工触发
垃圾回收算法
分代 垃圾回收需要触发,内存不够的情况下扫描,扫描一次活下来的对象就是一代,再扫描活下来的对象就是二代…
手动触发 System.out.gc
老年代:一般经历15次回收,或者年轻代满了,会放到老年代
还有大对象(引用着好多对象的对象,存在好多引用关系),也会进入到老年代
新生代:98%的对象都是朝生夕死,所以新生代用复制算法,因为它不会存在大量对象
老年代:存活率比较高,使用标记整理算法,整理是因为如果有内存碎片的话,可能会造成内存泄露
标记-清除算法
标记从根节点开始的可达对象,被标记的就是垃圾对象 清除:清除掉垃圾对象
缺点:标记清除效率是比较低的,清除后会产生大量不连续内存碎片,分配大对象没空间了,还得触发GC
老年代里都是些“老不死”的对象,假设你有一个文件夹,里面有 1000 张图片,其中 997 张是有用的,你会怎样删除这其中的 3 张垃圾图片呢?你肯定不会将 997 张有用的图片复制出去,然后将整个文件夹干掉,因为复制的代价太大了,耗时久,而且 997 张图片的位置都变了,反应在 java 对象上,就是 997 个 java 对象相互之间引用的地址都得换个遍。相反,你会挨个去删除 3 张垃圾图片,因为删除次数少,也不需要大量移动文件。所以老年代适合使用标记清除算法、不适合使用复制算法( 移动 997 张图片,就为了可以整体一次删除 3 个垃圾图片,好傻 )
标记-整理算法
和标记清除一样,但是不清理,而是将存活的对象都向另一端移动,然后清理掉边界以外的内存
优点:解决了内存碎片的问题
缺点:标记整理算法会Stop The World,所有的线程都停止,因为担心在整理的过程的中,产生新对象或者又有对象突然失效,但是这严重影响程序的性能
标记-复制算法
将内存分为大小相等的两块,每次只使用一块,当这块内存用完,就把存活的放到另一块中,然后再把使用过的这块内存清理掉,
新生代里绝大部分都是垃圾对象,可以使用复制算法将小部分存活对象复制到另一个区域,然后留下来的都是垃圾对象,可以一网打尽,一下子清除掉。因为存活的对象少,所以“复制”的次数少;虽然留下来的垃圾对象多,但是可以一网打尽,所以“删除”的次数也少。就像你有一个文件夹,里面有 1000 张图片,其中只有 3 张是有用的,你要删除余下的 997 张垃圾图片,你会怎么删除呢?你肯定不会一张一张的去删除 997 张垃圾图片,这样需要删除 997 次,你显然认为把 3 张有用的图片复制出去,然后将整个文件夹干掉来的快
优点:没有内存碎片
缺点:浪费空间, 对象存活率高的时候需要复制多次,效率低
垃圾收集器gc
新生代:98%的对象都是朝生夕死,所以新生代用复制算法,因为它不会存在大量对象
老年代:存活率比较高,使用标记整理算法,整理是因为如果有内存碎片的话,可能会造成内存泄露
Serial收集器:
标记-复制算法,只会使用一个处理器或一条收集线程去完成垃圾收集工作,而且会Stop The World ,而且停顿时间长会造成严重性能影响
单线程下最高效,额外内存消耗最小的 对于运行在客户端模式下的虚拟机来说是一个很好的选择
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法,仍然会Stop The World
ParNew收集器
是Serial收集器的多线程并行版本,多核的情况下,Stop The World停止的时间会短一些,为了尽可能缩短停顿的时间
标记复制算法
不同的代使用不同的收集器 早期版本ParNew(新生代使用)和CMS(老年代使用)收集器配合使用
CMS收集器
目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上
C/S 客户端或者手机端软件形式开发
B/S 浏览器网页开发
1.7 1.8 CMS和G1混合使用,可以自己设置,1.9开始默认使用G1垃圾收集器
处理老年代
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户
线程和GC线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
CMS收集器是基于标记-清除算法实现
1)初始标记 标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World
2)并发标记 垃圾回收和用户线程同时进行,不会Stop The World,标记所有old对象,能gcroot的对象不会清理,没有标记到的会被清理
3)重新标记 为了修正并发标记期间,因用户程序继续运作(方法出入栈)而导致标记产生变动的那一部分对象的标记记录,需
要“Stop The World,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短,
4)并发清除,标记-清理算法,可以用户执行和回收垃圾同时进行,只需要对标记的清理,不标记的不用清理,但是会产生垃圾碎片
CMS的问题:
1.并发回收导致CPU资源紧张:
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程
数是: (CPU核数+3) 14,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。
2.无法清理浮动垃圾:
在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束
以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。
3.并发失败( Concurrent Mode Failure) :
由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代
几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了92%的空间后就会触发CMS垃
圾回收,这个值可以通过-XX: CMSInitiatingOccupancyFraction参数来设置。
这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次”并发失败”(Concurrent Mode Failure),这时候
虚拟机将不得不启动后备预案: Stop The World,临时启用Serial Old来重新进行老年代的垃圾回收,这样-来停顿时间就很长了。
4.内存碎片问题:
CMS是-款基于**“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生**。内存碎片过多时,将会给大对象分配带来麻烦,往往会
出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在FullGC时开启内存碎片的合并整
理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外- -个参XX:CMSFullGCsBeforeCompaction,
这个参数的
作用是要求CMS在执行过若千次不整理空间的FullGC之后,下一次进入FullGC前会先进行碎片整理
(默认值为0,表示每次进入Full GC时都进行碎片整理)。
Parallel Scavenge收集器
标记-复制算法,能并行收集多线程,和Parallel Old组合使用
Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,所以关注的不是单次GC的时间,关注的总的gc的时间,一般客户端使用多
吞吐量:如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,1、控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数
2、直接设置吞吐量大小的**-XX:GCTimeRatio**参数。
Parallel Old收集器
:支持多线程并发收集,基于标记-整理算法实现, 关注吞吐量,单次执行时间会比较长
G1收集器
JDK1.9彻底取代CMS
G1不再使用年轻年老代分化,之前的收集器,就比如一个年轻代区域正在执行垃圾回收,刚标记完,用户线程来申请空间,会产生冲突,
G1收集器是把连续的Java堆划分为好多个大小相等的独立区域(Region),每一个Region都可以根据需求,扮演old区域或者新生代的Eden空间、两个Survivor空间。回收的时候也互不影响,这边用户线程申请空间,另一边回收,等着这边的回收完了,再去选择一个Region去回收
Region中还有专门储存大对象的Humongous区域,属于old区域的一部分,大对象:大小超过了一个Region容量一半的对象
当对象>=0.5Region,会被直接存到old区域的
当对象>1Region,会申请多个H区域跨区域去存储,这个就需要记录一下别的区域指向自己的指针,涉及到了哈希表,key值就是区域的起始地址,value记录指向哪个区域。
Java堆内存划分
新生代默认占总空间的1/3,老年代默认占2/3。
新生代有3个分区: Eden、 To Survivor0、From Survivor1,它们的默认占比是8:1:1.
新生代的垃圾回收(又称MinorGC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
老年代的垃圾回收(又称Major GC)通常使用”标记清理”或”标记整理”算法。
G1回收:
**年轻区域的回收:**分为E区和两个S区,需要用标记复制算法,把E区和一个S0区中存活的对象拷贝到S1区域
●对象优先在Eden分配。当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC.
在Eden区执行了第一次GC之后,存活的对象会被移动到其中个Survivor0分区;
Eden 区再次GC,这时会采用复制算法,将Eden和Survivor0区-起清理,存活的对象会被复制到Survivor1区;移动一次,对象年龄加1,对象年龄
大于15会直接移动到老年代。
GC年龄的阀值可以通过参数-XX:MaxTenuringThreshold设置,默认为15;
Survivor区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。
●大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
●老年代满了而无法容纳更多的对象,Minor GC之后通常就会进行Full GC,Full GC清理整个内存堆-包括年轻代和老年代。
old区域的回收:1、初次标记,标记gcroot达到的对象和他们所在的region(Rootregion),会Stop The World,但耗时很短,而且是借用进 行Minor GC的时候同步完成的,
2.并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较
长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
4、重新标记、用了SATB,效率更高,需要Stop The World,
5、复制清理,也会Stop The World, 对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移
动,必须暂停用户线程,由多条回收器线程并行完成的。
G1和CMS的区别:
cms并发标记清除:
最大的缺点就是stop the word 所有程序停止,进行垃圾回收,如果分了很大的内存,比如一个服务器256G的内存,给jvm分了100G的内存,在进行垃圾回收的时候,要停掉所有的程序,这绝对是不行的,会带了很大的隐患,这是很要命的。
**处理方法:**分为小服务器(256G分为多个8G的),类似集群
G1:g1把整个存储分为很多的小区域,每一个可以独立回收内存,元空间,其他的可以照常运行,不会产生stop the word。适合大内存的服务器
Minor GC
当Eden区满时,触发Minor GC。
频繁full gc的常见原因
full gc 触发条件是 老年代空间不足, 所以追因的方向就是导致 老年代空间不足的原因:
大量对象频繁进入老年代 + 老年代空间释放不掉
- 系统并发高、执行耗时过长,或者数据量过大,导致 young gc频繁,且gc后存活对象太多,但是survivor 区存放不下(太小 或 动态年龄判断) 导致对象快速进入老年代 老年代迅速堆满
- 发程序一次性加载过多对象到内存 (大对象),导致频繁有大对象进入老年代 造成full gc
- 存在内存溢出的情况,老年代驻留了大量释放不掉的对象, 只要有一点点对象进入老年代 就达到 full gc的水位了
- 元数据区加载了太多类 ,满了 也会发生 full gc
- 堆外内存 direct buffer memory 使用不当导致
- 也许, 你看到老年代内存不高 重启也没用 还在频繁发生full gc, 那么可能有人作妖,在代码里搞执行了 System.gc();
如何防止频繁fullgc
1、尽量不然创建这种大对象;
2、万一创建了尽量在新生代多保存一段时间(增大默认参数:15),最好在新生代被回收掉;
3、使用CMS垃圾收集器提供了一个可配置的参数-XX:+UseCMSCompactAtFullCollection开关参数,就是在Full GC之后会有一个碎片整理的过程,但是此过程内存无法并发的,停顿时间较长;还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,用于设置在执行多少次不进行内存碎片压缩的Full GC后,跟着来一次带压缩的。
类加载
类加载过程?
虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;
加载–验证–准备–解析–初始化–使用–卸载
加载:通过全类名获取类的二进制流,然后解析成静态数据结构存储在方法区,并在堆中生成一个便于用户调用的java.lang.Class类型的对象
验证:验证class文件的格式是够正确 。不会威胁到JVM安全
准备:为类的静态变量分配内存,并初始化值
解析:将类,接口、字段和方法符号引用转为直接引用 假设一个类A被编译成class文件,并且A引用了B,在编译阶段,A就需要一个字符串去记录B的地址,字符串就叫符号引用,在运行时,A类被加载,但是B未被加载,那么将加载B,此时A的符号引用会被替换成B的实际地址
**初始化:**下面有 总而言之就是类被调用了
什么时候触发类初始化
类首次被调用都会触发类初始化,当类(.class文件)没有被调用时,是在磁盘上储存,被调用时才会被加载到内存上,所以当tomcat启动时加载类文件,所以启动会比较慢
类初始化的六种情况
1、 1、使用new关键字实例化对象的时候
2、读取或设置一个类型的静态字段的时候
3、调用一个类型的静态方法的时候
2、反射调用触发
3、初始化一个类的子类,父类会在子类触发前先触发初始化
4、虚拟机启动指定包含main方法的类
5、接口的实现类初始化前 接口先初始化
6、手动触发类加载 class.forname
类加载器
类加载负责执行类加载,去磁盘进行识别,识别完后加载到内存
启动类加载器(BootStrapClassLoader) :用来加载java核心类库,无法被java程序直接引用;
扩展类加载器(Extension ClassLoader) :用来加载java的扩 展库, java的 虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
系统类加载器(AppClassLoader) :它根据java的类路径来加载类,一般来说,java应 用的类都是通过它来加载的;
自定义类加载器:由java语言 实现,继承自AppClassLoader;
双亲委派
当一个类加载收到加载请求,自己不会加载,而是往上传到Bootstrap ClassLoader,只有Bootstrap ClassLoader加载器无法加载,下面的加载器才会尝试去加载,无法加载就是类加载器无法在自己的负责的路径的找到该类 。而且这是一种逻辑关系 不是父子关系
双亲委派好处:大部分场景下,不需要程序去干涉程序中的类加载情况
为什么要用到双亲委派?
JVM规范每个类加载器都有自己的命名空间,也就是说用不同的类加载器加载了同一个限定名的类,JVM也会认为这是两个不同的类,这会产生混乱,所以就需要到了双亲委派:让一个限定名的类只被一个类加载器加载并解析使用
**遇到限定名-样的类,这么多类加载器会不会产生混乱?**2.越核心的类库被越上层的类加载器加载,而某限定名的类- - 旦被加载过了,被动情况
下,就不会再加载相同限定名的类。这样,就能够有效避免混乱。
**具体实现:**首先通过findLoadedClass判断这个类是否被加载过,然后定义一个pannrent变量,然后当parent==null代表panret为Bootstrap ClassLoader,就调用findBootstrapClassOrNull方法让Bootstrap去尝试进行加载,如果不等于null,就让panrent根据限定名去尝试加载该类并返回class对象,如果返回的为null,就说明panrent没有能力去加载这个类,就调用findClass方法去寻找该类。需要各个类加载器去自己实现,比如ExtClassLoader 和AppClassloader都有一段逻辑去实现,然后再调用defineClass去执行后续流程,defineClass被final修饰,不允许被重写,符合了类加载过程中除了加载阶段读取二进制流,其他阶段都是由JVM内部实现的原则
破坏双亲委派
第一破坏:
我们可以重写ClassLoader的loadClass方法,但是双亲委派的逻辑就是存在于这个方法,重写就会破坏
所以JDK1.2之后引入了findClass方法,重写这个方法而不是loadClass
第二次破坏:
各种数据库,JDK提供接口给他们,他们按照接口实现自己的类库,但调用JDK接口时会引起,接口中的类会引起第三方类库的加载,不符合自上而下的加载机制,出现父类加载器请求子类加载器去完成类加载的动作,
SPI
spi机制是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在JDBC中就使用到了SPI机制。
原生的JDBC中 Driver 驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库厂商去实现的。原生的JDBC中的类是放在 rt.jar 包的,是由启动类加载器进行类加载的,在JDBC中的 Driver 类中需要动态加载不同数据库类型的 Driver 类,而 mysql-connector-.jar 中的 Driver 类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,于是乎,这个时候就引入SPI,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
2、Tomcat
Tomcat为什么不使用默认的双亲委派模型?
Tomcat 作为一个 web 容器,存在以下使用场景:
1、部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个程序能使用不同版本的依赖
2、应用程序的类库都是独立的,保证相互隔离;
3、部署在同一个 web 容器中相同的类库相同的版本可以共享;
4、容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来;
2.2、Tomcat 如何实现自己的类加载机制?
Tomcat 自己实现了自己的类加载器:
CommonLoader:Tomcat最基本的类加载器,加载路径中的 class 可以被Tomcat容器本身以及各个 Webapp 访问;
CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的 class 对于 Webapp 不可见;
SharedClassLoader:各个 Webapp 共享的类加载器,加载路径中的 class 对于所有 Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个 Webapp 私有的类加载器,加载路径中的 class 只对当前 Webapp可见;
JspClassLoader:每一个JSP文件对应一个Jsp类加载器。
从图中的委派关系中可以看出:
CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,从而实现了公有类库的共用;
CatalinaClassLoader 和 Shared ClassLoader 自己能加载的类则与对方相互隔离;
WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个WebAppClassLoader 实例之间相互隔离;
JasperLoader 的加载范围仅仅是这个JSP文件所编译出来的那一个 .Class 文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 JasperLoader 来实现JSP文件的热插拔功能。
能不能自己写一个类,也叫java.lang.String
可以,但是即使你写了这个类,也没有用
这个问题涉及到加载器的委托机制,层层调用,最底下才是我们自定义的类,Java虚拟机会将java.lang.String类的字节码加载到内存中。
为什么只加载系统通过的java.lang.String类而不加载用户自定义的java.lang.String类呢?
因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类,
加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader,根据优先使用父类加载器原理,
AppClassLoader加载器的父类为ExtClassLoader,所以这时加载String使用的类加载器是ExtClassLoader,
但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrap,
父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。
所以,用户自定义的java.lang.String不被加载,也就是不会被使用。
调优命令
jps、jstat、jinfo、jmap、jhat、jstack
JDK下的指令,只有安装了JDK才能运行
jps查看虚拟机进程状况 jps -l、jps -m、jps -q、jps -v
jstat 显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jinfo 查看java详细配置信息
jmap 能获取对内具体的对象信息,转储当前的快照文件(dump快照信息)
jhat 用来分析jamp生成的快照 代替品:VisualVM、Eclipse Memory Analyzer
jstack 生成当前时刻线程快照 jstack -l 查看堆栈信息,显示线程的状态
java调优用的可视化工具
JConsole:在java的bin目录下的jconsole.exe,堆栈使用情况,CPU占用率
JHSDB:
VisualVM:运行监视和故障处理程序,
显示进程的配置、环境信息(jps、jinfo)
监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息(jstat、jstack)
dump以及分析堆转储快照(jmap、jhat)
JMC
调优
1、大内存导致垃圾收集困难,时间停顿长,用G1收集器缓解合适
解决stop the word
把一台服务器虚拟成好多台小服务器,在每台服务器上安装java运行
2、集群同步导致内存溢出
3、由windows虚拟内存导致的长时间停顿
操作系统虚拟内存:把暂时不用的页寄存在磁盘,用的时候再拉回到内存, 但是存在磁盘不慢吗?在磁盘存的基本都是hashMap结构,进行一次映射,取一次数据只经过一次磁盘,不会内存和磁盘来回交互,但还是会慢一些
ng.class,将其加载到内存中。这就是类加载器的委托机制。
所以,用户自定义的java.lang.String不被加载,也就是不会被使用。
调优命令
jps、jstat、jinfo、jmap、jhat、jstack
JDK下的指令,只有安装了JDK才能运行
jps查看虚拟机进程状况 jps -l、jps -m、jps -q、jps -v
jstat 显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jinfo 查看java详细配置信息
jmap 能获取对内具体的对象信息,转储当前的快照文件(dump快照信息)
jhat 用来分析jamp生成的快照 代替品:VisualVM、Eclipse Memory Analyzer
jstack 生成当前时刻线程快照 jstack -l 查看堆栈信息,显示线程的状态
java调优用的可视化工具
JConsole:在java的bin目录下的jconsole.exe,堆栈使用情况,CPU占用率
JHSDB:
VisualVM:运行监视和故障处理程序,
显示进程的配置、环境信息(jps、jinfo)
监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息(jstat、jstack)
dump以及分析堆转储快照(jmap、jhat)
JMC
调优
1、大内存导致垃圾收集困难,时间停顿长,用G1收集器缓解合适
解决stop the word
把一台服务器虚拟成好多台小服务器,在每台服务器上安装java运行
2、集群同步导致内存溢出
3、由windows虚拟内存导致的长时间停顿
操作系统虚拟内存:把暂时不用的页寄存在磁盘,用的时候再拉回到内存, 但是存在磁盘不慢吗?在磁盘存的基本都是hashMap结构,进行一次映射,取一次数据只经过一次磁盘,不会内存和磁盘来回交互,但还是会慢一些