1.说一说jvm的主要组成部分
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。 类的加载器:将编译器生成的字节码文件加载到运行时数据方法区中,只要符合文件结构就加载,至于能否运行,它不负责,而是由执行引擎负责; 执行引擎:也叫解释器,负责执行命令,交由操作系统执行。 本地接口:融合其它语言为Java所用 运行时时数据区: 1.堆:Java对象的存储区域,用new字段分配的Java实例和数组,都被分配在堆上,Java7后运行时常量池从方法区上移到了堆上。 2.方法区:和堆一样都是所有线程共享的,主要存储的是类信息,静态变量,常量,即时编译器编译后的代码等数据。 3.虚拟机栈:虚拟机中执行每个方法的时候,都会创建一个栈帧用于存储局部变量,操作数栈,动态链接,方法出口等信息。 4,本地方法栈:与虚拟机栈类似,但是使用的是native方法 5.程序计数器:指示虚拟机下一条执行的字节码指令。 流程:首先编译器将代码编译成字节码文件,类加载器利用全类名将字节码文件加载带运行时数据区的方法区,字节码只是jvm的一套指令规范,操作系统不能识别,所以就需要执行引擎将字节码翻译成底层系统指令,交给cpu去执行。在这个过程中还需要调用其它语言的本地接口。
2.说一下堆栈的区别
堆:主要用于存储实例化的对象,数组,由jvm动态的分配内存一个jvm只有一个堆内存,线程是可以共享数据的,物理地址是不连续的,内存大小是运行时决定的 栈:主要用于存储局部变量和对象的引用,每个线程都会有一个独立的栈空间,线程之间不共享数据。物理地址是连续的,内存大小是编译时确定的
3.Java的内存泄露
内存泄漏指的是JVM中某些不再需要使用的对象,仍然存活于JVM中而不能及时释放而导致内存空间的浪费。 Java中,我们可能会遇到栈内存泄露和堆内存泄漏。 其中堆内存泄漏是由于创建后的对象一直存在于堆中,不再需要的对象其引用一直没有被移除。这些无用的对象会慢慢占用内存,最后导致内存溢出。 栈内存泄漏由于方法不断被调用,但是一直没有退出方法。这种情况可能发生在无限循环或递归掉用时,最终导致栈内存溢出。
4.内存泄漏的原因
Java中内存泄漏主要是因为不能正确释放不需要的资源,长生命周期对象持有短生命周期对象的引用。 静态字段 静态字段引起的内存泄漏比较常见,如果某个不需要的类中含有静态字段,那么就会造成内存泄漏。单例模式中如果持有其他的类引用就会造成内存泄漏,静态集合如HashMap,LinkedList等持有的一些对象没有及时释放等。 Thread Local threadlocal引用一个对象使用完成后并没有被及时remove掉,线程一直存活的情况下(使用线程池时)就会发生内存泄漏。 大多时候内存泄漏都是由于开发人员的代码错误导致的,要防止这种内存泄漏,就需要编写必要的代码来配合垃圾回收器释放资源。
5.实践中如何避免Java内存泄漏
使用最新稳定版本的Java 尽量减少使用静态变量,使用完之后及时赋值 null,移除引用 明确对象的有效作用域,尽量缩小对象的作用域。局部变量回收会很快。 减少长生命周期对象持有短生命周期的引用 各种连接应该及时关闭(数据库连接,网络,IO等) 使用内存泄漏检测工具如MAT,Visual VM,jprofile 等 避免在代码中使用System.gc() 避免使用内部类 内存泄漏很难定位并修复,但是我们可以遵循以下几个步骤去定位并修复:
6.定位并修复内存泄漏
确定是否存在内存泄漏,启用详细的GC跟踪。 使用一些第三方插件进行分析(jprofile Visual VM等) 检查调用堆栈是否有未释放的引用(分析GC状态) 找出对象没有被垃圾回收的原因 编写代码手动删除此类对象
7.GC如何判断一个对象是否为垃圾?
1.引用计数法 主要是查看该对象是否还有引用指向它,如果有则说明该对象不是垃圾,反之则为垃圾。 具体就是给一个对象上标一个数字用来记录有多少个引用指向了该对象,当这个数字记录为0时,那就表示这个对象已经没有引用指向它了,那么这个对象就变成了垃圾。存在问题:循环引用 2.根可达算法 基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
8.GC Roots如何选取
在Java语言中,可以作为GCRoots的对象包括下面几种: (1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。 (2). 方法区中的类静态属性引用的对象。 (3). 方法区中常量引用的对象。 (4). 本地方法栈中JNI(Native方法)引用的对象。
9.jvm有哪些垃圾回收算法
1.标记-清除算法 标记-清除算法对根集合进行扫描,对存活的对象进行标记。标记完成后,再对整个空间内未被标记的对象扫描,进行回收。 优点: 实现简单,不需要进行对象进行移动。 缺点: 标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。 2.复制算法 这种收集算法解决了标记清除算法存在的效率问题。它将内存区域划分成相同的两个内存块。每次仅使用一半的空间,
JVM
生成的新对象放在一半空间中。当一半空间用完时进行GC
,把可到达对象复制到另一半空间,然后把使用过的内存空间一次清理掉。 优点: 按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。 缺点: 可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。 3.标记-整理算法 标记-整理算法 采用和 标记-清除算法 一样的方式进行对象的标记,但后续不直接对可回收对象进行清理,而是将所有的存活对象往一端空闲空间移动,然后清理掉端边界以外的内存空间。 -优点: 解决了标记-清理算法存在的内存碎片问题。 缺点: 仍需要进行局部对象移动,一定程度上降低了效率。 3.分代收集算法 绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为 minor GC。 新生代 中存在一个Eden区和两个Survivor区。新对象会首先分配在Eden中(如果新对象过大,会直接分配在老年代中)。在GC中,Eden中的对象会被移动到Survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代。 可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=8 指定 老年代/新生代 为8/1. 老年代 占堆大小的 7/8 ,新生代 占堆大小的 1/8(默认即是 1/8)。 老年代(Old generation) 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代要少得多。对象从老年代中消失的过程,可以称之为major GC(或者full GC)。
10.jvm有哪些垃圾回收器
新生代收集器(全部的都是复制算法):Serial、ParNew、Parallel Scavenge 老年代收集器:CMS(标记-清理)、Serial Old(标记-整理)、Parallel Old(标记整理) 整堆收集器: G1(一个Region中是标记-清除算法,2个Region之间是复制算法) 同时,先解释几个名词: 1,并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态 2,并发(Concurrent):用户线程和垃圾收集线程同时执行 3,吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间)
11.说一说双亲委派机制
首先,我们需要知道的是,Java语言系统中支持以下4种类加载器: Bootstrap ClassLoader 启动类加载器 Extention ClassLoader 标准扩展类加载器 Application ClassLoader 应用类加载器 User ClassLoader 用户自定义类加载器 所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
12.为什么需要双亲委派?
首先,通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。 另外,通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。 那么,就可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改
13."父子加载器"之间的关系是继承吗?
双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的。
14.什么情况下父加载器会无法加载某一个类呢?
Java中提供的这四种类型的加载器,是有各自的职责的: Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%lib下的rt.jar、resources.jar、charsets.jar和class等。 Extention ClassLoader,主要负责加载目录%JRE_HOME%libext目录下的jar包和class文件。 Application ClassLoader ,主要负责加载当前应用的classpath下的所有类 User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件 那么也就是说,一个用户自定义的类,如com.li.ClassHollis 是无论如何也不会被Bootstrap和Extention加载器加载的。
15.为什么Tomcat要破坏双亲委派
我们知道,Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。 不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。 如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。 如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。 所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。 Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
16.说一说类的加载过程
类加载的过程主要分为三个部分:加载,链接,初始化 而链接又可以细分为三个小部分:验证,准备,解析 加载 简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。 这里有两个重点: 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。 验证 主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。 包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息? 对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载? 对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。 准备 主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。 特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。 比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456 解析 将常量池内的符号引用替换为直接引用的过程。 两个重点: 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量 举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。 在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。 初始化 这个阶段主要是对类变量初始化,是执行类构造器的过程。 换句话说,只对static修饰的变量或语句进行初始化。 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。