简单描述一下JVM的内存模型

jvm内存主要分为五个部分:方法区,java堆,java栈,程序计数器,本地方法栈。

  1. 方法区(永久代,线程共享):存储被虚拟机加载的类信息,常量,静态常量,静态方法,运行时常量池等。
  2. java堆(线程共享):存放所有new出来的东西。 1.堆是java虚拟机所管理的内存区域中最大的一块,java堆是被所有线程共享的内存区域,在java虚拟机启动时创建,堆内存的唯一目的就是存放对象实例,几乎所有的对象实例都在堆内存分配空间。2.堆是GC管理的主要区域,从垃圾回收的角度看,由于现在的垃圾收集器都是采用的分代收集算法,因此java堆还可以初步细分为新生代和老年代。
  3. java栈(线程私有方法级):为虚拟机执使用到的方法服务。每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。局部变量表存放的是:编译期可知的基本数据类型、对象引用类型。
  4. 程序计数器(线程私有):保证线程切换后能恢复到原来的位置。在线程创建时创建,指向下一条指令的地址,执行本地方法时,其值为undefined。为了线程切换后能够恢复到正确的执行位置,每条线程都有一个独立的程序计数器,这块儿属于“线程私有”的内存。
  5. 本地方法栈(线程私有):为虚拟机执使用到的Native方法服务。本地方法栈则为虚拟机执使用到的Native方法服务,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError。

什么情况下会触发FullGC?

  1. System.gc()方法的调用。此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。
  2. 旧生代空间不足。旧生代空间只有在新生代对象转入及创建大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space 。
  3. Permanet Generation空间满了。Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
  5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

Java类加载器有几种,关系是怎么样的?

  1. 引导类加载器(启动类加载器) bootstrap class loader 由C++编写,无法通过程序得到。主要负责加载JAVA中的一些核心类库。它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中
  2. 扩展类加载器 extensions class loader 负责加载JAVA_HOME/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
  3. 系统类加载器 application class loader 它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
  4. 自定义类加载器 java.lang.classloder 通过继承java.lang.ClassLoader类的方式

    关系:

    启动类加载器,由C++实现,没有父类。
    拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
    系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
    自定义类加载器,父类加载器肯定为AppClassLoader。

双亲委派机制的加载流程是怎样的,有什么好处?

  1. 虚拟机类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
  2. 类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
  3. 类加载过程包括加载、验证、准备、解析和初始化五个阶段。
  4. 某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
  5. 使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅是类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。每个类加载器加载自己有可能出现多个不同的但是名字相同的类。

简单讲一下类加载过程

  1. 类加载过程包括加载、链接(验证、准备、解析)和初始化五个阶段。
  2. 加载:加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。按照类加载器:一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
  3. 验证:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。文件格式的验证、元数据的验证、字节码的验证、符号引用的验证等
  4. 准备: 准备阶段是为类的静态变量分配内存,并将其初始化为默认值。
  5. 解析:把常量池中的符号引用转换为直接引用。
  6. 初始化:JVM负责主要对类变量(类变量就是static修改的变量)进行初始化。 方式:1.声明静态类变量时指定初始值 2.使用静态代码块为类变量指定初始值

Java8为什么用Metaspace替换掉PermGen?Metaspace保存在哪里?

  1. 整个永久代有一个JVM本身设置的固定大小上限,无法进行调整,而原空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。Metaspace将根据运行时的应用程序需求动态地调整大小。
  2. 元空间并不在虚拟机中,而是使用本地内存。

编译期会对指令做那些优化(简单描述编译器的指令重排)

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  1. 编译器优化的重排序
    编译器在不改变单线程程序语义的前提下(代码中不包含synchronized关键字),可以重新安排语句的执行顺序。
  2. 指令级并行的重排序
    现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。
    由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

简单描述一下volatile可以解决什么问题?如何做到的

  1. 重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
  2. 强制主内存读写同步,保证共享变量对所有线程的可见性。1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;2.这个写会操作会导致其他线程中的缓存无效。

