JVM类加载到对象创建以及常见OOM

JVM类加载到对象创建

jvm内存区域

jvm内存加载区域
  1. 模型图

[外链图片转存失败(img-e3bsrs9J-1564888367035)(assets/企业微信截图_93ca3b6f-cc99-475b-9657-10a8e526a9e9.png)]

  1. 运行时数据区域
    1. 程序计数器:线程私有,pc寄存器的一种,可以认作为当前线程的行号指示器,不会出现OOM

    2. java虚拟机栈:线程私有,栈描述的是Java方法执行的内存模型。

      1. 每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到执行完成的过程,就对应栈帧从入栈到出栈的过程
      2. 局部变量表中,存储的就是基本数据类型以及对象引用;
      3. stackOverflowError:线程请求的栈深度大于虚拟机所允许的深度; OutOfMemoryError:虚拟机栈动态拓展时无法申请到足够的内存
    3. 本地方法栈:线程私有,虚拟机使用到的native方法服务,可能底层调用的c或者c++,和虚拟机栈区别就是,虚拟机栈为java方法调用,本地方法栈为native方法调用

    4. java堆(java-heap):线程共享的,堆是java虚拟机管理内存最大的一块内存区域,存放的是对象;可以通过配置(-Xmx和-Xms)来拓展堆空间
      在这里插入图片描述

      老年代:三分之二的堆内存,生命周期比较长,通常采用标记-压缩算法进行垃圾回收
      年轻代:三分之一的堆内存,生命周期比较短,用复制算法对新生代进行垃圾回收

      • eden区:8/10 的年轻代空间
      • survivor0区(from survivor):1/10 的年轻代空间
      • suvivor1区(from survivor):1/10 的年轻代空间
    5. 方法区(Non-heap):线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量

      • 运行时常量池:常量池用于存放编译期间生成的各种字面量和符号引用。

      jdk1.7以前,方法区被称为永久代(Perm区),1.8以后使用metaspace代替永久代,两者只是方法区不同的实现方式而已;相对于永久代而言,metaspace最大的区别就是放在本地内存中而非jvm中,仅受本地内存大小的限制;

    6. 直接内存:jdk1.4引入了NIO,它可以使用Native函数库直接分配堆外内存。

着重关注虚拟机栈,java堆以及方法区三个块区域;

分析下retail-finane的启动脚本;

#!/bin/sh
MEMORY=`free -m | awk '/Mem/{print $2}'`
if [ $MEMORY -gt 15000 ];then
    JVM_HEAP="8192"
elif [ $MEMORY -gt 7500 ];then
    JVM_HEAP="4096"
elif [ $MEMORY -gt 3500 ];then
    JVM_HEAP="2048"
elif [ $MEMORY -gt 1800 ];then
    JVM_HEAP="1024"
else
    JVM_HEAP=$((MEMORY/7*4))
fi
JVM_EDEN=$((JVM_HEAP/8*3))
JVM_META=$((JVM_HEAP/4))
JVM_MAX_META=$((JVM_HEAP/2))
ACTIVE_PROFILE=qa
JAVA_OPTS="-server -Xms${JVM_HEAP}m -Xmx${JVM_HEAP}m -Xmn${JVM_EDEN}m -XX:MetaspaceSize=${JVM_META}m -XX:MaxMetaspaceSize=${JVM_MAX_META}m -XX:SurvivorRatio=8 -Xss512k -XX:-UseAdaptiveSizePolicy -XX:+PrintPromotionFailure -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/retail-finance/oom.hprof -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:GCLogFileSize=100M -Xloggc:/data/logs/retail-finance/gc.log"

以机器2g内存为例:

  1. 堆内存共分配了1g;

  2. eden分配了1*(3/8)=375m

  3. 方法区分配了250m ,最大为500m

    [外链图片转存失败(img-nnrICgpu-1564888367041)(assets/Image-1564124533229.png)]

[外链图片转存失败(img-NTJawvTS-1564888367041)(assets/Image [2]-1564124537956.png)]

类的加载和实例化

