未经允许所有内容都可都可都可转载,不对的地方欢迎指正

JDK体系结构

JDK = jre (java运行时环境) + tool (编译器,打包工具等) JRE = java虚拟机 + java常用API

Javac

javac 将java文件编译成字节码文件.class,因为jvm识别的是字节码文件。 javac 做的就是.java文件和.class文件的对等信息转换。

类的加载机制

  • 装载 -> 连接 -> 初始化 -> 使用 -> 卸载
  • 装载

    1. 将class文件转化为——> 字节流 ——> 由类加载器加载
    2. 类加载器将自己流中所代表的静态数据接口 ——> 转化为方法区的运行时数据
    3. java堆中产生一个class文件对象 ——> 作为方法区中静态数据访问入口
  • 连接 (验证—> 准备 —> 解析)

    1、 验证: 验证字节码文件是否正确 2、 准备: 为类变量分配内存,赋值默认值,例如 private static int a = 1; 此时test分配的默认值为0 3、解析: 将符号引用变为直接引用,在类的编译期间,类中的引用在内存中的地址无法确定。因此都市符号引用。 例如: A.class调用了 B.class的test方法,A就包含了对B整个类的符号引用。通过解析,jvm会把整个引用编程一个指针指向B类test中方法在方法区中的位置。这个指针就是直接引用。

类加载器结构

启动类加载器 Bootstrap ClassLoader : C 语言编写,主要负责加载 %JAVA_HOME%/lib下的jar包和class文件。 扩展类加载器 ExtClass Loader: 负责加载 %JAVA_HOME%/lib/ext文件夹下的jar包和class文件。 AppClassLoader 负责加载classpath下的类文件。 如果我们需要自定义类加载器,需要继承AppClassLoader。

双亲委派、父类委托

1、当一个类需要被加载,先使用类名加锁。 2、查看该类是否被加载,如果没有加载继续。 3、在程序没有自定义类加载器的时候,程序写的类都是AppClassLoader加载的。他的双亲就是ExtensionClassLoader和BootStropClassLoader,双亲委派就是指我们的类被加载时,AppClassLoader不会直接加载,先交给父类ExtClassLoader加载,如果父类ExtClassLoader无法加载,ExtClassLoader也不会加载,继续往上交给BootStrapClassLoader。只有父类加载器无法加载的时候,参会由子类加载器加载。如果BootStapClassLoader 由上到下都无法加载,程序最终会抛出ClassNotFoundException (每个类加载器找不到都会抛出整个异常,源码中写的只有最后一个子类找不到才会抛出)

双亲委派的好处

如果没有双亲委派,每个类加载器都可以不管父类,自行加载类。 会导致同名实际上不同的类被同时加载。 例如,我么可以在src下建立ava.lang包,建立一个String类,在有双亲委派的机制下,我们自己编写的类可以正常编译,但是无法被正常加载。 如果没有双亲委派,类会被重复加载造成混乱。同时核心jar有被篡改的风险。

如何打破双亲委派

自定义类加载器,重新loadClass方法。

运行时数据区

  • 包括 堆、虚拟机栈、本地方法栈、程序计数器、方法区

    方法区

    1、类型信息(类信息):

    1. 1 类的完整有效名称,即包名加类名
    2. 2 父类完整有效名称
    3. 3 类的修饰符 public abstract final
    4. 4 实现的直接接口的有序列表

    2、域名信息:(属性信息)

      1) 属性字段的声明顺序
      2) 字段的修饰信息(private protected public)
    

    3、方法信息:

      1) 声明顺序
      2) 返回值类型
      3) 参数类型
      4) 修饰符信息
      5) 方法的字节码、操作数栈、局部变量表
      6) 异常表, 每个异常处理的开始位置、结束位置、被捕获的       异常类的常量池索引
    

    4、运行时常量池:

栈(线程私有)

java虚拟机栈:每调用一个方法,就会进一次栈,线程私有 栈帧: invoke一个方法就会压一次栈 本地方法栈: native方法

程序计数器(程序计数器)

多线程执行时,抢占cpu资源,每个线程执行过程中,如果没有了执行权,程序计数器会记录该线程所执行到的位置即栈帧信息,到重新抢占到执行权后,从程序计数器记录位置继续执行。 每个线程有一个程序计数器,即线程私有。