简单描述一下GC的分代回收

  1. Java的堆内存被分代回收,分代管理是为了方便垃圾回收。 1.大部分对象很快就不再使用;2.还有一部分不会立即无用,也不会持续很长时间。
  2. 虚拟机划分为年轻代、老年代、永久代。
  3. 年轻代主要存放新创建的对象,年轻代分为Eden区和了两个Survivor区。大部分对象在Eden区中生成。当Eden区满了,还存活的对象会在两个Survivor区中交替保存,达到一定次数会晋升到老年代。
  4. 老年代用来存放从年轻代晋升而来的,存活时间较长的对象。
  5. 永久代,主要保存类信息等内容,这里的永久代是指对象划分方式,不是专指 1.7 的 PermGen,或者 1.8 之后的 Metaspace。
  6. 根据年轻代与老年代的特点,JVM 提供了不同的垃圾回收算法。垃圾回收算法按类型可以分为引用计数法、复制法和标记清除法。
  7. JVM 中提供的年轻代回收算法 Serial、ParNew、Parallel Scavenge都是复制算法,而 CMS、G1、ZGC 都属于标记清除算法。

G1垃圾回收算法和CMS的区别有哪些?

CMS:以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现。 优点是并发收集,停顿小。

过程:

  1. 初始标记:独占CPU,仅标记GCroots能直接关联的对象
  2. 并发标记:可以和用户线程并行执行,标记所有可达对象
  3. 重新标记:独占CPU(STW),对并发标记阶段用户线程运行产生的垃圾对象进行标记修正
  4. 并发清理:可以和用户线程并行执行,清理垃圾

G1:是一款面向服务端应用的垃圾收集器 并行于并发 分代收集 可预测的停顿

过程:

  1. 初始标记(Initial Making)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

对象引用有哪几种方式,有什么特点?

  1. 强引用 代码中普遍存在的类似”Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  2. 软引用 描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java中的类SoftReference表示软引用。
  3. 弱引用 描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。
  4. 虚引用 这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用。

简单说一下Java对象的创建过程

  1. 类加载检查 检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查符号引用代表的类是否已被加载、解析、初始化。若没有则执行类加载过程。
  2. 分配内存 虚拟机为新生对象分配内存。分配方式有”指针碰撞”和”空闲列表”两种,选取何种由Java堆是否规整决定,Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。 内存分配并发问题两种方式保证线程安全: CAS+失败重试(CAS十的一种实现方式,虚拟机采用CAS配上失败重试的方式保证操作的原子性) TLAB
  3. 初始化零值 虚拟机将分配的内存初始化为零值(不包括对象头)确保对象实例字段可不赋初值直接使用。
  4. 设置对象头 对对象进行设置 如这个对象是那个的实例 对象的hash码等
  5. 执行init方法 按照程序员的意愿进行初始化

如何判断对象是否已经死亡

  1. 引用计数法 给对象加引用计数器,计数器为0的对象就是不可能再被使用的。
  2. 可达性分析算法 通过一系列的称为”GC Roots”的对象作为起点,从这些节点向下搜索,节点走过的路径称为引用链,当一个对象到GC Roots没有任何应用链相连,此对象不可用。

内存泄漏 内存溢出 解决或者避免的方法

内存泄露

是指程序在申请内存后,无法释放已申请的内存空间就造成了内存泄漏,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

内存溢出

指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出,简单来说就是自己所需要使用的空间比我们拥有的内存大内存不够使用所造成的内存溢出。

内存泄漏一般分为 常发性内存泄漏、偶发性内存泄漏、一次性内存泄漏、隐式内存泄漏。

解决方法

  1. 资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉
  2. 避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作。

内存溢出原因

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 集合类中有对对象的引用,使用完后未清空,产生了堆积,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小

内存溢出的解决方案

  1. 修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
  2. 检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
  3. 对代码进行走查和分析,找出可能发生内存溢出的位置。