首先了解java的数据类型,包括基本数据类型和引用数据类型;
基本类型包括byte、short、char、int、long、float、double、boolean,这些是jvm预先定义好的;引用数据类型根据虚拟机规范就是类型 (class type),数组类型(array type)和接口类型(interface type);
其中数组类是由Java虚拟机直接生成的,而类和接口的使用需要jvm加载对应的字节流来创建;

类的加载过程

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程。

主要包括加载、验证、准备、解析、初始化总共五个加载阶段,加上使用以及卸载,就是类的整个生命周期;

[外链图片转存失败(img-3Q8Ax1Tr-1564888367042)(assets/Image [2].jpg)]

  1. 加载:

    • 先查找二进制字节流,一般从jar包、网络、或者动态代理生成获取
    • 然后解析二进制流将里面的静态存储结构(类)载入到方法区,
    • 在内存中生成对应的java.lang.Class对象,作为访问方法区数据的入口,注意再hotspot中,Class对象存储在方法区;
  2. 验证:判断class文件的字节流的合法性,是否符合当前虚拟机要求;大致有4个阶段

    • 文件格式验证,验证字节流是否符合Class文件格式的规范
    • 元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
    • 字节码验证,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
    • 符号引用验证,确保解析动作能正确执行
  3. 准备:为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配
    [外链图片转存失败(img-ugas21to-1564888367046)(assets/Image [3].png)]

  4. 解析:虚拟机将常量池内的符号引用替换为直接引用的过程

以上三个过程也统称为连接(Linking)过程;

  1. 初始化:执行类构造器<clinit>()方法的过程,是真正开始执行类中的定义的代码,主要包括执行static变量赋值语句,staic{}语句块

    注意和实例构造器<init>方法区分,<init>方法主要是执行成员变量和成员对象的赋值,普通语句块的执行,构造函数的执行;

可以看到,以上5个步骤,只有加载和初始化是可以通过用户自定义去参与,其他三个阶段都是虚拟机自动完成的。

[外链图片转存失败(img-P9TO6Qhi-1564888367048)(assets/Image [4].png)]

以上类加载以及初始化的过程可以看到,所有的操作几乎都是在方法区操作,存放了类的信息,静态变量,静态代码块,以及符号引用等;

类的加载时机和初始化时机

类的加载时机没有强制约束,是虚拟机自行控制的。

类的初始化时机,虚拟机规范明确指明有且只有5种情况下会对类做初始化。

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  5. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

以上都被称之为对类的主动引用,其他所有引用类的方式都被称为被动引用

被动引用的例子:通过子类引用父类的静态字段,不会导致子类初始化;引用常量也不会触发该类的初始化;

类是如何加载的
类加载器

类加载器:就是指实现加载类整个动作的代码模块;

类加载器和类本身 确定该类再虚拟机中的唯一性;

java中只有2种加载器类型:

  • 一种是启动类加载器(Bootstrap ClassLoader),由C++实现,是虚拟机自身的一部分;

    1. 负责加载$JAVA_HOME/lib中照虚拟机可识别的(按文件名识别)加载到虚拟机中,java无法获取到
  • 另一种就是所有其他的类加载器,由java实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader,还可以细分为以下3类:

    1. 扩展类加载器:负责加载$JAVA_HOME/lib/ext目录中的类库,可以在java中获取到
    2. 应用程序类加载器:负责加载Classpath上所指的的类库,也称之为系统类加载器,可以在java中获取到;
    3. 自定义的类加载器;
双亲委派模型

[外链图片转存失败(img-7mc6jdcv-1564888367048)(assets/Image [5]-1564124998516.png)]

上图就是类加载器之间的层次关系,称之为双亲委派模型;

什么是双亲委派模型?

双亲委派模型的工作流程

  1. 类加载器收到类加载的请求;
  2. 把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
  3. 启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,通知子加载器进行加载。如果最初发起类加载请求的子类也无法加载时,就抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载

