JVM
JDK、JRE、JVM关系
-
JRE=JVM+Java程序运行需要的类库(如rt.jar)
-
JDK=JRE+Java开发辅助工具(bin目录下的,如javac)
Java源程序编译运行过程
- java源程序->编译->.class->jvm上运行
JVM工作总体机制
-
元信息:类的元信息:类中有哪些方法,静态变量等。
-
方法区:jdk1.8之前叫永久代,1.8以后叫元空间,用来存放类的相关信息(元数据)、常量池。
-
java栈:线程私有的。
-
本地方法栈:本地方法在本地方法栈中运行,本地方法native是c++写的。
-
直接内存:物理内存分配内存给jvm,jvm再分成堆栈,给java程序使用。但是物理内存也可以不给jvm,直接给java程序使用,就叫做直接内存(NIO使用直接内存效率提高)。
-
pc程序计数器。
-
堆内存:存放new出来的对象,分新生代(伊甸区、幸存者区)、老年代。
-
执行引擎:.class字节码转成指令,在计算机CPU上运行。
-
本地方法接口:允许非java语言程序在jvm上运行。
JVM类加载器
-
启动类加载器:用c++编写的类加载器,在java环境下看不到,它把.class加载到jvm中。
-
扩展类加载器:com.misc.Launcher.ExtClassLoader
-
应用类加载器:com.misc.Launcher.AppClassLoader
-
自定义加载器:程序员自己开发一个类继承java.lang.ClassLoader,定制类加载方式。
-
父子关系1:启动类加载器是扩展类加载器的父加载器。
-
父子关系2:扩展类加载器是应用类加载器的父加载器。
双亲委派机制
-
当我们需要加载任何一个范围内的类时,首先找到这个范围对应的类加载器。
-
但是这个类加载器不是马上开始查找我们要加载的类。而是把任务给父加载器,直到找到。任务传递:PersonCLassLoader->AppCLassLoader->ExtCLassLoader->BootCLassLoader。查找和加载的顺序:正好相反。
-
全类名是类的唯一标识。
-
好处:避免类的重复加载,附加载器加载了一个类,子加载器不加载了。保证了jvm全类名是唯一标识。
-
避免恶意替换jre定义的核心API。
总体机制中不重要的部分
- 本地方法栈:负责本地方法运行时提供栈空间,存放本地方法每一次执行时创建的栈帧。
- 程序计数器PC:存放CPU下一条要执行的指令的地址。
元空间
-
不同版本的名称:
- 标准层面:方法区
- 具体实现:<=jdk1.6:永久代 ,=jdk1.7:提出去永久代,>jdk1.8元空间
-
元:描述数据的数据。如对象和类,类就是元数据或元信息。
-
元空间存储数据:类信息、静态变量、常量、运行时常量池、类中方法的代码。
方法栈
- 方法栈不是JVM的内存空间,是描述方调用过程的一个逻辑概念。
- 方法1调用方法2,方法2先结束,方法2后结束。
- 先进后出。
- 方法每次执行都会生成一个栈帧(在栈空间中申请一个栈帧),方法执行完栈帧会释放。
栈帧
-
栈帧存储:局部变量、动态链接、方法出口等信息。
-
栈帧结构:
- 局部变量表:方法执行时的参数,方法体内声明的局部变量。
- 操作数栈:存储中间运算结果,临时存储空间。如a++,先有一个临时的空间保存计算后的结果,再赋值回a,然后释放临时空间。
- 帧数据区:保存访问常量池指针,异常处理表。
-
操作数栈案例:
//从基本语法规则角度推测是34 int n = 10; n += (n++) + (++n); System.out.println(n); //结果是32 //过程:从局部变量表取变量n到操作数栈,操作数栈中的n++然后又++n,这两次++操作的是同一个n所以变成:n+=10+12,又有一次+=,因为又要一个临时空间,所以从局部变量表取变量n(10)到操作数栈进行计算:n=10+10+12
栈溢出异常
-
异常名称
java.lang.StackOverflowError
Exception下除了RunTimeException,都是编译时异常。
-
异常产生原因
方法栈中栈帧塞满了方法栈。如:写一个没有退出机制递归。会不断的产生栈帧但不会退出方法,不会释放栈帧,会导致方法栈塞满。
栈空间私有性
- 一个线程一个方法栈,某个线程的方法栈溢出,抛异常了不会影响到其他线程。
堆空间的总体结构介绍
- 堆空间:新生代、老年代
- 新生代:伊甸区、幸存者区
- 老年代
- 永久代(非堆)
- 内存示意图:
堆空间的工作机制
-
对象刚创建的时候放在伊甸区,程序进行,不断创建新对象,当伊甸区有点不够,会触发小gc(专门清理伊甸区的gc),小gc会清理那些没有被指针指向(没有引用等于这个对象)的对象。当小gc运行后,没有被清理的对象,就是幸存者,会被放到幸存者区,幸存者区分两个,其中一个幸存者区会把正在使用的对象移到另一个幸存者区,剩下的都是不在用的,这时可以认为这个幸存者区是空的,有新的对象就放在这个“空的”幸存者区,然后幸存者区的from和to指针会交换,口诀(复制必交换,谁空谁为to,不容易产生碎片)经历一次gc年龄就会加1岁,当年龄到达15的话就会把对象移动到老年代(存在时间比较长的对象。如ioc中的对象),如果幸存者区满了,即使对象没有到达15岁,也会被移动到老年代。
-
ioc中的对象:Tomcat->servlet->ioc->ioc中的组件。->表示引用,只要tomcat还在运行,ioc中的对象就不会被gc清理掉。
-
堆溢出异常
public static void main(String[] args) { List<Object> list = new ArrayList<>(); while (true){ list.add(new Object()); } }
在run中设置VMOPtion: -XX:+PrintGC 打印GC运行日志,-Xms200 jvm启动时的堆内存大小,-Xmx200m jvm最大的堆内存大小
运行程序可以看到Java heap space 堆内存溢出。
-
OOM的另一种异常:PermGen space 永久代(方法区)内存溢出:永久代一般用于存放类的元数据和常量池等信息,当一个程序中加载的类过多时,会把永久代的堆内存占满,出现永久代异常。
GC的基本问题
-
为什么要GC?服务器长时间运行,会产生很多对象,需要清理。GC处理的是堆内存,元空间(永久代)也会捎带清理。
栈内存不需要清理,因为栈内存中存放的是栈帧,运行一次方法就会产生一个栈帧,方法结束的时候会自动清理栈帧,所以不需要手动清理。
什么是垃圾对象:不再使用或者获取不到的对象。就是没有引用指向他的对象。
标记垃圾对象
-
引用计数法
在每一次对象被引用的时候,都给对象的专属的【引用计数器】加1,当引用技术器是0 的时候就会被gc清理。
隐患:当两个对象中的成员变量都引用了另一个对象时互的时候,即使把对象的引用置空,对象【引用计数器】也不会是0,这时候这两个对象永远也不会被gc清理
代码实例:
Member member01 = new Member(); Member member02 = new Member(); //setFriend方法是在对象中引用另一个对象 member01.setFriend(member02); member02.setFriend(member01); member01 = null; member02 = null;
这时候,两个对象的【引用计数器】都是1 并不会被gc处理掉,但是这两个对象的引用都被置空了,已经没有办法再拿到这两个对象了,已经是垃圾对象了,却没有清理。
这种互相引用的例子有:ServletContext 和IOC容器对象。ServletContext对象会用setAttribute方法将ioc容器存入应用域。在ioc中有ServletContext 对象,可以直接装配。
-
GC Roots可达性分析
核心原理:判断一个对象是否存在从堆外到堆内的引用。
对象可回收就一定会被回收吗?对象有finalize方法,判断对象是否执行了finalize方法,如果没有,就先执行,将当前对对象和GC Roots关联,如果不可达,就不会关联,将被回收,finalize方法只能被执行一次。
-
哪些对象可以作为GC Roots对象
GC Roots:作为根节点出发,顺着引用路径一直查找堆空间内的对象,GC Roots可以找到的对象。
虚拟机栈(java栈)中的局部变量
本地方法栈中JNI(本地方法栈)的局部变量
方法区中的类变量、常量
垃圾回收算法
-
引用计数法
在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其他对象引用,那么他的【引用计数器】加1,删除则减1。致命缺点:循环引用问题。
-
标记清除法
当堆中的有效内存被耗尽时,会暂停和挂起整个程序(stop the world),然后进行标记和清除。
标记:从根节点开始遍历所有对象,然后将所有存活的对象标记为可达的对象。
清除:清除会遍历堆中所有的对象,将没有标记的对象全部清除掉。
实现简单,效率低,因为标记和清除都要遍历所有对象,垃圾收集后会造成大量的内存碎片,垃圾收集时可能会造成程序暂停。
-
标记压缩法
标记:和标记清除法一样,把可达的对象标记出来。
压缩:移动所有的可达对象到堆内存的同一个区域,使他们紧凑排列。从而将所有非可达对象释放出来的内存集中在一起。
优点:优化了标记清除法,解决了碎片化的问题。
缺点:效率低,在标记清除法上又加了一步操作,效率更低了。
-
复制算法
把内存空间一份为二,一是有对象的(空间1),一半是空的(空间2),然后把空间1的可达的对象移动到空间2,空间1中剩下的对象都是不可用 的,直接全部清除。结果:空间2的对象就是可达的、连续的,空间1是空的。
优点:垃圾多时(新生代)效率比较高。清理后没有内存碎片。
缺点:浪费一半内存空间,在存活对象比较多的情况下,效率较差。
-
综合算法:
-
分代算法
新生代适合使用复制算法(幸存者区就是用的复制算法,从伊甸区移动到幸存者去也是用的复制算法)
老年代适合使用标记清除法和标记压缩法。老年代标记压缩法并不是标记清除后马上压缩,而是等连续空间不够了,再进行压缩。当然也可以马上压缩,具体看哪种jvm。
-
分区算法
将整个堆空间划分为连续的不同大小的区间,每个独立区间独立使用,独立回收,这样就不需要stop the world。这样就可以控制一次回收多少个小区间。相当于多线程并发GC,减少一次GC产生的停顿。
-
JVM参数设置入门
-
Runtime类使用案例
public class Demo09Runtime { public static void main(String[] args) { System.out.println("最大堆内存大小"); System.out.println(Runtime.getRuntime().maxMemory()/1024/1024+"M"); System.out.println("new前堆内存大小"); System.out.println(Runtime.getRuntime().totalMemory()/1021/1024+"M"); for (int i = 0; i <20 ; i++) { final byte[] bytes = new byte[1024 * 1024]; } System.out.println("当前堆内存大小"); System.out.println(Runtime.getRuntime().totalMemory()/1021/1024+"M"); } }
在VM options设置 -XX:+PrintGC 查看堆内存情况和GC情况
-
JVM常用参数调优设置
-Xms 堆内存的初始大小
-Xmx 堆内存的最大值
-Xmn 新生代大小
-XX:PermSize 设置持久带最大值
-Xss 每个线程的堆栈大小
-XX:New Ratio年轻代与老年代的比值
-XX:SurvivoRatio 伊甸区和幸存者区的大小比值。