JVM复习笔记

1. Java内存结构:

JVM复习笔记_第1张图片
Java内存结构
  • 类加载子系统:负责从文件系统或者网络加载Class信息,加载的信息存放在一块称之方法区的内存空间。
  • 方法区:就是存放类的信息、常量信息、常量池信息、包括字符串字面量和数字常量等。
  • Java堆:在Java虚拟机启动的时候建立Java堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放到
    Java堆中,堆空间是所有线程共享。
  • 直接内存:JavaNio库允许Java程序直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
  • 栈:每个虚拟机线程都有一个私有栈,一个线程的Java栈在线程创建的时候被创建,Java栈保存着局部变量、方法参数、同事Java的方法调用、
    返回值等。
  • 本地方法栈:最大不同为本地方法栈用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通过使用C语言写)
  • 垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理,下一节课详细讲。
  • PC(Program Couneter)寄存器也是每个线程私有的空间, Java虚拟机会为每个线程创建PC寄存器,在任意时刻,
    一个Java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,
    如果是本地方法,则PC寄存器值为Underfined,寄存器存放如果当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。
  • 虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。

2.类加载机制:

从类被加载到虚拟机内存中开始,到卸御出内存为止,它的整个生命周期分为7个阶段,加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。其中验证、准备、解析三个部分统称为连接。

JVM复习笔记_第2张图片
类的生命周期

1. 加载
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2. 验证

  • 2.1 文件格式的验证
    主要验证字节流是否符合Class文件格式的规范,如果符合则把字节流加载到方法区中进行存储。
    文件头、主次版本验证等等
  • 2.2 元数据验证
    主要对字节码描述的信息进行语义分析,保证其描述符合Java语言的要求。
    类是否有父类
    是否继承了不允许被继承的类(final修饰过的类)
    如果这个类不是抽象类,是否实现其父类或接口中所有要求实现的方法
    类中的字段、方法是否与父类产生矛盾(如:覆盖父类final类型的字段,或者不符合个则的方法)
  • 2.3 字节码验证
    最复杂的一个阶段。主要目的是通过数据量和控制流分析,确定程序语义是合法的,符合逻辑的。
    保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
  • 2.4 符号引用验证
    符号引用中通过字符串描述的全限定名是否能找到对应的类。
    在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
    符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

3. 准备
准备阶段正式为类变量分配内存并设置初始值阶段。
public static int value=123; 初始后为 value=0;
对于static final类型,在准备阶段会被赋予正确的值
public static final value=123;初始化为 value=123;
如果是boolean值默认赋值为:false
如果是对象引用默认赋值为:null
4. 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:简单的理解就是字符串,比如引用一个类,java.util.ArrayList 这就是一个符号引用,字符串引用的对象不一定被加载。
直接引用:指针或者地址偏移量。引用对象一定在内存(已经加载)。
5. 初始化
执行类构造器
初始化静态变量、静态块中的数据等(一个类加载器只会初始化一次)
子类的调用前保证父类的被调用

什么是类装载器ClassLoader
ClassLoader是一个抽象类
ClassLoader的实例将读入Java字节码将类装载到JVM中
ClassLoader可以定制,满足不同的字节码流获取方式
ClassLoader负责类装载过程中的加载阶段。

JVM中的类加载器

  • 启动类加载器(BootStrap ClassLoader):引导类装入器是用本地代码实现的类装入器,它负责将 jdk中jre/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
  • 扩展类加载器(Extension ClassLoader):扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将jdk中jre/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
  • 系统类加载器(System ClassLoader):系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

双亲委派机制:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

JVM复习笔记_第3张图片
类加载器双亲委派模型


3.垃圾收集算法:

  • 标记-清除算法:
    标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
  • 复制算法:
    这种收集算法将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,JVM生成的新对象则放在另一半空间中。GC运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。并且对于指定大小堆来说,需要两倍大小的内存,因为任何时候都只使用其中的一半。
  • 标记整理算法:
    标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往一端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
  • 分代收集算法:
    在上边三种收集思想中加入了分代的思想。

4.JVM参数:

-XX:+PrintGC 每次触发GC的时候打印相关日志
-XX:+UseSerialGC 串行回收
-XX:+PrintGCDetails 更详细的GC日志
-Xms 堆初始值
-Xmx 堆最大可用值
-Xmn 新生代堆最大可用值
-Xss 每个线程的堆栈大小
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例
-XX:NewRatio 新生代和老年代的比例
-XX:MaxTenuringThreshold 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代.对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率.该参数只有在串行GC时才有效.

