了解Java虚拟机JVM的基本结构及JVM的内存溢出方式

JVM内部结构图

Java虚拟机主要分为五个区域:方法区、堆、Java栈、PC寄存器、本地方法栈。下面
来看一些关于JVM结构的重要问题。

1.哪些区域是共享的?哪些是私有的?

Java栈、本地方法栈、程序计数器是随用户线程的启动和结束而建立和销毁的,
每个线程都有独立的这些区域。而方法区、堆是被整个JVM进程中的所有线程共享的。

2.方法区保存什么?会被回收吗?

方法区不是只保存的方法信息和代码,同时在一块叫做运行时常量池的子区域还
保存了Class文件中常量表中的各种符号引用,以及翻译出来的直接引用。通过堆中
的一个Class对象作为接口来访问这些信息。

虽然方法区中保存的是类型信息,但是也是会被回收的,只不过回收的条件比较苛刻:

(1)该类的所有实例都已经被回收

(2)加载该类的ClassLoader已经被回收

(3)该类的Class对象没有在任何地方被引用(包括Class.forName反射访问)

3.方法区中常量池的内容不变吗?

方法区中的运行时常量池保存了Class文件中静态常量池中的数据。除了存放这些编译时
生成的各种字面量和符号引用外,还包含了翻译出来的直接引用。但这不代表运行时常量池
就不会改变。比如运行时可以调用String的intern方法,将新的字符串常量放入池中。

package com.cdai.jvm; 

public class RuntimeConstantPool { 

  public static void main(String[] args) { 

    String s1 = new String("hello");
    String s2 = new String("hello");
    System.out.println("Before intern, s1 == s2: " + (s1 == s2)); 

    s1 = s1.intern();
    s2 = s2.intern();
    System.out.println("After intern, s1 == s2: " + (s1 == s2)); 

  } 

}

4.所有的对象实例都在堆上分配吗?

随着逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术使得“所有对象都分配
在堆上”也变得不那么绝对。

所谓逃逸就是当一个对象的指针被多个方法或线程引用时,我们称这个指针发生逃逸。
一般来说,Java对象是在堆里分配的,在栈中只保存了对象的指针。假设一个局部变量
在方法执行期间未发生逃逸(暴露给方法外),则直接在栈里分配,之后继续在调用栈
里执行,方法执行结束后栈空间被回收,局部变量就也被回收了。这样就减少了大量临时
对象在堆中分配,提高了GC回收的效率。

另外,逃逸分析也会对未发生逃逸的局部变量进行锁省略,将该变量上拥有的锁省略掉。
启用逃逸分析的方法时加上JVM启动参数:-XX:+DoEscapeAnalysis?EscapeAnalysisTest。

5.访问堆上的对象有几种方式?

(1)指针直接访问

栈上的引用保存的就是指向堆上对象的指针,一次就可以定位对象,访问速度比较快。
但是当对象在堆中被移动时(垃圾回收时会经常移动各个对象),栈上的指针变量的值
也需要改变。目前JVM HotSpot采用的是这种方式。

(2)句柄间接访问

栈上的引用指向的是句柄池中的一个句柄,通过这个句柄中的值再访问对象。因此句柄
就像二级指针,需要两次定位才能访问到对象,速度比直接指针定位要慢一些,但是当
对象在堆中的位置移动时,不需要改变栈上引用的值。

JVM内存溢出的方式
了解了Java虚拟机五个内存区域的作用后,下面我们来继续学习下在什么情况下
这些区域会发生溢出。

1.虚拟机参数配置

-Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。

-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。

-Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。

-XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。

-XX:MaxPermSize:设置持久代最大值。物理内存的1/4。

2.方法区溢出

因为方法区是保存类的相关信息的,所以当我们加载过多的类时就会导致方法区
溢出。在这里我们通过JDK动态代理和CGLIB代理两种方式来试图使方法区溢出。

2.1 JDK动态代理

package com.cdai.jvm.overflow; 

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy; 

public class MethodAreaOverflow { 

