技术问答集锦(三)

354 阅读22分钟

1 Java基础

1.1 编译型语言VS解释型语言

编译型语言:程序在执行之前需要一个专门的编译过程,把程序编译成为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。因此效率比较高。比如 C 语言。

解释型语言:程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次。因此效率比较低。比如Basic语言,专门有一个解释器能够直接执行Basic程序,每个语句都是执行的时候才翻译。

C语言是编译型的。C程序——>机器语言(编译)

Java比较特殊,Java程序也需要编译,但是没有直接编译成机器语言,而是编译成字节码,然后用解释方式执行字节码。 Java程序—— >字节码(编译)—— >机器语言(解释)

1.2 JVM工作原理

JVM 主要由 ClassLoader执行引擎 两子系统组成,运行数据区分为五个部分: 方法区、堆、栈、程序计数器、本地方法栈。其中的方法区和堆是所有线程共享的,JVM将临时变量放在栈中,每个线程都有自己独立的栈空间和程序计数器。

任何一个Java类的main函数运行都会创建一个JVM实例,JVM实例启动时默认启动几个守护线程,比如:垃圾回收的线程,而 main 方法的执行是在一个单独的非守护线程中执行的。只要非守护线程结束JVM实例就销毁了。

那么在Java类main函数运行过程中,JVM的工作原理如下:

  1. 根据系统环境变量,创建装载JVM的环境与配置;
  2. 寻找JRE目录,寻找jvm.dll,并装载jvm.dll;
  3. 根据JVM的参数配置,如:内存参数,初始化jvm实例;
  4. JVM实例产生一个 引导类加载器实例(Bootstrap Loader),加载Java核心库,然后引导类加载器自动加载 扩展类加载器(Extended Loader),加载Java扩展库,最后扩展类加载器自动加载 系统类加载器(AppClass Loader),加载当前的Java类;
  5. 当前Java类加载至内存后,会经过 验证、准备、解析 三步,将Java类中的 类型信息、属性信息、常量池 存放在方法区内存中,方法指令直接保存到栈内存中,如:main函数;
  6. 执行引擎开始执行栈内存中指令,由于main函数是静态方法,所以不需要传入实例,在类加载完毕之后,直接执行main方法指令;
  7. main函数执行主线程结束,随之守护线程销毁,最后JVM实例被销毁;

1.3 JVM类加载机制

类加载是Java程序运行的第一步,在java.lang包里有个ClassLoader类,ClassLoader 的基本目标是 对类的请求提供服务,按需动态装载类和资源 ,只有当一个类要使用 (1. Class.forName();2. 调用类的静态方法;3. 使用 new 关键字来实例化一个类) 的时候,类加载器才会加载这个类并初始化。当我们自定义类加载器加载类文件时(继承自ClassLoader类,只需覆盖 findClass方法,即可),其类加载机制如下:

当自定义类加载器加载类时,会调用loadClass方法加载类,而由于类加载的双亲委托模式,会将类的加载代理给父类加载器:系统类加载器来完成,依次类推至最顶层引导类加载器加载,如果父类加载器没有加载到类,则最终返回由自定义类加载器加载类,通过双亲委托模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类

需要说明一下Java虚拟机是如何判定两个Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。然而,类加载器又分初始类加载器定义类加载器,由于类加载的代理模式,初始类加载器并不一定是定义类加载器,所以确切的说,判定两个 Java 类是否相同的, 哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器

1.4 JVM内存分配策略

