JVM汇总
0. JVM的运行流程
1. 类初始化流程
- 父类静态块、父类静态变量
- 子类静态块,子类静态变量
- 父类非静态块、父类非静态变量,父类构造函数
- 子类非静态块、子类非静态变量,子类构造函数
2. java类加载流程
加载
- 根据类名找到.class文件,将该文件读取到内存中
- 建一个数组,存储二进制流的结构
- 在内存中创建Class对象,作为作为类的数据访问入口
验证
- 验证.class文件是否正确,并且保证不会危害到虚拟机
- 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
- 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
- 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
准备
- 给类的静态变量准备内存,并初始化为默认值。
- 准备阶段不分配类的实例变量的内存,实例变量会在类实例化时,随着对象一起分配到Java堆中。
解析
- 符号引用转换为直接引用。
初始化
- 如果类有父类,那先初始化父类。
- 然后初始化该类,按照初始化顺序进行,非静态块、非静态变量、构造函数。
3. JVM的类加载机制
全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
4. JVM的类加载器
- 启动类加载器(Bootstrap ClassLoader):JVM启动的类加载器,JVM就像一个应用程序,启动时就要靠这个类加载器,相当于JVM的启动器。负责加载$JAVA_HOME/jre/lib/下核心API或者-Xbootclasspath选项指定的jar包,如java.lang.*
- 扩展加载器(Extension ClassLoader):加载位置 :jre\lib\ext中
- 系统类加载器(APP ClassLoader):加载ClassPath下的类,也就是我们程序中定义的类
- 自定义类加载器:必须继承ClassLoader,可以加载我们定义的路径
自定义类加载器怎么写?
- 继承Class Loader
- 重写findClass方法
5. JVM的构成
方法区
- 方法区就是永久代,会发生GC,主要是对方法区里的常量池和对类型的卸载
- 方法区主要存放虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等信息
- 方法区是被线程共享的
- 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
虚拟机栈
- 虚拟机栈跟栈的结构一样,是先进后出,调用一个方法就进栈,调用完方法后就出栈,每一个进栈出栈的叫栈帧。
- 栈帧分为:局部变量表、操作数栈、帧数据区
- 局部变量表:存放方法参数,方法运行时的局部变量,其本质就是一个通过索引来找数据的数组
- 操作数栈:程序运算时,临时数据的存储区域,其本质是通过进栈出栈来存放计算时临时数据的数组。如:计算a = b + c,首先会b进栈,然后c进栈,然后b、c都出栈,计算出a = b + c,然后a的值进栈,然后a的值出栈,存进局部变量表。
- 帧数据区:常量池的指针、方法返回值、方法返回异常
堆
- 线程共享的内存
- 创建的对象都存在这里,是垃圾回收的主要对象
本地方法栈(不需要深入学习)
- 和虚拟机栈类似,只不过本地方法栈为Native方法服务。
程序计数器(不需要深入学习)
- 记录程序运行到哪里
- 内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。
6. Java 8的内存分代改进
从永久代到元空间,永久代是启动时就定好内存大小,元空间是使用本地内存,主要是为了避免内存溢出。物理上,永久代就是方法区,永久代在堆里;但逻辑上方法区和堆是分开的。
7. 什么是垃圾回收(GC)
垃圾回收,就是将已经用完的对象回收掉,腾出内存空间给其他程序使用,并且这个是又JVM自动执行的,当发现内存不够用的时候,JVM就会自动执行,不用我们去手动回收。还有垃圾回收主要是回收堆中的对象。
8. 怎么判断一个对象需要垃圾回收
根搜索法:JVM定义一些GC Roots的对象,从这些对象一直往下找引用到的对象,最后没有被任何引用的对象就能被垃圾回收。
成为GC Roots的对象的条件:
- 虚拟机栈中引用的对象
- 方法区的类静态属性引用的对象
- 方法区的常量池引用的对象
- 本地方法栈中JNI(即Native方法)引用的对象
9. 什么时候进行垃圾回收
- GC在优先级最低的线程中运行,一般在应用程序空闲即没有应用线程在运行时被调用。
- Java堆内存不足时,GC会被调用。
10. JVM垃圾回收机制,描述一下垃圾回收的流程,主要说说怎么晋升到老年代
JVM中共划分为三个代:年轻代、年老代和持久代
- 年轻代:存放所有新生成的对象
- 老年代:在年轻代中经过N次垃圾回收,依然存活的对象,可以理解为生命周期较长的对象
- 永久代:java类,方法,静态变量等
年轻代垃圾回收叫Minor GC
老年代垃圾回收叫Full GC
年轻代的结构:1个伊甸园 + 2个生存区
垃圾回收流程:
- new一个新对象的时候,会优先在年轻代中的伊甸园分配空间,如果是大对象、大数组(一个连续的存储空间)会直接分配到老年代。
- 如果伊甸园空间不够,就会进行一次Minor GC,对象有一个岁数,每进行一次,岁数+1。
- 如果再次发现伊甸园空间不够,再进行Minor GC,将伊甸园和一个生存区存活的对象复制到另一个生存区。
- 其实年轻代使用的就是标记复制算法
- 当对象岁数大于限制(默认15,可以改)就可以晋升到老年代
- 如果晋升到老年代空间不够的情况下,就会进行Full GC
11. 堆中的内存分配
12. 有哪些垃圾回收器,各有什么优缺点,重点说说CMS、G1
串行收集器:单线程的垃圾回收器,适合单CPU,年轻代用标记复制算法,老年代用标记整理算法。
- 优点:速度快
- 缺点:垃圾回收时要整个系统停顿
并行收集器:多线程的垃圾回收器,适合多CPU,年轻代用标记复制算法,老年代用标记整理算法。
- 优点:性能速度比串行的更好
- 缺点:垃圾回收时要整个系统停顿
CMS收集器:这是一个以最短停顿时间为目标的垃圾回收器, 只是用作老年代,用标记清除算法。分为4个步骤:初始标记、并发标记、重新标记、并发清除。
- 优点:减少了垃圾回收引起的停顿时间
- 缺点:对CPU要求比较高,影响吞吐量,有内存碎片,所以需要更大的老年代空间或定期整理
G1收集器:将对分成多个小块,从而能更好得控制垃圾回收的小块,控制垃圾回收的时间,年轻代、老年代放在一起,使用标记整理算法。
- 优点:既能控制垃圾回收时间,也能去掉内存碎片
- 缺点:适合很大的堆
吐量优先和响应优先的垃圾收集器的选择
- 吞吐量优先的并行收集器:以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
- 响应时间优先的并发收集器:保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
13. 垃圾回收算法有哪些
标记 - 清除
- 优点:速度快
- 缺点:内存碎片
标记 - 复制
- 优点:没有内存碎片
- 缺点:需要双倍的内存空间
标记 - 整理
- 优点:不需要双倍空间,也没有内存碎片
- 缺点:虽然速度还行,但是比标记 - 清除慢点
分代回收
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记 - 整理 或者 标记 - 清除。
14. java内存模型
java内存模型(JMM)是线程间通信的控制机制.JMM定义了主内存和线程之间抽象关系。线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 线程B到主内存中去读取线程A之前已更新过的共享变量。
可以想想volatile的作用:用于修饰变量,每次修改,会马上写回到共享内存中,但是不能保证线程安全,i++。
15. JVM常见的错误及原因
java.lang.OutOfMemoryError:java heap space:堆内存溢出,也就是说堆中内存不够用了
解决方法:
- 首先看是内存泄漏还是内存溢出
- 如果是内存泄漏,要查看是不是因为资源未关闭导致,如:数据库连接
- 如果是内存溢出,一方面要看代码,看是否有创建很多对象或对象生命周期过长的情况,看看是否可以优化代码;另一方面可以调大检查JVM参数(-Xmx与-Xms),看是否有所改善。
java.lang.OutOfMemoryError:unable to create new native thread:没有足够空间创建线程,每新建一个线程,都要为其分配一定的内存(默认1M)
可创建线程数的计算公式
- 剩余内存 = 总内存 - Xmx(最大堆容量) - MaxPermSize(最大方法区容量)
- 可以创建线程的数量 = 剩余内存 / 线程的容量
解决方法:
- 其实每创建一个线程,都需要一定的内存
- 看看代码,是不是创建了太多的线程了,一般1000~2000个线程是没有问题的,如果创建线程太多,看能不能减少创建,或使用线程池
- 可以通过调整参数(-Xss)改小每个线程所需要的内存值
java.lang.StackOverflowError:栈内存溢出,栈的深度大于虚拟机栈所允许的深度
解决方法:
- 看看代码有没有太多次循环,如:死循环,这样就要优化代码
java.lang.OutOfMemoryError:PermGen space:这是方法区挤爆了,有可能是静态变量、方法、类、太多,或者jsp太多也有可能出现。
解决方法:
- 修改JVM参数(-XX:PermSize和-XX:MaxPermSize)
- 看看代码是不是JSP太多
16. JVM常用参数配置
跟踪GC情况:
- -XX:+printGC:打印GC简要信息
- -XX:+PrintGCDetails:后台打印垃圾回收详日志
- -Xloggc:log/gc.log:制定GC log的位置
- -XX:+PrintGCTimeStamps:打印GC发生的时间戳
堆的分配参数
-Xmx –Xms:指定最大堆和最小堆
-Xmn:新生代大小
-XX:PermSize -XX:MaxPermSize:永久代分配空间
-XX:NewRatio:新生代(eden+2*s)和老年代(不包含永久区)的比值
例如:4,表示新生代:老年代=1:4,即新生代占整个堆的1/5
-XX:SurvivorRatio(幸存代):设置两个Survivor区和eden的比值
例如:8,表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10
这两个一起用
-XX:+HeapDumpOnOutOfMemoryError:OOM时导出堆到文件
根据这个文件,我们可以看到系统dump时发生了什么。
-XX:+HeapDumpPath:导出OOM的路径
例子:-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
-XX:OnOutOfMemoryError:在OOM时,执行一个脚本,可以发邮件或重启程序。
例子:-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p //p代表的是当前进程的pid
栈的分配参数
-Xss:设置栈空间的大小。通常只有几百K;决定了函数调用的深度;每个线程都有独立的栈空间;局部变量、参数 分配在栈上
参考
http://blog.csdn.net/mr__fang/article/details/47723767
17. JVM优化