  static interface OOMInterface {
  } 

  static class OOMObject implements OOMInterface {
  } 

  static class OOMObject2 implements OOMInterface {
  } 

  public static void main(String[] args) {
    final OOMObject object = new OOMObject();
    while (true) {
      OOMInterface proxy = (OOMInterface) Proxy.newProxyInstance(
          Thread.currentThread().getContextClassLoader(),
          OOMObject.class.getInterfaces(),
          new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
              System.out.println("Interceptor1 is working");
              return method.invoke(object, args);
            }
          }
      );
      System.out.println(proxy.getClass());
      System.out.println("Proxy1: " + proxy); 

      OOMInterface proxy2 = (OOMInterface) Proxy.newProxyInstance(
          Thread.currentThread().getContextClassLoader(),
          OOMObject.class.getInterfaces(),
          new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
              System.out.println("Interceptor2 is working");
              return method.invoke(object, args);
            }
          }
      );
      System.out.println(proxy2.getClass());
      System.out.println("Proxy2: " + proxy2);
    }
  } 

}

虽然我们不断调用Proxy.newInstance()方法来创建代理类,但是JVM并没有内存溢出。
每次调用都生成了不同的代理类实例,但是代理类的Class对象没有改变。是不是Proxy
类对代理类的Class对象有缓存?具体原因会在之后的《JDK动态代理与CGLIB》中进行
详细分析。

2.2 CGLIB代理

CGLIB同样会缓存代理类的Class对象,但是我们可以通过配置让它不缓存Class对象,
这样就可以通过反复创建代理类达到使方法区溢出的目的。

package com.cdai.jvm.overflow; 

import java.lang.reflect.Method; 

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy; 

public class MethodAreaOverflow2 { 

  static class OOMObject {
  } 

  public static void main(String[] args) {
    while (true) {
      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(OOMObject.class);
      enhancer.setUseCache(false);
      enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object obj, Method method,
            Object[] args, MethodProxy proxy) throws Throwable {
          return method.invoke(obj, args);
        }
      });
      OOMObject proxy = (OOMObject) enhancer.create();
      System.out.println(proxy.getClass());
    }
  } 

}

3.堆溢出

堆溢出比较简单,只需通过创建一个大数组对象来申请一块比较大的内存,就可以使
堆发生溢出。

package com.cdai.jvm.overflow; 

public class HeapOverflow { 

  private static final int MB = 1024 * 1024; 

  @SuppressWarnings("unused")
  public static void main(String[] args) {
    byte[] bigMemory = new byte[1024 * MB];
  } 

}

4.栈溢出

栈溢出也比较常见,有时我们编写的递归调用没有正确的终止条件时,就会使方法不断
递归,栈的深度不断增大,最终发生栈溢出。

package com.cdai.jvm.overflow; 

public class StackOverflow { 

  private static int stackDepth = 1; 

  public static void stackOverflow() {
    stackDepth++;
    stackOverflow();
  } 

  public static void main(String[] args) {
    try {
      stackOverflow();
    }
    catch (Exception e) {
      System.err.println("Stack depth: " + stackDepth);
      e.printStackTrace();
    }
  } 

}
(0)

