1、GC是什么?为什么要有GC?
GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。
2、垃圾回收的优点和原理。并考虑2种回收机制。
Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有"作用域"的概念,只有对象的引用才有"作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。
垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清出和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
回收机制有分代复制、标记清除、标记压缩。
3、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
4、java中会存在内存泄漏吗,请简单描述。
理论上Java因为有垃圾回收机制不会存在内存泄露问题,然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。
例如Hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。
下面例子中的代码也会导致内存泄露
import java.util.Arrays; import java.util.EmptyStackException; public class MyStack<T> { private T[] elements; private int size = 0; private static final int INIT_CAPACITY = 16; public MyStack() { elements = (T[]) new Object[INIT_CAPACITY]; } public void push(T elem) { ensureCapacity(); elements[size++] = elem; } public T pop() { if(size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity() { if(elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } }
上面的代码实现了一个栈结构,似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的pop方法却存在内存泄露的问题,当我们用pop方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用
在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。
Vector v = new Vector(10); for (int i = 1; i < 100; i++) { Object o = new Object(); v.add(o); o = null; }
5、能不能自己写个类,也叫java.lang.String?
可以,但在应用的时候,需要用自己的类加载器去加载,否则,系统的类加载器永远只是去加载jre.jar包中的那个java.lang.String。由于在tomcat的web应用程序中,都是由webapp自己的类加载器先自己加载WEB-INF/classess目录中的类,然后才委托上级的类加载器加载,如果我们在tomcat的web应用程序中写一个java.lang.String,这时候Servlet程序加载的就是我们自己写的java.lang.String,但是这么干就会出很多潜在的问题,原来所有用了java.lang.String类的都将出现问题。
package java.lang; public class Test { public static void main(String[] args) { System.out.println("哈哈哈"); } } //报告的错误如下:java.lang.NoSuchMethodError:main Exception inthread "main" //这是因为加载了jre自带的java.lang.String,而该类中没有main方法。
6、强引用、软引用、弱引用、虚引用
强引用StrongReference
最常见的引用类型,jvm即使是oom也不会回收拥有强引用的对象,是最难被GC回收的,宁可虚拟机抛出异常中断程序,也不回收强引用指向的实例对象。
Object object = new Object();
上面的对象就是拥有强引用。想要gc回收这个对象,就需要显式的将object = null ,那么对象就不存在引用关系
ArrayList<Object> arrayList = new ArrayList<>(); for (int i = 0; i < 9999999; i++) { arrayList.add(new BufferedImage(999, 999, BufferedImage.TYPE_INT_RGB)); }
运行上面的代码,发现报错oom
软引用SoftReference
在jvm内存不够的时候就会回收拥有软引用的对象,在jvm内存充足的时候不会回收
ArrayList<SoftReference<Object>> arrayList = new ArrayList<>(); for (int i = 0; i < 9999999; i++) { arrayList.add(new SoftReference<>( new BufferedImage(999, 999, BufferedImage.TYPE_INT_RGB))); }
跟强引用不同的是,每当jvm内存不够的时候,就会回收软引用对象new SoftReference<>(new BufferedImage(999, 999, BufferedImage.TYPE_INT_RGB)),所以并没有抛出oom。Android很多图片框架使用软引用来缓存bitmap,避免app的内存不足
弱引用WeakReference
跟软引用对象不一样的是,弱引用对象会在每一次的gc中被回收,不管jvm的内存怎么样,但是gc在jvm中的线程优先级是很低的,执行的次数比较少。
WeakReference<BufferedImage> reference = new WeakReference<BufferedImage>("hello"); System.gc(); if (reference.get() != null) { BufferedImage bufferedImage = reference.get(); System.out.println("no null"); }
大多数都不会打印,因为虽然调用gc()函数,但是只是建议jvm进行gc操作,但是大多数情况jvm不会接受这个建议
虚引用PhantomReference
该回收就回收,无所谓了我随便回收你,也叫幽灵引用,其实就是相当于没有指向任何实例对象
ReferenceQueue<String> queue = new ReferenceQueue<>(); PhantomReference<String> phReference = new PhantomReference<String>("hello", queue); System.out.println(phReference.get()); //null
7、heap和stack有什么区别。
java的内存分为两类,一类是栈内存,一类是堆内存。
栈内存是指程序进入一个方法时,会为这个方法单独分配一块私属存储空间,用于存储这个方法内部的局部变量,当这个方法结束时,分配给这个方法的栈会释放,这个栈中的变量也将随之释放。
堆是与栈作用不同的内存,一般用于存放不放在当前方法栈中的那些数据,例如使用new创建的对象都放在堆里,所以它不会随方法的结束而消失。方法中的局部变量使用final修饰后,放在堆中而不是栈中。
8、什么是类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
9、类的生命周期
加载:查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
连接:连接又包含三块内容:验证、准备、初始化。
验证,文件格式、元数据、字节码、符号引用验证;
准备,为类的静态变量分配内存,并将其初始化为默认值;
解析,把类中的符号引用转换为直接引用
初始化:为类的静态变量赋予正确的初始值
使用:new出对象程序中使用
卸载:执行垃圾回收
10、类加载器
启动类加载器:Bootstrap ClassLoader,负责加载存放在JDKjrelib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DKjrelibext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
11、类加载机制
全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载
缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
12、jvm内存结构
程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java虚拟机的多线程就是通过线程轮流切换并分配处理器时间来实现的,为了线程切换后能恢复到正确的位置,每条线程都需要一个独立的程序计数器互不影响,该区域为“线程私有”。
Java虚拟机栈:线程私有的与线程生命周期相同,用于存储局部变量表、操作栈、方法返回值。局部变量表放着基本数据类型,还有对象的引用。
本地方法栈:跟虚拟机栈很像,不过它是为虚拟机使用到的Native方法服务。
Java堆:所有线程共享的一块内存区域,对象实例几乎都在这分配内存。
方法区:各个线程共享的区域,储存虚拟机加载的类信息,常量,静态变量,编译后的代码。
运行时常量池:代表运行时每个class文件中的常量表。包括几种常量:编译时的数字常量、方法或者域的引用
13、对象分配规则
对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
14、GC算法
GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。适用于新生代
标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。适用于老年代4.分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
15、垃圾回收器
Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
16、什么是JVM
JVM是java虚拟机的缩写,是由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的字节码,就可在多种平台上不加修改的运行,这也是Java能够“一次编译,到处运行的”原因。
JVM生命周期介绍
-
启动。启动一个Java程序,一个JVM实例就产生。拥有public static void main(String[] args)函数的class可以作为JVM实例运行的起点。
-
运行。main()作为程序初始线程的起点,任何其他线程均可由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM使用,程序可以指定创建的线程为守护线程。
-
消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。
17、Java加载类的过程
-
装载(loading):负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名、ClassLoader完成类的加载。因此标识一个被加载了的类:类名 + 包名 + ClassLoader实例ID。
-
链接(linking):负责对二进制字节码的格式进行校验,完成校验后,JVM初始化类中的静态变量,并将其赋值为默认值。最后对类中的所有属性、方法进行验证,以确保要调用的属性、方法存在,以及具备访问权限(例如private、public等),否则会造成NoSuchMethodError、NoSuchFieldError等错误信息。
-
初始化(initializing):负责执行类中的静态初始化代码、构造器代码以及静态属性的初始化
18、JVM类加载顺序
-
Booststrap ClassLoader:JVM启动时初始化此ClassLoader,并由此完成$JAVA_HONE中jre/lib/rt.jar中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现
-
Extension ClassLoaderJVM用此classloader来加载扩展功能的一些jar包
-
System ClassLoaderJVM用此ClassLoader来加载启动参数中指定的ClassPath中的jar包以及目录,在Sun JDK中ClassLoader对应的类名为AppClassLoader
-
User-Defined ClassLoaderUser-Defined ClassLoader是Java开发人员继承ClassLoader抽象类实现的ClassLoader,基于自定义的ClassLoader可用于加载非ClassPath中的jar以及目录
19、java运行时数据区
-
PC寄存器:用于存储每个线程下一步将要执行的JVM指令,若该方法为native的,则PC寄存器中不存储任何信息。Java多线程情况下,每个线程都有一个自己的PC,以便完成不同线程上下文环境的切换。
-
JVM栈:JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放当前线程中局部基本类型的变量(Java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)非基本类型的对象在JVM栈上仅存放一个指向堆的地址。
-
堆(Heap):它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。堆在JVM启动的时候就被创建,堆中储存了各种对象,这些对象被垃圾回收器管理。JVM将Heap分为两块:新生代New Generation和旧生代Old Generation
-
方法区域(Method Area):方法区域存放所加载类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,同时方法区域也是全局共享的,在一定条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,就会抛出OutOfMemory的错误信息。
-
运行时常量池(Runtime Constant Pool):存放的为类中的固定常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
-
本地方法堆栈(Native Method Stacks):JVM采用本地方法堆来支持native方法的执行,此区域用于存储每个native方法调用的状态。
20、ClassLoader.loadClass()与Class.forName()的区别
Class.forName()会初始化静态代码块
ClassLoader.loadClass不会初始化静态代码块
例如,在JDBC编程中,常看到这样的用法,Class.forName("com.mysql.jdbc.Driver"),如果换成了 getClass().getClassLoader().loadClass("com.mysql.jdbc.Driver")就不行了
static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } }
Driver在static块中会注册自己到java.sql.DriverManager。而static块就是在Class的初始化中被执行。所以这个地方就只能用Class.forName(className)。