JVM内存主要是指运行数据区,粗略的分为:堆、栈,细致区分五部分:方法区、堆、栈、程序计数器、本地方法栈

  1. 程序计数器: 线程私有,记录线程所执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数值则为空。唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

  2. Java虚拟机栈: 线程私有,描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表,操作数栈,动态链接,方法出口 等信息。每一个方法从调用直至执行完成的过程,就对应这一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧是方法运行时的基础数据结构。如果线程请求栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存,抛出 OutOfMemoryError异常。

  3. 本地方法栈: 线程私有,描述native方法执行的内存模型。有的虚拟机(如Sun HotSpot虚拟机)直接就把 本地方法栈和虚拟机栈 合二为一。

  4. Java堆: 线程共享,存放对象实例及数组,是垃圾收集器管理的主要区域,采用分代收集策略,所以Java堆会细分为新生代和老年代。根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续的即可。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出OutOfMemoryError异常。

  5. 方法区线程共享,存储已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆),目的应该是与Java堆区分开来。如果方法区无法满足内存分配需求,抛出OutOfMemoryError异常。对于HotSpot虚拟机,方法区被称为永久代,本质上两者不等价,仅仅是因为HotSpot虚拟机设计团队选择把GC分代收集扩至方法区,或者说使用永久代来实现方法区而已

    JVM堆一般又可以分为以下三部分:Young 新生代、Tenured 老年代、Perm 永久代;

    Perm永久代主要保存class,method,filed对象,这部分的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误。

    Tenured老年代主要保存生命周期长的对象,多次未被GC掉的对象

    Young新生代主要保存新生成对象,根据JVM的策略,在经过几次垃圾收集后,而没有被垃圾回收的对象将被移动到Tenured区间。有时候该区经常会遇到java.lang.OutOfMemoryError :Java heap space的错误。

    -Xms:指定了JVM初始启动以后 初始化内存

    -Xmx:指定JVM堆得 最大内存,在JVM启动以后,会分配-Xmx参数指定大小的内存给JVM,但是不一定全部使用,JVM会根据-Xms参数来调节真正用于JVM的内存;

    -Xmn:参数设置了新生代的大小;老年代等于-Xmx减去-Xmn;

    -XX:Xss:参数设置了永久代的大小;

  6. 直接内存: 不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现。例如:NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了Java堆和Native堆中来回复制数据。直接内存虽然不会受到Java堆大小的限制,但是,既然是内存,仍会受到本机总内存大小及处理器寻址空间的限制。

  7. JVM参数:

    -XX:+HeapDumpOnOutOfMemoryError:可让虚拟机在内存溢出异常时Dump出当前的内存堆转储快照以便事后分析;

    -Xss:设置线程栈大小;

    -XX:PermSize:设置永久代初始大小;

    -XX:MaxPermSize:设置永久代最大大小;

    -XX:MaxDirectMemorySize:设置直接内存大小,默认与Java堆最大值一样

1.5 对象是否可回收

  1. 引用计数算法 存储对特定对象的所有引用数,也就是说,当应用程序创建引用以及引用超出范围时,JVM必须适当增减引用数。当某对象的引用数为0时,便可以进行垃圾收集。

    优点:实现简单、效率高;

    缺点:很难解决对象之间相互引用问题;

  2. 可达性分析算法 通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

    可作为GC Roots的对象包括:

    1. 虚拟机栈(栈帧的本地变量表)中引用的对象;
    2. 方法区中类静态属性引用的对象;
    3. 方法区中常量引用的对象;
    4. 本地方法栈中JNI(即一般说的是native方法)引用的对象;

1.6 四种引用

  1. 强引用:只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
  2. 软引用:对软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
  3. 弱引用:对弱引用关联着的对象,只能生存到下一次垃圾收集发生之前。
  4. 虚引用:对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得对象实例。关联虚引用唯一目的就是能在对象被收集器回收时收到系统通知。

1.7 finalize()方法

任何一个对象的finalize()方法都仅会被系统自动调用一次。如果对象面临下一次回收,它的finalize()方法不会被再次执行。建议避免使用该方法。

1.8 JVM垃圾回收策略