相关推荐

  • Java虚拟机JVM性能优化(二):编译器

    本文将是JVM 性能优化系列的第二篇文章(第一篇:传送门),Java 编译器将是本文讨论的核心内容. 本文中,作者(Eva Andreasson)首先介绍了不同种类的编译器,并对客户端编译,服务器端编译器和多层编译的运行性能进行了对比.然后,在文章的最后介绍了几种常见的JVM优化方法,如死代码消除,代码嵌入以及循环体优化. Java最引以为豪的特性"平台独立性"正是源于Java编译器.软件开发人员尽其所能写出最好的java应用程序,紧接着后台运行的编译器产生高效的基于目标平台的可执行代

  • Java虚拟机装载和初始化一个class类代码解析

    在 java 应用程序开发中,只有被 java 虚拟机装载的 Class 类型才能在程序中使用.只要生成的字节码符合 java 虚拟机的指令集和文件格式,就可以在 JVM 上运行,这为 java 的跨平台性提供条件.下面,我们来看看虚拟机是如何装载和初始化一个 class 类的. 装载一个类 学习过C/C++语言的读者知道,C/C++源代码必须首先别编译成本地的机器代码,然后还需要一个链接代码过程.该链接过程的主要任务就是:合并不同的源码文件产出的中间代码,并最终获得一个可直接执行的应用程序.然

  • Java虚拟机最多支持多少个线程的探讨

    McGovernTheory在StackOverflow提了这样一个问题: Java虚拟机最多支持多少个线程?跟虚拟机开发商有关么?跟操作系统呢?还有其他的因素吗? Eddie的回答: 这取决于你使用的CPU,操作系统,其他进程正在做的事情,你使用的Java的版本,还有其他的因素.我曾经见过一台Windows服务器在宕机之前有超过6500个线程.当然,大多数线程什么事情也没有做.一旦一台机器上有差不多6500个线程(Java里面),机器就会开始出问题,并变得不稳定. 以我的经验来看,JVM容纳的

  • Java虚拟机JVM性能优化(一):JVM知识总结

    Java应用程序是运行在JVM上的,但是你对JVM技术了解吗?这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化.之后的文章会讲JVM性能优化,包括最新的JVM设计--支持当今高并发Java应用的性能和扩展. 如果你是一个开发人员,你肯定遇到过这样的特殊感觉,你突然灵光一现,所有的思路连接起来了,你能以一个新的视角来回想起你以前的想法.我个人很喜欢学习新知识带来的这种感觉.我已经有过很多次这样

  • Java虚拟机JVM性能优化(三):垃圾收集详解

    Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源.在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制.她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的). 垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java

  • 浅谈Java的虚拟机结构以及虚拟机内存的优化

    工作以来,代码越写越多,程序也越来越臃肿,效率越来越低,对于我这样一个追求完美的程序员来说,这是绝对不被允许的,于是除了不断优化程序结构外,内存优化和性能调优就成了我惯用的"伎俩". 要对Java程序进行内存优化和性能调优,不了解虚拟机的内部原理(或者叫规范更严谨一点)是肯定不行的,这里推荐一本好书<深入Java虚拟机(第二版)>(Bill Venners著,曹晓刚 蒋靖 译,实际上本文正是作者阅读本书之后,对Java虚拟机的个人理解阐述).当然了,了解Java虚拟机的好处

  • 了解Java虚拟机JVM的基本结构及JVM的内存溢出方式

    JVM内部结构图 Java虚拟机主要分为五个区域:方法区.堆.Java栈.PC寄存器.本地方法栈.下面 来看一些关于JVM结构的重要问题. 1.哪些区域是共享的?哪些是私有的? Java栈.本地方法栈.程序计数器是随用户线程的启动和结束而建立和销毁的, 每个线程都有独立的这些区域.而方法区.堆是被整个JVM进程中的所有线程共享的. 2.方法区保存什么?会被回收吗? 方法区不是只保存的方法信息和代码,同时在一块叫做运行时常量池的子区域还 保存了Class文件中常量表中的各种符号引用,以及翻译出来的

  • java虚拟机学习笔记基础篇

    1.前言(基于JDK1.7) 最近想把一些java基础的东西整理一下,但是又不知道从哪里开始!想了好久,还是从最基本的jvm开始吧!这一节就简单过一遍基础知识,后面慢慢深入... 水平有限,我自己也是很难把jvm将清楚的,我参考一本书<深入java虚拟机第二版>(版本比较老,其实很多大佬的博客都是参考的这本书的内容...) 所谓jvm,又名java虚拟机.我们平常写java程序的时候几乎是感觉不到jvm的存在的,我们只需要根据java规范去编写类,然后就可以运行程序了,当然只有我们程序出现bu

  • 带着新人看java虚拟机01(推荐)

    1.前言(基于JDK1.7) 最近想把一些java基础的东西整理一下,但是又不知道从哪里开始!想了好久,还是从最基本的jvm开始吧!这一节就简单过一遍基础知识,后面慢慢深入... 水平有限,我自己也是很难把jvm将清楚的,我参考一本书<深入java虚拟机第二版>(版本比较老,其实很多大佬的博客都是参考的这本书的内容...),电子档pdf文件链接:https://pan.baidu.com/s/1bxs4i0gnVpz7Lkjl2fxS9g 提取码:n5ou ,有兴趣的小伙伴可以自己下载自己好好

  • java虚拟机内存溢出及泄漏实例

    测试参数设置: 1.循环调用new A()实现堆溢出,java.lang.OutOfMemoryError: Java heap space, 虚拟机参数:-Xms1M -Xmx1M -XX:+HeapDumpOnOutOfMemoryError,解释:将-Xmx和-Xms设置为一样可以避免堆自动扩展,-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照 // while (true){ // new A().do2();

  • 深入理解Java虚拟机 JVM 内存结构

    目录 前言 JVM是什么 JVM内存结构概览 运行时数据区 程序计数器 Java虚拟机栈 本地方法栈 方法区 运行时常量池 Java堆 直接内存 前言 JVM是Java中比较难理解和掌握的一部分,也是面试中被问的比较多的,掌握好JVM底层原理有助于我们在开发中写出效率更高的代码,可以让我们面对OutOfMemoryError时不再一脸懵逼,可以用掌握的JVM知识去查找分析问题.去进行JVM的调优.去让我们的应用程序可以支持更高的并发量等......总之一句话,学好JVM很重要! JVM是什么 J

  • 详解Java虚拟机(JVM)运行时

    JVM(Java虚拟机)是一个抽象的计算模型.就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域.目的是为构建在其上运行的应用程序提供一个运行环境.JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构.本文主要介绍Java虚拟机(JVM)运行时详解. 我们知道的JVM内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享. 线程不共享区域 每个线程的数据区域包括

  • java虚拟机JVM类加载机制原理(面试必问)

    目录 1.类加载的过程. 1)加载 2)验证 3)准备 4)解析 5)初始化 2.Java 虚拟机中有哪些类加载器? 1)启动类加载器(Bootstrap ClassLoader): 2)扩展类加载器(Extension ClassLoader): 3)应用程序类加载器(Application ClassLoader): 3.什么是双亲委派模型? 4.为什么使用双亲委派模式? 5.有哪些场景破坏了双亲委派模型? 1)线程上下文类加载器 2)Tomcat 的多 Web 应用程序 3)OSGI 实现

  • Java虚拟机栈jvm栈的作用

    目录 1. 定义 2. 栈的线程安全问题 3. 栈内存溢出 1. 定义 栈:线程运行时需要的内存空间,一个栈存在多个栈帧.栈具有先入后出,后入先出的特点. 栈帧:每个方法运行时需要的内存(局部变量表.操作数栈.动态链接和方法返回值等信息.),每次调用一个方法,便会将栈帧压入栈中,方法执行完毕将栈帧从栈顶压出 活动栈帧:指在栈顶的栈帧,既正在调用的方法,每个线程只能有一个活动栈帧,对应着该线程正在调用的那个方法 现在我们用代码来演示一下Java虚拟机如何将栈帧压入及压出栈中 public clas

  • Java虚拟机内存结构及编码实战分享

    推荐 原创 了解JVM内存结构的目的 在Java的开发过程中,因为有JVM自动内存管理机制,不再需要像在C.C++开发那样手动释放对象的内存空间,不容易出现内存泄漏和内存溢出的问题.但是,正是由于把内存管理的权利交给了JVM,一旦出现内存泄漏和内存溢出方面的问题,如果不了解JVM是如何使用内存的,不了解JVM的内存结构是什么样子的,就很难找到问题的根源,就更难以解决问题. JVM内存结构简介 在JVM所管理的内存中,大致分为以下几个运行时数据区域: 程序计数器:当前线程所执行的字节码的行号指示器

随机推荐