栈帧

  1. 局部变量表: 局变变量、方法参数
  2. 操作数栈: 计算数据
  3. 动态连接: 符号引用变为直接引用,多态
  4. 返回地址: 正常 (return 或者 void),返回PC程序计数器返回地址值。

    异常后如果外层无捕捉,需要异常处理器处理。
    无论正常异常都需要返回到该方法指令的下一条指令的地址。
    

    Tread.currentThread.getStackTrace() 就是通过栈帧获取方法名称等信息。

public static类变量 和 public static final全局变量的初始化时间

  1. non-final 类变量:

    1) 静态变量和类关联在一起,随着类的加载而被加载。
    

    2) 所有实例共享,即使没有实例也可以访问。 初始化时间:在类加载的第二个阶段(装载、连接、初始化、使用、卸载)的连接阶段会被赋值默认值。然后在第三个阶段初始化阶段 会被显示初始化(赋值)

  2. final类变量:

    编译期就确定了final不可变对象,连接的第二个阶段又分为了(验证、准备、解析)三个阶段。 final类变量的初始化时间为准备阶段、而non-final的类变量在连接阶段的准备阶段只是赋值了默认中,在第三阶段初始化阶段才会被赋值。
    

对象在内存中的布局

  1. 对象头

1) markword (hash码,分代年龄,锁状态标志) 2) classPoint (指定对象对应的类元数据中的内存地址) 3) length (数组对象特有的长度)

  1. 实例数据
  2. 填充部门,保证对象大小为8字节的整数倍。

为什么是8的整数倍? 64位操作系统,每次寻址的单位为64bit,即8b, 8字节。

堆内存

  1. 年轻代 8(Eden):1(S0):1(s1)

1) 年轻代的特征,朝生夕灭,GC比较频繁,采用的是复制算法。 2) 由于采用的复制算法,因此 S0和S1的大小必须相等,这个是能复制的前提。 3)复制算法必须要保证S0 或者 S1 始终有一个是空着的。也就是说,Eden的空间最高达到90%就要进行垃圾回收。 4) 一般情况下,是内存满了,才gc,如果不是8:1:1,如果是 1:1:1 ,就意味着在内存到66% 的时候就要进行GC,这个显然是不合理的。 90% 与100% 比较接近。 而且 大多数对象都不会进入 幸存区。 综合 幸存区放10%的空间就足够 和 90%与100% 相差不大。默认采用了8:1:1

  1. 老年代:

1) 与eden的比例比例为2:1 2) 为什么是2:1,首先,当年轻代的大对象放不下,直接交给老年代管理,极端场景下,即年轻代为空老年代直接放的场景,也要保证老年代空间可以满足整个机制,所以,老年代至少要比年轻代大。 其次,经过15次 minorGC无法回收的对象也需要往来年代放,综合考虑设置为2:1

老年代、元空间、永久代、方法区?

  1. 方法区: 是JVM规范中定义的一个概念,用户存储类信息、常量池、静态变量、JIT编译后的代码数据等等。具体放在什么地方,不同的实现可以放在不同的地方!!!
  2. java7之前,永久代类似方法区的实现,没有把所有东西放永久代。
  3. java8,永久代被移除,取代的是一块与堆不相连接的一块本地内存- 元空间(Metaspace).

为什么要分young和old

内存中有两种对象: 一种朝生夕灭,声明周期非常短。 另一种声明周期比较长,经过很长时间依然被引用。 在内存中的大多数对象都属于前者。 如果不分young和old,会导致每次垃圾回收都要进行全局扫描,那些声明周期较长的对象可能会别多次垃圾回收重复扫描,全局扫描时间慢、浪费系统资源。

为什么young要分eden和survivor?

主要是解决内存不连续的。 年轻代采用的垃圾回收算法是复制算法。 如果不分,年轻代总大小为10,经过一次垃圾回收后,回收了9个,剩一个存活对象,把年轻代空间看做一个大小为10的数组,如果存活对象不再数组下标0,或者下标9的位置上,此时如果新产生一个大小为9的对象,理论上剩余空间为9能放下,实际上由于空间不连续,大小为9的对象年轻大放不下。 而分了eden和survivor后,经过一次minorGC后,剩余存活的对象会被放在survivor区的s0或者s1中,剩余的eden区域内存空间是连续的,就不会产生实际上空间够,而由于内存不连续放不下的问题。