GC即垃圾收集机制是指JVM用于释放那些不再使用的对象所占用的内存。常用机制:

  1. 标记-清除算法【适用于老年代】 首先根据可达性分析算法,标记出所有需要回收的对象,在标记完成后统一回收所有标记的对象。

    缺点:效率问题:标记、清除两个过程效率都低;空间问题:标记清除之后产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中,需要分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

  2. 复制算法【空间换时间,适用于对象存活率低的新生代】 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

    优点:实现简单、效率高、无内存碎片;

    缺点:内存使用率低,太浪费。

    由于新生代中的对象98%是朝生夕死的,所以 将内存分为一块较大的内存、两块较小的内存。每次使用一块大内存和一块小内存,当垃圾回收时,会将该大内存和小内存中存活的对象一次性的复制到另外一块小内存中,最后清理掉该大内存、小内存。但是如果对象的存活率较高,那么当复制对象至另一块小内存时,该小内存空间会不够用,则需要依赖其他内存(老年代)进行分配担保

    当对象的存活率较高时,复制算法要进行较多的复制操作,效率会变低。

  3. 标记-整理算法【适用于老年代】

    标记过程与“标记-清除”算法一样,唯一区别是在后续步骤不是直接对内存进行清除,而是先让所有活着的对象都向一端移动,然后直接清除掉端边界以外的内存

  4. 分代收集策略 一般是把Java堆分成 新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

    新生代中对象存活率低,采用复制算法,有老年代对它进行分配担保

    老年代中对象存活率高,无额外空间对它进行分配担保,必须采用 “标记-清除”或“标记-整理” 算法。

  5. 回收方法区(永久代) 很多人认为方法区(永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,况且在方法区中进行垃圾收集性价比很低;

    永久代的垃圾收集主要回收两方面: 废弃常量和无用的类

    废弃常量:判断常量池的对象是否还存在任何引用;

    无用的类:(1)该类的所有实例都被回收;(2)该类的ClassLoader已被回收;(3)该类的Class对象没有任何引用;

1.9 垃圾收集器

JDK1.7 Update14之后的HotSpot虚拟机正式提供了G1收集器;

  1. Serial收集器: 单线程、Stop The Wold、Client模式默认新生代收集器复制算法

  2. ParNew收集器: Serial多线程版、并行、Stop The Wold、Server模式默认新生代收集器、只有该收集器能与CMS收集器配合、 复制算法

    ParNew收集器也是使用:

    -XX:+UseConcMarkSweepGC 选项后的默认新生代收集器;

    -XX:+UserParNewGC 选项来强制指定它;

    -XX:ParallelGCThreads 参数来限制垃圾收集的线程数,默认开启的线程数与CPU的数量相等;

  3. Parallel Scavenge收集器: 多线程、并行、Stop The Wold、 新生代收集器、Server模式复制算法; CMS等收集器的关注点是 尽可能地缩短垃圾收集时用户线程的停顿时间,而 该收集器关注的是达到一个可控制的吞吐量

    所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),吞吐量与垃圾收集时间成反比;

    停顿时间越短就越适合需要与用户交互的程序,而高吞吐量则可以高效率的利用CPU,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务;

    -XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间;

    -XX:GCTimeRatio:设置吞吐量大小,大于0且小于100的整数,默认为99;

    -XX:UseAdaptiveSizePolicy:设置打开GC自适应的调节策略,以达到最大的吞吐量;

  4. Serial Old收集器: 单线程、Stop The Wold、 老年代标记-整理算法、Client模式;作为CMS收集器的备选方案,在并发收集发生Concurrent Mode Failure时使用;

  5. Parallel Old收集器: 多线程、Stop The Wold、 老年代标记-整理算法、Server模式、并行;适合与Parallel Scavenge配合使用

  6. CMS收集器: 多线程、Stop The Wold、 老年代标记-清除算法、Server模式、并发; 缺点:内存碎片

    -XX:+UseCMSCompactAtFullCollection:默认开启,用于在CMS收集器顶不住进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发,会导致停顿时间变长;

    -XX:CMSFullGCsBeforeCompaction:用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的GC,默认为0,表示每次进行Full GC时都进行碎片整理;

  7. G1收集器: 多线程、新老年代复制+标记-整理算法、并发、Server模式、GC整个堆; 特点:并行与并发、分代收集、空间整理、可预测的停顿

    G1虽然保存了新老年代的概念,但已经不是物理分割了,他们都是一部分Region(不需要连续)的集合

    args="-Dfile.encoding=UTF-8 
    -J-server 
    -J-Xss128k
    -J-XX:ThreadStackSize=128
    -J-XX:PermSize=64m -J-XX:MaxPermSize=256m 
    -J-verbose:gc -J-XX:+PrintGCDetails -J-XX:+PrintGCTimeStamps 
    -Djava.library.path=${RESIN_HOME}/libexec:/opt/j2sdk/lib:/usr/lib64 
    -Djmagick.systemclassloader=false -DNO_TIMEOUT"
    
    args="$args -Xdebug - Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=9090"
    args="$args -Xmn5g -Xms10g -Xmx10g"
    args="$args -J-XX:+UseParNewGC -J-XX:+UseConcMarkSweepGC"
    

    Client模式:默认-XX:+UseSerialGC,Serial + Serial Old; Server模式:默认-XX:+UseParallelGC,Parallel Scavenge + Serial Old;

    -XX:+PrintGC             打印GC信息
    -XX:+PrintGCDetails   打印较为详细的GC信息
    -XX:+PrintGCTimeStamps  打印GC时间戳,相对于应用程序启动的时间
    

    Serial:串行收集器,当进行垃圾收集时,会暂停所有线程; Parallel:并行收集器,是串行收集器的多线程版本,多CPU下; ParallelOld:老年代的Parallel版本; ConcMarkSweep:简称CMS,是并发收集器,将部分操作与用户线程并发执行; CMSIncrementalMode:CMS收集器变种,属增量式垃圾收集器,在并发标记和并发清理时交替运行垃圾收集器和用户线程; G1:面向服务器端应用的垃圾收集器,计划未来替代CMS收集器;

