程序员必备:JVM核心知识点总结( 二 )


文章插图
 
推荐博客(如果英语好的话,也可以去官网查看):
字节码指令大全
然后,再结合我们上面的Java虚拟机栈这块知识,就能轻松阅读了 。
栈帧的创建是需要耗费资源的,尤其是对于 Java 中常见的 getter、setter 方法来说,这些代码通常只有一行,每次都创建栈帧的话就太浪费了 。
另外,Java 虚拟机栈对代码的执行,采用的是字节码解释的方式,考虑到下面这段代码,变量 a 声明之后,就再也不被使用,要是按照字节码指令解释执行的话,就要做很多无用功 。
另外,我们了解到垃圾回收器回收的目标区域主要是堆,堆上创建的对象越多,GC 的压力就越大 。要是能把一些变量,直接在栈上分配,那 GC 的压力就会小一些 。
3.程序计数器既然是线程,就要接受操作系统的调度,但总有时候,某些线程是获取不到 CPU 时间片的,那么当这个线程恢复执行的时候,它是如何确保找到切换之前执行的位置呢?这就是程序计数器的功能 。
和 Java 虚拟机栈一样,它也是线程私有的 。程序计数器只需要记录一个执行位置就可以,所以不需要太大的空间 。事实上,程序计数器是 JVM 规范中唯一没有规定 OutOfMemoryError 情况的区域 。
4.本地方法栈与 Java 虚拟机栈类似,本地方法栈,是针对 native 方法的 。我们常用的 HotSpot,将 Java 虚拟机栈和本地方法栈合二为一,其实就是一个本地方法栈,大家注意规范里的这些差别就可以了 。
5.元空间元空间是一个容易引起混淆的区域,原因就在于它经历了多次迭代才成为现在的模样 。关于这部分区域,我们来讲解两个面试题,大家就明白了 。

  • 元空间是在堆上吗?
答案:元空间并不是在堆上分配的,而是在堆外空间进行分配的,它的大小默认没有上限,我们常说的方法区,就在元空间中 。
  • 字符串常量池在那个区域中?
答案:这个要看 JDK 版本 。
在 JDK 1.8 之前,是没有元空间这个概念的,当时的方法区是放在一个叫作永久代的空间中 。
而在 JDK 1.7 之前,字符串常量池也放在这个叫作永久带的空间中 。但在 JDK 1.7 版本,已经将字符串常量池从永久带移动到了堆上 。
所以,从 1.7 版本开始,字符串常量池就一直存在于堆上 。
  • 为什么使用元空间替换永久代?
表面上看是为了避免OOM异常 。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误 。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制啦 。
6.直接内存直接内存,指的是使用了 Java 的直接内存 API,进行操作的内存 。这部分内存可以受到 JVM 的管控,比如 ByteBuffer 类所申请的内存,就可以使用具体的参数进行控制 。
需要注意的是直接内存和本地内存不是一个概念 。
  • 直接内存比较专一,有具体的 API(这里指的是ByteBuffer),也可以使用 -XX:MaxDirectMemorySize 参数控制它的大小;
  • 本地内存是一个统称,比如使用 native 函数操作的内存就是本地内存,本地内存的使用 JVM 是限制不住的,使用的时候一定要小心 。
GC Roots对象主要是在堆上分配的,我们可以把它想象成一个池子,对象不停地创建,后台的垃圾回收进程不断地清理不再使用的对象 。当内存回收的速度,赶不上对象创建的速度,这个对象池子就会产生溢出,也就是我们常说的 OOM 。
把不再使用的对象及时地从堆空间清理出去,是避免 OOM 有效的方法 。那 JVM 是如何判断哪些对象应该被清理,哪些对象需要被继续使用呢?
这里首先强调一个概念,这对理解垃圾回收的过程非常有帮助,面试时也能很好地展示自己 。
垃圾回收,并不是找到不再使用的对象,然后将这些对象清除掉 。它的过程正好相反,JVM 会找到正在使用的对象,对这些使用的对象进行标记和追溯,然后一股脑地把剩下的对象判定为垃圾,进行清理 。
了解了这个概念,我们就可以看下一些基本的衍生分析: