什么是字节码?采用字节码的好处是什么?
java中的编译器和解释器:
java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器.这台虚拟的机器在任何平台上都提供给编译程序一个共同的接口.
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行.在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机.
每一种平台的解释器是不同的,但是实现的虚拟机是相同的.Java源程序经过编译器编译后完成字节码,字节码由虚拟机解实执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后再特定的机器上运行.这也就是解释了java的编译与解释并存的特点
Java源代码–>编译器–>jvm可执行的Java字节码(即虚拟指令)---->jvm—>jvm中的解释器------>机器可执行的二进制代码---->程序运行
采用字节码的好处
Java语言通过字节码的方法,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点,所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无序重新编译便可在多种不同的计算机上运行
java类加载器有哪些?
JDK自带有三个类加载器:bootstrapClassLoader,ExtClassLoader,AppClassLoader.
- BootStrapClassLoader时ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件.
它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path路径下的内容)。用于提供 JVM 自身需要的类 - ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类
- AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件(系统类加载器,线程上下文加载器)
- 继承ClassLoader实现自定义类加载器
双亲委派模型
图解
源码
protected Class<?> loadClass(String name, boolean resolve) //resolve:true 加载 Class 的同时进行解析操作
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) { //同步操作,保证只能加载一次
// 首先,在缓存中判断是否已经加载同名类
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 获取当前类加载类的父类加载器
if (parent != null) {
// 如果存在父类加载器,则调用父类加载器进行类加载
c = parent.loadClass(name, false);
} else { // parent 为 null :父类加载器是引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) { // 当前类的加载器的父类加载未加载其此类 or 当前类的加载器未加载此类
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {//是否进行解析操作
resolveClass(c);
}
return c;
}
}
好处
- 主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如String.
- 同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类
双亲委派模型的破坏
破坏双亲委派机制 1
重写loadClass就可以破坏双亲委派机制,推荐重写findClass
破坏双亲委派机制 2
线程上下文类加载器
破坏双亲委派机制3
模块热部署
GC如何判断对象可以被回收
- 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收
- 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链.当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象
引用计数法,可能会出现A 引用了B,B又引用了A,这时候就算它们都不再使用了,但因为相互引用,计数器=1,永远无法被回收
GC Roots的对象有
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中的JNI(即一般说的Native方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会,对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是由虚拟机自动简历的Finalizer队列中判断是否需要执行finalize()方法
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收.否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法,执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象复活
每个对象只能触发一次finalize()方法
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘
强引用,软引用,弱引用,虚引用
强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:
Object o = new Object(); // 强引用
软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
弱引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
WeakHashMap
WeakHashMap存储的entry,当key不再被使用(被GC回收)后,entry对象就会被自动删除
原理:在调用get,size等方法时,会触发expungeStaleEntries()方法,从ReferenceQueue中取出被回收的key,并删除
虚引用
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
强引用 > 软引用 > 弱引用 > 虚引用
intern()方法
intern() 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
请描述一下对象的创建过程
Object o = new Object()在内存中占了多少字节
面试官问我:Object o = new Object() 占用了多少个字节?
- 在开启指针压缩的情况下,markword占用8字节,classpoint占用4字节,Instance data无数据,总共是12字节,由于对象需要为8的整数倍,Padding会补充4个字节,总共占用16字节的存储空间。
- 在没有指针的情况下,markword占用8字节,classpoint占用8字节,Instance data无数据,总共是16字节。
标量替换:简单地说,就是用标量替换聚合量。这样做的好处是如果创建的对象并未用到其中的全部变量,则可以节省一定的内存。对于代码执行而言,无需去找对象的引用,也会更快一些
- 标量是指不可分割的量,如java中基本数据类型和reference类型,相对的一个数据可以继续分解,称为聚合量;
- 如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换;
- 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是在栈上创建若干个成员变量;
一个线程OOM后,其他线程还能运行吗?
可以的
当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行
说一下JVM中,哪些是共享区,哪些可以作为GC Root
- 堆区和方法区是所有线程共享的,栈,本地方法栈,程序计数器是每个线程独有的
- 什么是gc root,JVM在进行垃圾回收时,需要找到"垃圾"对象,也就是没有被引用的对象,但是直接找"垃圾"对象是比较耗时的,所以反过来,先找"非垃圾"对象,也就是正常对象,那么就需要从某些"根"开始去找,根据这些"根"的引用路径找到正常对象,而这些"根"有一个特征,就是它只会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量,方法区中的静态变量,本地方法栈中的变量,正在运行的线程等可以作为gc root
进程之间是如何进行通讯的
线程之间如何进行通讯
- 线程之间可以通过共享内存或基于网络进行通信
- 如果是通过共享内存来进行通信,就需要考虑并发问题,什么时候阻塞,什么时候唤醒
- wait(),notify()
- 通过网络就比较简单明了,通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式
JVM垃圾回收机制,GC发送在JVM哪部分,有几种GC,它们的算法是什么
GC发生在堆里
GC:分代收集算法
- 次数上频繁收集Young区 Minor GC
- 次数上较少收集Old区 Full GC
- 基本不动perm区
垃圾回收相关算法
标记阶段:引用计数算法(Reference Counting)
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销.存在循环应用的问题,所以jvm并没有采用这种算法
标记阶段:可达性分析算法
可达性分析算法:也可以称为 根搜索算法、追踪性垃圾收集
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)
- 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots可以是哪些?
老年代对象如果跨代引用新生代对象的话,会被作为GC Root。而哪段老年代对象可能引用了新生代对象,是通过CardTable来标记的,当Young GC时,除了扫描方法栈引用的对象并标记为活着外,还会通过扫描CardTable中标记为1的DirtyCard,将DirtyCard中所有老年代对象引用的新生代对象也标记为活着。
- 虚拟机栈中引用的对象
- 比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内JNI(通常说的本地方法)引用的对象方法区中类静态属性引用的对象
- 比如:Java类的引用类型静态变量
- 方法区中常量引用的对象
- 比如:字符串常量池(string Table)里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用。
- 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、outOfMemoryError),系统类加载器。
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
清除阶段:标记-清除算法
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是
- 标记一清除算法(Mark-Sweep)
- 复制算法(copying)
- 标记-压缩算法(Mark-Compact)
标记-清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。
执行过程
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
- 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
- 标记的是引用的对象,不是垃圾!!
- 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
什么是清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。
关于空闲列表是在为对象分配内存的时候 提过
- 如果内存规整
- 采用指针碰撞的方式进行内存分配
- 如果内存不规整
- 虚拟机需要维护一个列表
- 空闲列表分配
缺点
- 标记清除算法的效率不算高
- 在进行GC的时候,需要停止整个应用程序,用户体验较差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表
清除阶段:复制算法
背景
为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。
核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
把可达的对象,直接复制到另外一个区域中复制完成后,A区就没有用了,里面的对象可以直接清除掉,其实里面的新生代里面就用到了复制算法
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
注意
如果系统中的垃圾对象很少,复制算法就不会很理想,复制算法需要复制的存活对象数量通常并不会太大,或者说非常低才行(老年代大量的对象存活,那么复制的对象将会有很多,效率会很低)
在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
清除阶段:标记-整理算法
背景
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。
1970年前后,G.L.Steele、C.J.Chene和D.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。
执行过程
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标清和标整的区别
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标整的优缺点
优点
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。即:STW
小结
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
标记清除 | 标记整理 | 复制 | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
综合我们可以找到,没有最好的算法,只有最合适的算法
对象在内存中的内存布局
JVM参数类型
如何查看一个正在运行中的java程序,它的某个jvm参数是否开启?具体值是多少?
jps -l
jinfo -flag 具体参数 端口号
jinfo -flags 端口号
标配参数
-version
-help
java -showversion
X参数(了解)
-Xint 解释执行
-Xcomp 第一次使用就编译成本地代码
-Xmixed 混合模式
XX参数(重点)
- Booleanl类型
公式:-XX:+或者-某个属性值(+表示开启,-表示关闭)
-XX:+PrintGCDetails
-XX:+UseParallelGC - KV设置类型
-XX:属性key=属性值value
-XX:MetaspaceSize=128m
-XX:MaxTenuringThreshold=15
-Xms 等价于-XX:InitialHeapSize
-Xmx 等价于 -XX:MaxHeapSize
盘点家底
java -XX:+PrintFlagsInitial
-XX:+PrintFlagsFinal
-XX:PrintCommandLineFlags
你平时用过的JVM常用基本配置参数有哪些?
-Xms:初始化大小内存,默认为物理内存1/64,等价于-XX:InitialHeapSize
-Xmx:最大分配内存,默认为物理内存1/4,等价于-XX:MaxHeapSize
-Xss:设置单个线程栈的大小,一般默认为512k-1024k,等价于-XX:ThreadStackSize
-Xmn 设置年轻代大小(一般不调)
-XX:MetaspaceSize 设置元空间大小
-XX:+PrintGCDetails 收集GC详细收集日志信息
-XX:SurvivorRatio 设置新生代中eden和s0/s1空间的比例(默认8:1:1)
-XX:NewRatio(配置年轻代和老年代在堆结构的占比)(默认1:2)
-XX:MaxTenuringThreshold:设置垃圾最大年龄(0-15)
谈谈你对OOM的认识
- java.lang.StackOverflowError
- java.lang.OurOfMemoryError:Java heap space
- java.lang.OurOfMemoryError:GC overhead limit exceeded
GC回收时间过长,过长的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出,加入不抛出GC overhead limit 错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU利用率一直是100%,而GC却没有任何成果
public class GCOverheadDemo {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while(true){
list.add(String.valueOf(++i).intern());
}
}catch (Exception e){
System.out.println("***************************" + i);
e.printStackTrace();
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at OOM.GCOverheadDemo.main(GCOverheadDemo.java:17)
- java.lang.OurOfMemoryError:Direct buffer memory
主要出现在使用NIO导致本地内存用光了的时候
//-XX:MaxDirectMemorySize=5m
public class DirectBufferMemoryDemo {
public static void main(String[] args) {
System.out.println(sun.misc.VM.maxDirectMemory() / (double)1024 / 1024 );
ByteBuffer bb = ByteBuffer.allocateDirect(6 *1024 * 1024);
}
}
5.0
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:693)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at OOM.DirectBufferMemoryDemo.main(DirectBufferMemoryDemo.java:13)
- java.lang.OurOfMemoryError:unable to create new native thread
导致原因:- 你的应用创建了太多线程了,一个应用进程创建多个线程,超过系统承载极限
- 你的服务器并不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报错
- java.lang.OurOfMemoryError:Metaspace
元空间内存溢出
垃圾回收器,谈谈你的理解?
垃圾回收器就是垃圾回收算法的落地实现
怎么查看默认的垃圾收集器?
java -XX:+PrintCommandLineFlags -version
CMS补充信息
CMS收集器无法清理浮动垃圾,因为并发清除阶段同用户线程一同工作,所以可能有新的垃圾产生,所以可能导致“Current Mode Failure”,就需要我们使用前面提到的Serial Old 作为后备方案;除此之外,由于CMS收集器采用的是标记-清除算法,有可能产生我们我们之前在算法篇提到的不连续的内存空间,无法分配给需要较大的空间的对象,不得不触发新一次的Full GC。
写屏障+增量更新