为什么经过15次GC后,young的对象会被放入old?

对象头中的markdown信息存储了分代年龄,大小为4bit, 最小值是0000 最大值是1111 ,转化为十机制就是15.

MetaSpace,为什么使用元空间代替永久代?

永久代主要存储的是类相关的信息,所以对于冬天生成类的场景,比较容易出现内存溢出。 最典型的就是在JSP页面较多的时候,容易出现永久代内存溢出 PermGen Space. 元空间的本质和永久代类似。 都是JVM规范中方法区的实现,不同的是元空间使用的本地内存,而不是JVM虚拟机内存。 类和方法的信息是比较难确定大小的,太小容易出现永久代溢出。 元空间直接使用本地内存,默认情况下仅受本地内存限制。

什么时候发生栈内存溢出?

  1. 栈的声明周期,线程私有,和线程同周期
  2. 存放什么? 方法每压一次栈,存放一个栈帧,栈帧存放了局部变量表,操作数栈、动态连接、方法出口等信息。
  3. 放不下栈帧,即创建栈帧的请求超过了栈的最大深度。
  4. 怎么解决 先代码后参数 xss.

什么时候发生堆内存溢出?

存放的是什么? 对象,所有线程的对象都在堆中存放。 怎么放? new一个object就放一个。 怎么回收? 可达性分析算法回收,即一个对象直接或者间接关联Gcroot节点。就不是垃圾。 new出来的对象要往堆中存放,而堆中经过fullGc后,依然没有足够空间存放,发生内存溢出。

内存泄漏怎么发生的?

本该被回收的对象,却没有被回收,GCRoot跟节点间接可达。 1) 长生命周期的静态集合类对短生命周期的对象的引用。 2) 各种连接、数据库连接、网络连接、IO连接等。连接不关闭就不是垃圾。无法回收。 比如FileInputStream 这些类都重写了finalize方法。 3) 变量不合理的作用域, 本应该在方法中结束生命周期的对象,定义在类中就和类同生命周期。 4) 改变hash值,当一个对象被存储在hashset中后,改变hash值后,remove时就无法找到整个对象,造成内存泄漏。

引用类型?

  1. 强引用: 工CGRoot跟节点开始,有直接引用关系就是强引用。特点,如果内存空间不够,不会回收内存,直接抛出OOM异常。
  2. 软引用: 使用SoftReference 创建对象的软引用,适用于缓存。当内存空间不够时,会释放引用,回收对象。
  3. 弱引用: 通过WeakReference 创建对象的弱引用,只要经过垃圾回收,即只要垃圾回收器扫描到,就直接回收。
  4. 虚引用: 通过PhantomReferfence实现。用来跟踪对象被添加到队列的时刻,所以需要和队列一起使用,无法通过get获取虚引用的对象,因为他的get被重写了,返回的永远都是null . 构造方法需要通知指定对象和队列。 如果当执行玩对象的finalize方法会加入队列。

ThreadLocal

说一说OOM发生时你的定位思路?

  1. ps -ef | grep java 或者 top 找到对应的java进程的PID。
  2. 确认下内存分配大小和 使用情况 jmap -heap {PID}
  3. 将类名实例数大小排序 jmap -histo:live {PID} more
  4. 或者直接jamp -histo:file xxx 指定dump文件导出当前内存信息。
  5. jstack {PID} 打印栈信息。
  6. 使用 eclipse 的Mat memoryAnalyzer.exe分析dump文件。
  7. 也可以使用jvisulvm 直接连接查看。

什么是垃圾对象

java中操作对象都是通过引用操作的。 如果一个对象没有任何引用(引用计数法判断),就是垃圾。 主流的垃圾回收器都摒弃了这种算法,因为java中有循环引用

Object A = new A(); // A的引用数为1
Object B = new B(); 

A.setB(B);
B.setB(A); // A的引用数为2.

A = null; // 此时A的引用数为1
B = null; // B为null,但是A还是被B的 A属性引用,A的引用数还是1,无法被回收。

可达性分析算法(跟踪搜索算法)