GC收集器

1.10 JVM参数配置

  1. 跟 Java 堆大小相关的 JVM 内存参数

    下面三个 JVM 参数用来指定堆的初始大小和最大值以及堆栈大小:

    -Xms 设置 Java 堆的初始化大小

    -Xmx 设置最大的 Java 堆大小

    -Xss 设置Java线程栈大小

  2. 关于打印垃圾收集器详情的 JVM 参数

    -verbose:gc 记录 GC 运行以及运行时间,一般用来查看 GC 是否是应用的瓶颈

    -XX:+PrintGCDetails 记录 GC 运行时的详细数据信息,包括新生成对象的占用内存大小以及耗费时间等

    -XX:+PrintGCTimeStamps 打印垃圾收集的时间戳

  3. 设置 Java 垃圾收集器行为的 JVM 参数

    -XX:+UseParallelGC 使用并行垃圾收集

    -XX:+UseConcMarkSweepGC 使用并发标志扫描收集 (Introduced in 1.4.1)

    -XX:+UseSerialGC 使用串行垃圾收集 (Introduced in 5.0.)

    需要提醒的是,但你的应用是非常关键的、交易非常频繁应用时,应该谨慎使用 GC 参数,因为 GC 操作是耗时的,你需要在这之中找到平衡点。

  4. JVM调试参数,用于远程调试

    -Xdebug -Xnoagent 
    -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000
    
  5. 用于修改 Perm Gen 大小的 JVM 参数

    下面的这三个参数主要用来解决 JVM 错误:java.lang.OutOfMemoryError:Perm Gen Space

    -XX:PermSize and MaxPermSize
    -XX:NewRatio=2  Ratio of new/old generation sizes.
    -XX:MaxPermSize=64m     Size of the Permanent Generation.
    
  6. 用来跟踪类加载和卸载的信息

    -XX:+TraceClassLoading-XX:+TraceClassUnloading 用来打印类被加载和卸载的过程信息,这个用来诊断应用的内存泄漏问题非常有用。

  7. 用于调试目的的 JVM 开关参数

    -XX:HeapDumpPath=./java_pid.hprof  Path to directory or file name for heap dump.
    -XX:+PrintConcurrentLocks       Print java.util.concurrent locks in Ctrl-Break thread dump.
    -XX:+PrintCommandLineFlags   Print flags that appeared on the command line.
    

    如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择;如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处;

