技术问答集锦(三)

1 Java基础

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

编译型语言:程序在执行以前须要一个专门的编译过程,把程序编译成为机器语言的文件,运行时不须要从新翻译,直接使用编译的结果就好了。所以效率比较高。好比 C 语言。java

解释型语言:程序不须要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次。所以效率比较低。好比Basic语言,专门有一个解释器可以直接执行Basic程序,每一个语句都是执行的时候才翻译。算法

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

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

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 核心库的类app

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

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) 增长操做系统内存

相关文章
相关标签/搜索