对象到GCRoots根节点如果存在直接或者间接引用,那对象就不是垃圾。 GRRoots根节点是什么?

  1. 虚拟机栈中局部变量表存储的对象的引用。
  2. 方法区中静态属性的引用。
  3. 方法区中常量的引用。
  4. 本地方法栈中JNI引用,即Native方法中局部变量引用的对象。

什么时候GC

堆内存分为老年代和新生代。 而新生代又分为Eden区和Survivor区。 一般情况下,对象创建出来都在Eden区,处分Eden大小放不下的大对象直接进入Old区。整个大小通过虚拟机参数pretenureSizeThreadhold参数指定。 当Eden区达到90%使用空间时,发生minorGC,也叫youngGC, 会将年轻代中存活对象年代信息增加1,通过复制算法,放入survivor的s0或者s1. 发不下的进入老年代。 当Old区对象放满后,有新对象放入,发生majorGC,也叫FullGC。

标记清除算法

标记: 根据可达性分析算法,标记内存中有哪些对象可以回收,哪些对象还可以继续使用。 清除: 清除掉跟GcRoot跟节点不可达的对象。 缺点: 标记清除效率低、随着对象的不断增多,效率就会下降,比如新生代中对象声明周期都很短,就不适合。 清除之后内存空间不连续,即有空间碎片,而太多的空间碎片可能导致新对象放不下。从而频繁GC.

复制算法

将内存划分为大小相等的两块,每次只使用其中一块 当使用的这一块空间不够时,将存活的对象放到另一块,然后将本块清空。 缺点: 内存代价太高,有效使用空间永远只有一半。 优点: 没有内存碎片。在对象存活率低的场景下非常适用。 年轻代对象存活率低,surivivor区中的from和to就是这种复制算法。 而老年代对象存活率比较高,就不适用。

标记整理算法

在标记整理的基础上多做了一步,清除之后将存活对象移动到内存空间一端而空余出另一端,解决内存空间不连续问题。 适用老年代的垃圾回收。

垃圾回收器选择

100M以下内存,或者单核CPU或者没有停顿时间要求,serial系列。 如果响应时间要求高,系统内存CPU满足要求,适用CMS,G1

并行和并发的概念

  1. 并行 parallel

    1) 同一时间做多件事情的能力 2) 多核CPU上不同的线程同时运行。

  2. 并发 concurrent

    1) 同一时间,应对 dealing with 一件事情的能力 2) 线程轮行适用CPU的做法。 3) 在同一个CPU上,多个线程抢占CPU资源,轮流执行,由于任务调度器将CPU时间切片分的很小 人类感觉是同时运行的。 即:微观串行,宏观并行。

垃圾收集器

  1. serial (串行垃圾回收器 )

1) 年轻代复制算法、老年代标记整理算法。 2) 单线程进行垃圾回收,垃圾回收过程中会STW.

  1. Parallel (并行垃圾回收器) - 1.8的默认垃圾收集器

1) 年轻代复制算法、老年代标记整理算法。 2) 多个线程并行进行垃圾回收,会STW,与serial相比,减少了STW的时间。

  1. CMS(并发垃圾收集器)

1) 用户线程和垃圾收集线程同时执行。(不一定是并行,有可能是并发) 2) 不需要停顿用户线程,没有STW

  1. G1垃圾回收器

1) 将内存分割成不同的区域 2) 并发进行垃圾回收

STW

进行垃圾回收时,需要停止工作线程。 在进行可达性分析的时候,如果不进行STW : 1) 新生成的对象,这些对象还没有指向引用,可能被误标记为垃圾。 2) 已经是垃圾的对象,有可能没有被标记。

CMS垃圾收集器

  1. 初始标记:只标记能直接关联到的对象,速度很快,需要STW.
  2. 并发标记: 同时执行GCROOT Tracing,耗时最长的过程。
  3. 重新标记: 重新标记因为并发标记是,用户程序运行而导致的标记发生移动的部分对象。一般比初始标记时间长点,但是比并发标记时间短。 需要STW.
  4. 清除垃圾对象

优点: 并发标记,停顿低。 缺点: 虽然不会停顿用户线程,但是会占用一部分线程数和CPU资源,占用资源就意味着会增加用户代码执行时间。 使用的是标记清除算法,容易产生空间碎片,等空间碎片过多,会给内存分配大对象产生影响。不得不提前进行一次FullGc。