1.11 Stop The Wold

可达性分析必须在一个能确保一致性的快照中进行,一致性是指在整个分析过程中整个执行系统必须冻结,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话可达性分析结果不准确。所以这点是导致GC进行时必须停顿所有线程的原因。

1.12 新生代GC、老年代GC

新生代GC:Minor GC,指发生在新生代的垃圾收集动作;

老年代GC:Major GC/Full GC,指发生在老年代垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC。会发生Stop The Wold。

1.13 对象进入老年代

  1. 大对象:通过参数 -XX:PretenureSizeThreshold=3145287,令大于这个设置值的对象直接在老年代分配。以避免大对象在新生代分配,从而触发新生代GC。

  2. 多次GC仍存活的对象:当对象每”熬过“一次Minor GC,对象年龄就增加1岁,当对象年龄增加到一定程度 (默认15岁),就会晋升到老年代中。对象晋升老年代的年龄阀值,可以通过参数 -XX:MaxTenurigThreshold 设置。

  3. 动态对象年龄判定:虚拟机并不是永远地要求对象年龄必须达到MaxTenurigThreshold阀值才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenurigThreshold阀值

  4. 新生代—分配担保:由于新生代采用复制收集算法,当新生代Minor GC 时,如果存活对象的大小大于Survivor,依据分配担保策略会将Survivor无法容纳的对象直接存入老年代。如果老年代仍不能够存放剩余的对象,则会发生Major GC/Full GC,就会Stop The Wold。

1.14 JVM常量池机制

常量池其实就是方法区一个内存空间,虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和。以字符串为例,在Java源代码中的每一个字面值字符串,都会在编译成class文件阶段,形成标志号为 8(CONSTANT_String_info)的常量表 。当JVM加载 class文件的时候,会为对应的常量池建立一个内存数据结构,并存放在方法区中。 如下代码:

public class Test{   
    private String str="我们"。
}

将Test编译之后形成class文件,那么在class文件中"我们"会以一种 CONSTANT_UTF8_info 表的形式存在,字节序列如下:1 0 6 230 136 145 228 187 172 1表示常量表的类型,0 6表示有6个字节的长度。后面6个字节是UTF-8编码的“我们”。当JVM运行的时候会将这些常量池的信息加载进方法区。也就是说在运行过程中内存存储的"我们"是UTF-8编码的。

1.15 为什么使用JVM

Java语言的一个非常重要的特点就是 平台无关性。而使用JVM是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言使用JVM屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在JVM虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,把字节码解释成具体平台上的机器指令执行

1.16 java.lang.OutOfMemoryError: unable to create new native thread

这个异常问题本质原因是我们 创建了太多的线程,而能创建的线程数是有限制的,导致了异常的发生。能创建的线程数的具体计算公式如下:

(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads

MaxProcessMemory:指的是一个进程的最大内存

JVMMemory:JVM内存

ReservedOsMemory:保留的操作系统内存

ThreadStackSize:线程栈的大小

在java语言里, 当你创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统线程的内存用的不是JVMMemory,而是系统中剩下的内存(MaxProcessMemory - JVMMemory - ReservedOsMemory),可以下面方法解决问题:

(1) 通过设置 -Xmx512m 减少JVM Heap size;

(2) 通过设置 -Xss64k 减少线程占用的Stack size;

(3) 增加操作系统内存