回收器选择
-XX:+UseParallelGC 选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20 配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。


5.JVM调优策略:

核心目标:

  • GC的时间足够的小
  • GC的次数足够的少
  • 发生Full GC的周期足够的长

将新对象预留在年轻代
众所周知,由于 Full GC 的成本远远高于 Minor GC,因此某些情况下需要尽可能将对象分配在年轻代,这在很多情况下是一个明智的选择。虽然在大部分情况下,JVM 会尝试在 Eden 区分配对象,但是由于空间紧张等问题,很可能不得不将部分年轻对象提前向年老代压缩。因此,在 JVM 参数调优时可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。
通过设置一个较大的年轻代预留新对象,设置合理的 Survivor 区并且提供 Survivor 区的使用率,可以将年轻对象保存在年轻代。一般来说,Survivor 区的空间不够,或者占用量达到 50%时,就会使对象进入年老代(不管它的年龄有多大)
我们可以尝试加上-XX:TargetSurvivorRatio=90参数,这样可以提高 from 区的利用率,使 from 区使用到 90%时,再将对象送入年老代

让大对象进入年老代
我们在大部分情况下都会选择将对象分配在年轻代。但是,对于占用内存较多的大对象而言,它的选择可能就不是这样的。因为大对象出现在年轻代很可能扰乱年轻代 GC,并破坏年轻代原有的对象结构。因为尝试在年轻代分配大对象,很可能导致空间不足,为了有足够的空间容纳大对象,JVM 不得不将年轻代中的年轻对象挪到年老代。因为大对象占用空间多,所以可能需要移动大量小的年轻对象进入年老代,这对 GC 相当不利。
基于以上原因,可以将大对象直接分配到年老代,保持年轻代对象结构的完整性,这样可以提高 GC 的效率。如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于 GC 来说会是一场灾难。原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。因此,在软件开发过程中,应该尽可能避免使用短命的大对象。
可以使用参数-XX:PetenureSizeThreshold
设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。参数-XX:PetenureSizeThreshold只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数。

设置对象进入年老代的年龄
如何设置对象进入年老代的年龄
堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,年老对象存放在年老代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在 Eden 区,经过一次 GC 后依然存活,则被移动到 Survivor 区中,对象年龄加 1。以后,如果对象每经过一次 GC 依然存活,则年龄再加 1。当对象年龄达到阈值时,就移入年老代,成为老年对象。
这个阈值的最大值可以通过参数
-XX:MaxTenuringThreshold
来设置,默认值是 15。虽然-XX:MaxTenuringThreshold的值可能是 15 或者更大,但这不意味着新对象非要达到这个年龄才能进入年老代。事实上,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阈值年龄的最大值。即,实际晋升年老代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold中较小的那个。

总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,
这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。


6.常见OOM问题:

设置堆内存大小

  • 错误原因: java.lang.OutOfMemoryError: Java heap space 堆内存溢出
  • 解决办法:设置堆内存大小-Xms1m –Xmx10m -XX:+HeapDumpOnOutOfMemoryError
public static void main(String[] args) throws InterruptedException {
        List list = new ArrayList<>();
        Thread.sleep(3000);
        jvmInfo();
        for (int i = 0; i < 10; i++) {
            System.out.println("i:"+i);
            Byte [] bytes=  new Byte[1*1024*1024];
            list.add(bytes);
            jvmInfo();
        }
        System.out.println("添加成功...");
    }
 
 

设置栈内存大小

  • 错误原因:java.lang.StackOverflowError 栈内存溢出
    栈溢出 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。
  • 解决办法:设置线程最大调用深度 -Xss5m 设置最大调用深度
public class JvmDemo04 {
     private static int count;
     public static void count(){
        try {
             count++;
             count(); 
        } catch (Throwable e) {
            System.out.println("最大深度:"+count);
            e.printStackTrace();
        }
     }
     public static void main(String[] args) {
         count();
    }
}

Tomcat内存溢出在catalina.sh 修改JVM堆内存大小
JAVA_OPTS="-server -Xms800m -Xmx800m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxNewSize=512m"

你可能感兴趣的:(JVM复习笔记)