如下就是双亲委派模型的代码实现,

    protected Class<?> loadClass(String name, boolean resolve)
        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 {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 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
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

双亲委派模型的优点:java类随它的类加载器一起具备了带有优先级的层级关系,保证了java程序的稳定运作;

双亲委派模型的破坏

为什么要破坏双亲委派模型?

举个例子,当要父类加载器需要调用加载子类加载器才能加载的类时,就无法通过双亲委派模型去加载需要的类,最典型的就是JDBC。

类的实例化
类的实例化方式

前面说的是类的初始化,接下来说下类的实例化。

先进行类的初始化,把类加载到虚拟机中,然后会进行类的实例化进行对象的创建;

实例化,就是由执行类实例创建表达式而引起的对象创建。有以下几种创建对象的方式:

  1. 使用new关键字创建对象
  2. 通过反射创建对象:
    • 使用Class类的newInstance方法;
    • 使用Constructor类的newInstance方法;
  3. 使用Clone方法创建对象
  4. 使用反序列化机制创建对象
package com.darkman.jvm.createobject;

import com.darkman.jvm.Employee;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author wbhe
 * @desc
 * @date 2019-07-25
 * @ver
 **/
public class ObjectTest {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, CloneNotSupportedException, IOException {
        // new 创建对象
        Employee employee = new Employee("new");
        System.out.println("employee"+"=====" + employee.toString());

        // Class类的newInstance()创建的对象, 调用的是无参构造;
        Employee employee1 = (Employee)Class.forName("com.darkman.jvm.Employee").newInstance();
        System.out.println("employee1" +"====="+ employee1.toString());

        // Constructor类的newInstance方法, 可以调用有参构造函数,甚至是私有构造函数
        Employee employee2 = Employee.class.getConstructor(String.class).newInstance("有参数的");
        System.out.println("employee2" +"====="+ employee2.toString());

        // Constructor类的newInstance方法, 私有构造函数
        Constructor constructor = Employee.class.getDeclaredConstructor(Long.class);
        constructor.setAccessible(true);
        Employee employee3 = (Employee) constructor.newInstance(1234L);
        System.out.println("employee3" +"====="+ employee3.toString());

        // clone创建,不会调用构造函数
        Employee employee4 = (Employee) employee.clone();
        System.out.println("employee4" +"=====" + employee4.toString());

        // 反序列化创建对象,不会调用构造函数
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employ"));
        oos.writeObject(employee);
        File file = new File("employ");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        Employee employee5 = (Employee)ois.readObject();
        System.out.println("employee5" +"=====" + employee5.toString());
    }
}

类实例化的过程
  • 为对象分配内存(类加载后就知道需要分配的内存大小);

    • 2种内存分配方式:
      1. 指针碰撞,内存比较规整使用指针碰撞;
      2. 空闲列表(虚拟机维护的一张空闲内存的列表),内存不是规整的,则使用空闲列表,
    • 内存分配的线程安全问题,用如下2种方式结合来保证原子性:
      1. CAS+失败重试;通过该方式分配TLAB内存;
      2. 本地线程分配缓冲(TLAB),通过-XX:+/-UseTLAB 设置是否使用TLAB;使用该方式分配对象内存
    • 常见的内存分配机制
      1. 对象优先分配再Eden
      2. 大对象直接分配在老年代;通过设置-XX:PretensureSizeThreshold参数设置临界值,超过这个值的对象将分配在老年代
      3. 长期存活的对象将进入老年代;当进行过1次young gc(minor gc)仍然存活,年龄设置为 1并进入survivor区,每熬过一次young gc年龄+1,当年龄大于指定的阈值(默认为15),该对象将会进入老年代;可以通过-XX:MaxTensuringThreshold设置阈值;
      4. 动态对象年龄判断;survivor区中相同年龄的对象占据survivor区大小的一半,则大于或者等于改、该年龄的都会进入老年代;
      5. 空间分配担保
        1. young gc前,jvm先检查老年代可用的最大连续空间是否大于新生代所有对象总空间大小;如果是直接进行young gc;如果不是进行下一步;
        2. 判断HandlePromotionFailure是否设置为true,如果为false,则直接进行full gc;如果为true则进行下一步;
        3. 判断老年代可用的最大连续空间是否大于历次进入老年代空间对象的平均大小;如果小于,则直接进行full gc;如果大于,继续进行young gc;
  • 将内存空间都初始化为零值(见上文零值图片);

  • 设置对象的基本信息(对象头);对象属于哪个实例,如何找到类的元数据信息,哈希吗,gc分代年龄等;

    以上步骤,对象已经产生了,只是还没有进行初始化值的设置;

  • 按顺序进行实例变量的初始化;执行<init> 方法,对实例变量按照编写的代码进行初始化设值(注意和零值的区别);

  • 执行调用构造函数 ;

对象创建的整体过程如下:

在这里插入图片描述具体的对象分配流程:

在这里插入图片描述

对象内存的分布

对象在内存中存储的布局可以分为三块区域:对象头(header),实例数据(Instance Data)和对齐填充(Padding)。

对象头:存储对象自身的运行时数据,以及类型指针;如果对象是数组,还会记录数组长度;

  1. 运行时数据包括,哈希码、gc分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳

  2. 类型指针,指的就是指向对象元数据的指针,即该对象指向那个类

实例数据:存储对象真正的有效信息,即对象中各个字段的内容

对齐填充:不是必须存在,也没有特殊含义,仅做占位符作用

对象的访问定位

有2种方式,一种是句柄访问,一种是直接指针访问;

通过句柄访问对象,栈中的refrence存储的是指向句柄的地址;

img

通过直接指针访问,栈中的refrence存储的是指向对象的地址;

img

比较以上2种方式的优劣点;

常见内存溢出

上面说到,在jvm中,只有程序计数器不会出现OOM,其他几个区域都换产生OOM,简单看下几类内存溢出的情况;

堆溢出

java堆溢出的demo:-Xms设置最小值,—Xmx设置最大值;-XX:+HeapDumpOnOutOfMemoryError设置堆OOM后自动Dump内存快照;

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * @author wbhe
 */
public class HeapOOM {

	static class OOMObject {
	}

	public static void main(String[] args) {
		List<OOMObject> list = new ArrayList<OOMObject>();

		while (true) {
			list.add(new OOMObject());
		}
	}
}
栈溢出

通过设置较少栈容量,达到栈溢出的目的;-Xss参数设置

/**
 * VM Args:-Xss128k
 * @author wbhe
 */
public class JavaVMStackSOF {

	private int stackLength = 1;

	public void stackLeak() {
		stackLength++;
		stackLeak();
	}

	public static void main(String[] args) throws Throwable {
		JavaVMStackSOF oom = new JavaVMStackSOF();
		try {
			oom.stackLeak();
		} catch (Throwable e) {
			System.out.println("stack length:" + oom.stackLength);
			throw e;
		}
	}
}
方法区溢出

运行时常量池导致的内存溢出,-XX:PermSize设置方法区大小,-XX:MaxPermSize设置方法区的最大容量;

/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 * @author wbhe
 */
public class RuntimeConstantPoolOOM {

	public static void main(String[] args) {
		// 使用List保持着常量池引用,避免Full GC回收常量池行为
		List<String> list = new ArrayList<String>();
		// 10MB的PermSize在integer范围内足够产生OOM了
		int i = 0; 
		while (true) {
			list.add(String.valueOf(i++).intern());
		}
	}
}


/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author wbhe
 */
public class JavaMethodAreaOOM {

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

	static class OOMObject {

	}
}


本机直接内存溢出

本机直接内存溢出demo,-XX:MaxDirectMemorySize知道直接内存的大小,如果不指定,默认和-Xmx值一样;

/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 * @author wbhe
 */
public class DirectMemoryOOM {

	private static final int _1MB = 1024 * 1024;

	public static void main(String[] args) throws Exception {
		Field unsafeField = Unsafe.class.getDeclaredFields()[0];
		unsafeField.setAccessible(true);
		Unsafe unsafe = (Unsafe) unsafeField.get(null);
		while (true) {
			unsafe.allocateMemory(_1MB);
		}
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值