一、JVM内存模型和存储结构
- 线程栈:JVM规范让每个Java线程拥有自己的独立的JVM栈,也就是Java方法的调用栈。当方法调用的时候,会生成一个栈帧。栈帧保存在虚拟机栈中,栈帧存储着方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。线程运行中,只有一个栈帧处于活跃状态, 称为当前活跃栈帧,当前活动栈帧始终是JVM栈的栈顶元素。
- 方法区:类的基本信息、静态变量。
- 本地方法栈:基本数据类型,及对象引用。
- 堆:对象实例
- 程序计数器:当前线程所执行的字节码的行号指示器。
二、JVM中堆的知识点
1. 堆的分区
堆被分为新生代、老年代。其中新生代又被进一步划分为Eden区和Survior区,Survior区由细分为FromSpace和ToSpace。
新生代:新建的对象都是由新生代分配内存,Eden区空间不足的话,会把存活的对象存入Survior区。
老年代:用以存放新生代中经过多次垃圾回收后仍然存活的对象。
2. 垃圾回收策略
存在三种垃圾回收与策略,分别是标记清除、复制、标记整理算法。
3. 垃圾回收过程
新生代就是主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。Eden区和Survior的FromSpace区,在经过Minor GC 新生代垃圾回收后,存活的对象会移动到ToSpace区,之后清空Eden区和FromSpace区,最后ToSpace变为FromSpace。这就是垃圾回收策略中复制算法。Eden:FromSpace:ToSapce=8:1:1。
新生代采用空闲指针的方式来控制GC的触发,指针保持最后一个分配内存的对象在新生代区间的位置,当有新的对象要分配内存的时候,用来检查空间是否足够,不够的话就会触发GC。当连续分配对象时,对象会逐渐从Eden到Survior,最终进入老年代。
MinorGC的过程:MinorGC采用复制算法。首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。
主要有下面三种方式:大对象,长期存活的对象,动态对象年龄判定
-
大对象直接进入老年代。比如很长的字符串,或者很大的数组等,参数-XX:PretenureSizeThreshold=3145728设置,超过这个参数设置的值就直接进入老年代
-
长期存活的对象进入老年代。在堆中分配内存的对象,其内存布局的对象头中(Header)包含了 GC 分代年 龄标记信息。如果对象在
eden 区出生,那么它的 GC 分代年龄会初始值为 1,每熬过一次 Minor GC 而不被回收,这个值就会增 加 1 岁。当它的年龄到达一定的数值时,就会晋升到老年代中,可以通过参数-XX:MaxTenuringThreshold设置年龄阀值(默认是 15岁) -
当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor
空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而不需要达到默认的分代年龄。
老年代:主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记—清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
4、垃圾收集器
新生代GC :串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)
串行GC:在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
并行回收GC:在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
并行GC:与老年代的并发GC配合使用。
老年代GC:串行GC(Serial MSC)、并行GC(Parallel MSC)和并发GC(CMS)。
串行GC(Serial MSC):client模式下的默认GC方式,可通过-XX:+UseSerialGC强制指定。每次进行全部回收,进行Compact,非常耗费时间。
并行GC(Parallel MSC):吞吐量大,但是GC的时候响应很慢:server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。
并发GC(CMS):响应比并行gc快很多,但是牺牲了一定的吞吐量。
三、类加载知识点
1、类加载机制
类加载的步骤主体分为加载、连接、初始化。
-
加载:将class字节码文件加载待内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态变量、常量池等),在堆中生成一个Class类对象代表这个类(发射原理),作为方法区类数据的访问入口。
-
连接:细分为三个部分:验证、准备、解析。
a.验证:验证类的文件格式、元数据、字节码、符号引用,确保符合JVM规范,没有安全方面的问题。
b.准备:为类的静态变量分配内存并设置类变量初始值,这些内存在方法区中进行分配。
c.解析:虚拟机常量池中的符号引用替换为直接引用(地址引用)的过程。 -
初始化:执行类构造器的过程。类构造器是由编译器自动收集类中所有类变量的赋值当作和静态语句块中的语句合并产生的。这个操作会把静态变量赋予正确的初始值。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先初始化其父类。虚拟机保证一个类的构造器在多线程环境中被正确加锁和同步。
2、双亲委派机制
类的加载器存在是三个:
- Bootstrap ClassLoader/启动类加载器:主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作.
- Extension ClassLoader/扩展类加载器:主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作
- System ClassLoader/系统类加载器:主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作.
工作过程:
- 当AppClassLoader加载一个class的时候,他首先不会自己去尝试加载这个类,而是把这个类加载请求委派给父类加载器ExtensionClassLoader去完成。
- 当ExtensionClassLoader加载一个class时,首先不会自己去尝试加载这个类,而是把类加载请求为委派给父类加载器BoostrapClassLoader去完成。
- 如果BoostrapClassLoader加载失败(例如在$JAVA_HOME/lib里面未查找到该class),会是使用ExtensionClassLoader来查找加载。
- 如果ExtensionClassLoader也加载失败,则会使用AppClassLoader来加载。
- 如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
优点:
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
3、类实例化顺序
Java程序初始化顺序:
- 父类的静态变量;
- 父类的静态代码块;
- 子类的静态变量;
- 子类的静态代码块;
- 父类的非静态变量;
- 父类的非静态代码块;
- 父类的构造方法;
- 子类的非静态变量;
- 子类的非静态代码块;
- 子类的构造方法