JVM知识点总结

  1. JVM:垃圾回收算法,垃圾回收器。
    垃圾回收算法:
    1. 标记-清除法:标记出没有用的对象,然后一个一个回收掉;
      缺点:标记和清除两个过程效率不高,产生内存碎片导致需要分配较大对象时无法找到足够的连续内存而需要触发一次GC操作。
    2. 复制算法: 按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉;
      缺点:将内存缩小为了原来的一半
    3. 标记-整理法:标记出没有用的对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内;
      优点:解决了标记-清除算法导致的内存碎片问题和在存活率较高时复制算法效率低的问题。
    4. 分代回收:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法;

垃圾回收器: 

1. Serial New 收集器是针对新生代的收集器,采用的是复制算法;
2. Parallel New(并行)收集器,新生代采用复制算法,老年代采用标记整理;
3. Parallel Scavenge(并行)收集器,针对新生代,采用复制收集算法;
4. Serial Old(串行)收集器,新生代采用复制,老年代采用标记清理;
5. Parallel Old(并行)收集器,针对老年代,标记整理;
6. CMS收集器,基于标记清理;
7. G1收集器(JDK):整体上是基于标记清理,局部采用复制;

综上:新生代基本采用复制算法,老年代采用标记整理算法。cms采用标记清理。

  1. Java内存模型?

    1. 程序计数器:当前线程所执行的字节码的行号指示器,用于记录下一条要运行的指令,线程私有
    2. Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有
    3. Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有
    4. Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享
    5. 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享;
  2. 介绍一下GC机制?
    Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控;
    Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理;
    可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用;

  3. GC中如何判断对象需要被回收?
    先判断对象的死活。主要有两种方法:

    1. 引用计数法:给对象添加一个引用计数器,没当被引用的时候,计数器的值就加一。引用失效的时候减一,当计数器的值为 0 的时候就表示改对象可以被 GC 回收了。缺点是:存在循环引用的情况。
    2. 可达性算法:通过一个叫 GC Roots 的对象作为起点,从这些结点开始向下搜索,搜索所走过的路径称为引用链,当一个对象没有与任何的引用链相连的时候则改对象就可以被。 GC 回收回收了Roots 包括:java 虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中常量引用的对象,方法区中静态属性引用的对象;
      在Java语言里,可作为GC Roots的对象包括以下几种:
      虚拟机栈(栈帧中的本地变量表)中的引用的对象
      方法区中的类静态属性引用的对象
      方法区中的常量引用的对象。
      本地方法栈中JNI(即一般说的Native方法)的引用的对象。
  4. 新生代、老年代、永久代的分配策略?

    1. 结构(堆大小 = 新生代 + 老年代 ):
      (1)新生代(1/3)(初始对象,生命周期短):Eden区、survivior0、survivior1(8:1:1);
      (2)老年代(2/3)(长时间存在的对象)
    2. 分配策略:
      (1)小对象优先分配在Eden区域;
      (2)如果Eden区域空间不够,那么尝试把活着的对象放到survivor0中去(Minor GC)
      如果survivor0可以放入,那么放入之后清除Eden区;
      如果survivor0不可以放入,那么尝试把Eden和survivor0的存活对象放到survivor1中;
      如果survivor1可以放入,那么放入survivor1之后清除Eden和survivor0,之后再把 survivor1中的对象复制到survivor0中,保持survivor1一直为空;
      如果survivor1不可以放入,那么直接把它们放入到老年代中,并清除Eden和survivor0,这个过程也称为分配担保(Full GC);
      (3)大对象直接进入老年代:因为他们需要大量连续空间;
      (4)长期存活的对象进入老年代(年龄计数器,每次在suvivor区域熬过一次minor GC增加1,默认最多15);
      (5)动态年龄判断,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代;
  5. 新生代到老年代的转换有哪些情况(条件)?
    有四种情况:
    (1)大对象直接进入老年代;
    (2)长期存活的对象;
    (3)Minor GC后,suvivor区依然无法存放的对象,进入老年代;
    (4)动态年龄判断,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代;

  6. 类加载机制?

  7. MinorGC和FullGC?
    Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快,一般采用复制-回收算法;
    Full GC/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,所采用的是标记-清除算法;

  8. Java内存堆和栈?

    1. 栈内存用来存储基本类型的变量和对象的引用变量,堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
    2. 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问;
    3. 如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.StackOverFlowError,如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError;
    4. 栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满,-Xss选项设置栈内存的大小。-Xms选项可以设置堆的开始时的大小;
  9. 静态变量存在哪?
    存放在方法区。

  10. Java的四种引用?

    1. 强引用(StrongReference)强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题
    2. 软引用(SoftReference)如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中
    3. 弱引用(WeakReference)弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
      弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中;
    4. 虚引用(PhantomReference)虚引用在任何时候都可能被垃圾回收器回收,主要用来跟踪对象被垃圾回收器回收的活动,被回收时会收到一个系统通知。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
  11. JVM三大性能调优参数-Xms -Xmx -Xss? 为什么JVM调优经常会将-Xms和-Xmx参数设置成一样?
    -Xss规定了每个线程堆栈的大小。一般情况下256K是足够了。影响了此进程中并发线程数大小。
    -Xms初始的Heap的大小。
    在很多情况下,-Xms和-Xmx设置成一样的。这么设置,是因为当Heap不够用时,JVM会反复重新申请内存,导致性能大起大落,影响程序运行稳定性。

  1. JAVA虚拟机的作用?
    将Java字节码转化为机器指令。实现了平台无关性。

  2. 在java 7 和 java 8中GC的区别。
    持久代被替换成了元空间,更容易进行参数调优。

  3. 如果经常出现full GC怎么定位代码哪里出了问题?

    1. System.gc()方法的调用。此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存。
    2. 老年代空间不足。不要创建过大的对象及数组。
    3. 堆中分配很大的对象。所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
  4. Java什么时候会发生内存泄漏?

    1. 长生命周期的对象持有短生命周期对象的引用:例如:在全局静态map中缓存局部变量,且没有清空操作,随着时间的推移,这个map会越来越大,造成内存泄露。
    2. 连接资源不释放,例如:Java 数据库连接一般用DataSource.getConnection()来创建,当不再使用时必须用Close()方法来释放;
  5. 为了避免内存泄露,在编写代码的过程中可以参考下面的建议:

    1. 尽早释放无用对象的引用
    2. 使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
    3. 尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
    4. 避免在循环中创建对象
    5. 开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
  1. 双亲委托类加载机制(Bootstrap,双亲委派,运行时常量池)?

    1. 概念:虚拟机把描述类字节码文件加载到内存,并对数据进行验证、准备、解析以及类初始化,最终形成可以被虚拟机直接使用的java类型(java.lang.Class对象);
    2. 类的声明周期:
      (1)加载过程:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中(方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
      (2)验证过程:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,文件格式验证、元数据验证、字节码验证、符号引用验证
      (3)准备过程:正式为类属性分配内存并设置类属性初始值的阶段,这些内存都将在方法区中进行分配
      (4)解析阶段:虚拟机将常量池内的符号引用替换为直接引用的过程
      (5)初始化阶段:类初始化阶段是类加载过程的最后一步。初始化阶段就是执行类构造器()方法的过程
      (6)使用阶段
      (7)卸载阶段
    3. Java类加载器:
      类加载器负责加载所有的类,同一个类(一个类用其全限定类名(包名加类名)标志)只会被加载一次;
      Bootstrap ClassLoader:根类加载器,负责加载java的核心类,它不是java.lang.ClassLoader的子类,而是由JVM自身实现;
      Extension ClassLoader:扩展类加载器,扩展类加载器的加载路径是JDK目录下jre/lib/ext,扩展类的getParent()方法返回null,实际上扩展类加载器的父类加载器是根加载器,只是根加载器并不是Java实现的;
      System ClassLoader:系统(应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径。程序可以通过getSystemClassLoader()来获取系统类加载器。系统加载器的加载路径是程序运行的当前路径;
    4. 双亲委派机制的作用:(1)共享功能:可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加载,一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。 (2)隔离功能:因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的类装载器,保证java/Android核心类库的纯净和安全,防止恶意加载。
    5. 双亲委派模型的工作过程:
      (1)首先会先查找当前ClassLoader是否加载过此类,有就返回;
      (2)如果没有,查询父ClassLoader是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
      (3)如果整个类加载器体系上的ClassLoader都没有加载过,才由当前ClassLoader加载(调用findClass),整个过程类似循环链表一样;
    6. 定义:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委托给自己的父加载器,每一层的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的Bootstrap ClassLoader中,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己加载。
  2. 内存泄漏了解多少,想办法写程序造成堆内存溢出,栈内存溢出,方法区溢出?
    堆内存:写一个类,然后在ArrayList中无限循环add这个类的对象;
    栈内存:无限递归调用;
    方法区:用一个装String的ArrayList,向里面add String.valueOf(i++).intern();
    String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

  3. new()创建过程都实现了什么?
    ①类加载检查: 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
    ②分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
    ③初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
    ④设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
    ⑤执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

  4. String对象的两种创建方式?
    String str1 ="abcd";String str2 = new String("abcd");。这两种不同的创建方法是有差别的,第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间创建一个新的对象。记住:只要使用new方法,便需要创建新的对象。

  5. 常量池?
    Java基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。

  6. Java对象创建方式有哪些?

    1. 使用new关键字创建对象;
    2. 使用Class类的newInstance方法(反射机制);
    3. 使用Constructor类的newInstance方法(反射机制);
    4. 使用Clone方法创建对象;
    5. 使用(反)序列化机制创建对象;
  7. Java常量池。
    Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
    1)所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
    2)而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
    常量池的好处:常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
    (1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
    (2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等;
    如果你用了String s1 = new String("abc"); 那么,会有两个String被创建,一个是你的Class被CLassLoader加载时,你的”abc”被作为常量读入,在constant pool里创建了一个共享的”abc” 。然后,当调用到new String("abc")的时候,会在heap里创建这个new String("abc");

  1. new开辟空间多大事怎么决定的?
    内存对齐。与操作系统有关。