jvm内存模型
分为:虚拟机栈、堆、方法区、本地方法栈、程序计数器
虚拟机栈:每个线程都有一个私有的栈,随着线程的创建而创建。栈中存放了局部变量表(基本数据类型和对象引用)、方法出口等信息。栈的大小是固定的(也可以动态扩展,jvm未实现,需自己实现,将多个栈用链表连接起来)。如果超出栈的深度,会抛出StackOverflowError(栈溢出)错误
本地方法栈:主要是虚拟机用到的本地方法
程序计数器:jvm支持多个线程同时运行。每个线程都有自己的程序计数器。其保存的是当前执行的指令地址。
堆:是所有线程共享的地方,在虚拟机启动时就已创建。所有对象和数组都在堆内存上进行分配,这部分空间会被GC进行回收。当申请不到空间时会抛出OutOfMemoryError。堆内存溢出异常
方法区:所有线程共享。主要存储类信息,常量池,方法代码,方法数据等
内存溢出与内存泄漏
内存泄漏
对象可达,但是未被使用,一直占用内存空间,不会被GC进行回收。
举例:
Vector v=new Vector(10);
for (int i=1;i<100; i++)
{
Object o=new Object();
v.add(o);
o=null;
}
对象o放入到容器v中,然后设置为null(内存释放),但由于容器还引用此对象,所以GC并不会进行回收。
内存溢出
如果内存泄漏严重,就会导致内存溢出(大量无用对象一直未被回收,导致内存空间不足)
JVM年轻代晋升到年老代过程
部分对象会在Survivor From和Survivor To区中复制来复制去(默认交换15次),如果最终存活,就存入到老年代
Survivor为什么会有两个?始终保持一个空的Survivor区,可以避免内存碎片化
fullGC频繁怎么排查
full gc是发生在老年代的垃圾回收动作,采用的算法是:标记-清除算法。
当老年代没有足够空间时,会触发full gc
- 查看老年代(jdk1.8没有永久代)值设置是否过于小
- 是否有主动full gc的调用 System.gc();
- 是不是频繁创建来大对象(大对象,将直接进入老年代)
如果一次full gc后,剩余对象不多,说明Eden区设置太小,导致短生命old区;
如果一次full gc后,old区回收率不大,说明old区太小;
堆内存划分
划分为:新生代(Eden,S0,S1),老年代。
这样划分是为了使jvm能够更好的管理内存中的对象,包括内存的分配及回收
根据堆的分区,垃圾回收分为两种:Minor GC、Full GC
年轻代:存放最新创建的对象
分为3个区域:Eden,Survivor0,Survivor1。
当发生Minor GC,会将新生代和S From中的对象复制到S To区域中。如果在指定次数回收后仍然存在存活的对象,将会移动到年老代中。算法使用的是复制算法,每个对象都标注存活的年龄,如果对象年龄超过15(默认),就会成为年老代
年老代是full gc的主要区域,采用的是标记-清除算法。都是存活久的对象,所以full gc次数不频繁。每一次full gc都需要花费较久的时间
永久代是方法区的一种实现,不是堆内存的一部分
(可以设置其大小,也可以设置年轻代与年老代的大小比例)
MinorGC
当Eden区分配满时,触发MinorGC操作
算法
标记-清除:首先标记所有需要回收u的对象,标记完成后统一回收被标记的对象
复制:将内存容量大小相等的两块,每次只使用其中一块。当一块内存块使用完,就将活着的对象复制到另一块内存上,然后把原先的内存空间清除掉。
标记-压缩:标记出存活的对象,让其向一端移动,然后清理掉边界以外的内存
分代收集:将java堆进行分代,然后根据其特点采用适当的算法
判断对象是否存活:
引用计数法:每引用一次计数+1,失去引用计数-1(难以解决对象之间循环引用问题)
可达性算法:主流使用。从GC ROOT
G1与CMS收集器
https://blog.csdn.net/rlnlo2pnefx9c/article/details/79722384
G1,是一款面向服务器的垃圾回收器,主
要针对配备多颗处理器及大容量内存的机器,具备高吞吐量性能。整体来看是基于标记-整理算法实现。局部看是复制算法的实现。(响应量有限的并发收集器)
CMS,以最短回收停顿时间为目标的收集器,需要消耗额外的cpu和内存资源。在cpu和内存资源紧张时,会加重系统负担。运行在java虚拟机的老年代中,基于标记-清除算法实现的。(吞吐量优先的并行收集器)
JVM参数与调优
jvm对内存的划分,设置垃圾收集器
跨系统运行
java编译生成.class文件交由jvm解析运行
jvm是跨系统的,所以java就做到了一次编译,到处运行
.class文件会被直接加载到jvm中吗?
java类的加载是动态的,并不会将所有类全部加载后在运行,而是保证程序运行的基础类被完全加载到jvm,至于其他类,只有需要的时候才加载,这是为了节省内存开销。
对类进行初始化:new对象,反射加载,初始化子类父类也被加载,程序运行所需的基类
如何将类加载到jvm中
class文件是通过类的加载机制装载到jvm中的
类加载器有4种:
- Bootstrap ClassLoader 启动类加载器(负责加载JAVA-HOME/jre/lib/rt.jar的class文件,由C++实现)
- ExtcClassLoader 扩展类加载器(负责加载java平台中扩展功能的一些jar,AVA-HOME/jre/lib/*.jar)
- AppClassLoader应用类加载器(负责加载classpath中指定的jar包和class文件)
- 自定义类加载器
加载过程是双亲委派模型。即,如果一个类加载器收到了类加载器的请求,它首先不会自己尝试去加载,而是把请求委托给父加载器去完成,以此向上。
- 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
- 5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
好处是:防止内存中出现多份同样的字节码
类加载详细过程
- 加载(查找并加载类的二进制数据,并创建类对象)
- 连接(验证,准备,初始化)
- 验证:验证文件格式、元数据、字节码、符号引用
- 准备:为类的静态变量分配内存,并为其初始化默认值
- 解析:把类中的符号引用转换为直接引用
- 初始化()
jdk8下的jvm变化
JDK8把存放元数据中的永久内存从堆内存中转移到了本地内存,这样就不会报永久内存不够的错误。
提供了设置元空间大小的参数。如果不设置,将会自动增加元空间。
好处是:不需要进行调优及监控内存的使用情况。(并不能消除内存泄漏)
默认情况下,元空间大小只受本地内存限制
元空间的垃圾回收:在达到MaxMetaspaceSize设置的参数设定值时,将会进行垃圾回收
如果元空间经常进行垃圾回收,说明内存泄漏活着设置的大小不合适
类实例化顺序
- 父类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
- 子类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
- 父类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
- 父类构造方法
- 子类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
- 子类构造方法