三色标记法(CMS垃圾收集器并发标记时的算法)

  1. 基于GcRoots,一开始所有对象都是白色的,白色的意思: 没有被GC扫描,扫描结束后可以被回收。
  2. 将GCRoots直接关联的对象设置为灰色。灰色的意思: 被GC扫描过过,但是至少有一个引用没有被扫描。
  3. 遍历灰色对象引用,灰色对象本身变为黑色,引用变为灰色。黑色的意思: 对象本身和他的直接引用都被扫描过,不能被回收。
  4. 重复上一步,直到没有灰色对象。
  5. 结束,白色为垃圾对象。

JVM参数

  • -XX:printGC 打印GC基本信息
  • -XX:+PrintGCDetails 打印GC详细信息
  • -XX:PrintGCTimeStamps 打印GC发生时间戳
  • -X:loggc:log/gc.log 输出gc信息的log文件位置
  • -XX:+PrintHeapAtGC 每次GC后,都打印堆信息
  • -XX:+TraceClassLoading 监控类的加载
  • -xmx -xms 最大,最小堆内存设置
  • -xmn 设置新生代内存大小
  • -XX:NewRatio 新生代老年代比例
  • -XX:SurvivorRatio 设置survivor 和 eden的比例
  • -XX:+HeapDumpOnOutMemeoryError 当发生OOM异常时,导出堆信息到指定文件。
  • -XX:+HeapDumpPath 堆信息文件的路径
  • -Xss:栈内存大小设置

获取对象在内存中大小分布情况

  • openjdk 开源layout 可以打印出当前对象的内存信息

          <!--jol-->
          <dependency>
              <groupId>org.openjdk.jol</groupId>
              <artifactId>jol-core</artifactId>
              <version>0.9</version>
          </dependency>
    

    Object obj = new Object()占多少内存空间?

    @Test
      public void testLayoutObj(){
          Hello obj = new Hello();
          System.out.println(ClassLayout.parseInstance(obj).toPrintable());
          // 从返回结果看,一个对象包含了对象头信息和对象指向的类信息,即指针
          // 对象头宝包含了 4字节的年代信息,4字节的 hashcode信息,4字节的锁信息。
          // 类指针为4字节。  所以如果一个对象没有成员变量,jvm也没有开启压缩,他的大小为16字节
    //        com.bczhaoa.mybatisplustest.OtherTest.TestJvm$Hello object internals:
    //        OFFSET  SIZE                                            TYPE DESCRIPTION                               VALUE
    //        0     4                                                 (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
    //        4     4                                                 (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    //        8     4                                                 (object header)                           90 b5 13 1e (10010000 10110101 00010011 00011110) (504608144)
    //        12     4   com.bczhaoa.mybatisplustest.OtherTest.TestJvm Hello.this$0                              (object)
    //        Instance size: 16 bytes
    //        Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
      }
    

    如果Object有一个int 一个 String 类型的属性呢。

    class Hello{
          int a;
          String b="testsssss";
      }
    
      @Test
      public void testLayoutObj(){
          Hello obj = new Hello();
          System.out.println(ClassLayout.parseInstance(obj).toPrintable());
          // 从返回结果看,一个对象包含了对象头信息和对象指向的类信息,即指针
          // 对象头宝包含了 4字节的年代信息,4字节的 hashcode信息,4字节的锁信息。
          // 类指针为4字节。  所以如果一个对象没有成员变量,jvm也没有开启压缩,他的大小为24字节 
          // 4个字节锁+4个年代信息+4个hash信息 + // 4个int信息+4个String信息+4个类指针 刚好24. 
          // 是8的整数倍,如果在来个boolean属性, 
          // 25不是8的整数本 会补到32
    //        com.bczhaoa.mybatisplustest.OtherTest.TestJvm$Hello object internals:
    //        OFFSET  SIZE                                            TYPE DESCRIPTION                               VALUE
    //        0     4                                                 (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
    //        4     4                                                 (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    //        8     4                                                 (object header)                           90 b5 13 1e (10010000 10110101 00010011 00011110) (504608144)
    //        12     4                                             int Hello.a                                   0
    //        16     4                                java.lang.String Hello.b                                   (object)
    //        20     4   com.bczhaoa.mybatisplustest.OtherTest.TestJvm Hello.this$0                              (object)
    //        Instance size: 24 bytes
    //        Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
      }