类加载 双亲委派 内存模型 对象内存分配 逃逸分析学习记录

类加载+双亲委派

main方法运行过程

C++语言实现的java.exe来创建jvm,和引导类加载器,并由引导类加载器来创建类加载器的启动器launcher,在类加载器启动器空参构造中就对剩下的拓展类加载器,应用程序加载器,和自定义加载器来进行了加载(本质上也是一个URL类加载器,通过类的路径来加载),加载完成之后,就会调用main方法,来启动程序;

类加载过程

加载:

将磁盘上的class文件,通过类加载器,加载到内存中,并生成一个class对象,包含了class文件中的信息;属性方法等;

类被加载到方法区后,主要包括,运行时常量池,类型信息,字段信息,方法信息,类加载器的引用,对应class实例的引用等信息;

类加载器的引用:这个类加载器实例的引用

对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的class类型的对象实例放到堆(heap)中,作为开发人员访问方法区中类定义的入口和切入点;

Jdk进行类加载的机制可以看成懒加载,只有在真正用到的时候才回去加载;

验证:

验证文件的格式的正确性,class字节码文件是16进制文件;

字节码文件(要符合一定规则)

比如开头的cafe babe标志,以及上面的0034转换成16进制就是52,代表JDK版本为JDK8;

准备:

对于加载阶段创建的class对象,因为静态变量属于class对象,这时就可以对静态变量分配内存,赋予默认值;

解析:

静态链接的过程;

符号引用:java文件中的一些关键字,变量类型,变量名,符号等,在解析的时候都会放到常量池,成为静态变量

直接引用:真实的内存地址

对于一些静态方法,或者java定义好的修饰符,关键字,符号等替换为直接引用,因为这些是JDK就规定好了的,一旦被加载到内存,它的地址不会改变,这就是静态链接;

对于

Java -p java提供的查看字节码文件工具,更便于我们浏览

Constant pool常量池,#1这种相当于它的标识,相当于序号,也可以说是key

Methodref:方法引用

Invokespecial在进行方法调用

动态链接:Math.compute();中compute在加载的时候不会去解析,真正运行的时候才会区解析,才会把符号链接到在内存中的真正地址;

可以理解为属于JDK,或者class对象的符号,用的是静态链接,属于正常的bean对象的用的是动态链接;

初始化

对静态变量 进行初始化,并执行静态代码块;

类加载过程中,执行静态代码块,在进行构造初始化init;

类加载器和双亲委派机制

类的加载过程主要有类加载器来完成,类加载器有

  1. 引导类加载器bootstrapclassloader,加载负责支撑JVM运行的核心类库,经常用到的JDK自带的类,String,Math,debug查看它为null,因为它是c实现的,不是一个java对象;
  2. 拓展类加载器extclassloader,加载支撑JVM运行的拓展jar,lib下的ext包
  3. 应用程序加载器appclassloader,加载程序中classpath,也就是JVM自带的之外的自定义的类
  4. 用户自定义类加载器,加载指定的类

类加载器中,都会保存一份它的父的类加载器,

查看引导类加载器加载的路径;

这个是appclassloader,加载的包,它不仅加载了我们定义的,它还包含了JDK的核心类库包,以及拓展类包,上面说核心类库由引导类加载器加载,拓展类包由拓展类加载器extclassloader加载,而应用程序加载器appclassloader的加载包中却有这些,这里就引出了双亲委派机制

双亲委派加载过程图解

  1. 向上委托查找,查看当前加载器是否加载过该类,没有则交给父加载器加载;
  2. 委托查找时,每个加载器加载过的类,都会有一个缓存,向上查找就是查找加载的class缓存;
  3. 向下加载,当顶层加载器引导类加载器,都没有加载过,那就让引导类加载器进行加载,如果有加载权限,就加载,没有加载权限,则交给子加载器加载;

向上查找就是为了保护JDK自己的类,不被用户自定义的加载器加载;而且为了保护不被其它自定义的加载器加载,还有个沙箱保护机制,当我们自定义类的包名,不允许和JDK定义好的包名一致,在类加载的过程中会进行判断;

全盘负责委托机制

当一个classloader装载一个类时,除非显示的使用另外一个classloader,装载该类时,需要引用的其它类也由该classloader加载;

打破双亲委派机制

双亲委派的代码,在classloader中实现的,向上查找,向下加载,findclass()

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();
//这里正真的去findclass加载
                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;
    }
}
 

以上源码可以看出,它先查找了当前类加载器有没有加载,没加载再去父类loadeclass()加载,

所以这里我们可以继承一个loadclass,重写loadclass方法,小小的改动,只要让它查找不到的时候不去用父类去loadeclass,而是直接findclass();修改后如下;

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) {
//这里直接当前加载器加载
                    findclass(name);
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
 

这里直接这么改的话,会出现问题,因为之前提到的全盘负责委托机制,我们定义的类中,它所有的类都由我们自定义的加载器加载,而有的类只能是引导类加载器加载,所以我们只需要在原来的基础上做小改动

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(name.contains("com.chenlei")){
                    c = findClass(name);
                }else {
                    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;
    }
}

Tomcat的一个WEB容器,可能是需要部署多个应用程序,多个war包,不同的应用程序,可能依赖的同一个第三方类库的不同版本,因此要保证每一个应用有它自己的独立的类库,来保证相互隔离;

Tomcat的应用隔离,它每个应用有自己的webappclassloader,用来加载自己目录下的class文件,不会传递给父类加载器,打破了双亲委派机制;

热部署原理,当加载了类过后,卸载加载器,然后再次需要的话,重新加载;

同一个JVM中,不同路径下,包名相同,类名相同,不同的classloader;这就相当于tomcat加载

JVM内存模型

JVM虚拟机的组成

类装载子系统

执行引擎

运行时数据区

  1. 类装载子系统

class文件,通过类装载子系统,把class文件放到了数据区,然后通过执行引擎来执行;

堆(Heap)

GC:执行引擎在后台会维护一个GC线程来做对象回收;

堆分为年轻代和老年代,年轻代又分为eden(伊甸园区)和servivor区,它们的默认内存大小比例为老年代:年轻代=2:1;eden:s0:s1=8:1:1;

新建的对象,都会放入Eden中,当Eden中的内存地址被对象放满之后,就会触发young_GC,从GC_ROOT根节点根据引用链来判断是否是需要回收的对象;如果是存在引用,那么这个对象以及引用链上的对象都会认为是有效对象,就会从Eden区移动到servivor区,并且分代年龄加1;剩余的留在Eden区的对象就会被认为是垃圾对象,进行回收;

当Eden重复被放满的情况下,有效对象会在s0和s1之间来回挪动,每次挪动分代年龄+1,当分代年龄达到一定值后,就会向把对象放在老年代中,或者来回挪动时,s0或者s1放不下了,也会放到老年代中;

当老年代被放满时,就会触发一次full_GC,对整个堆和方法区来进行一次回收,当回收过后,老年代还是放满的,这时再有对象放入老年代时,就会OOM;

Java 提供的JVM监控控工具 jvisualvm

STW机制

Stop the world,当jvm进行GC的时候,会把工作线程全部暂停,用户就会感觉是程序卡了一样,特别是FULL_GC它扫描的需要进行GC的区域比较大,STW的时间就会更长;

设置STW机制的原因,个人理解,有点像数据库各级级别中的可重复读,可重复读它可以粗略的理解成是一个快照,每次读的数据一样;这里就是要确保扫描结果的准确性,和一致性(GC完之后和此时内存中对象状态一致),但这里它是直接不再产生新对象,避免扫描的结果不确定;比如一边生成对象,或者方法出栈,局部变量对象的引用发生改变,一边之前扫描过的对象前一秒还是有效的,后一瞬间就变成了垃圾对象,之前的扫描感觉就像是无效的;

栈(stack)

不同的线程,在执行代码的时候,都会有自己的栈(所以也可以称为线程栈),来存放当前线程的局部变量,当方法被执行时,也会马上在在线程栈中分配一个栈帧,把不同方法的局部变量隔离开来;

方法进栈的原则是先进后出,后进先出;因为后进栈的方法先执行完,执行完它先出栈的话,它局部变量也就用完了,就能先释放掉内存空间;

栈帧:

方法进栈时分配的内存,主要含有局部变量表,操作数栈,动态链接,方法出口

局部变量表:

存放局部变量,其实是局部变量在内存中的地址,真正的局部变量对象放在堆中;

操作数栈:

需要进行修改计算等操作的数据的值,都是加载到操作数栈中,然后CPU从操作数栈中去取要进行操作的变量,然后把操作计算完的结果再压回操作数栈中;相当于一个临时中转站;

动态链接:

在运行的时候,存放调用的非静态的方法在内存中的实际地址;

方法出口:

记录当前方法调用完毕,下面该执行哪一行;

程序计数器

用来记录当前线程代码执行的位置;因为代码是由执行引擎来执行的,它执行完一行代码,字节码执行引擎都会去修改它到对应的位置;

这主要是考虑到多线程的情况,便于进行线程恢复的时候定位;

注:紫色的部分是每个线程都会有的,不会共享;

黄色是所有线程共享的;

方法区(元空间)

运行时常量(比如final修饰的)+静态变量+类信息,比如 static User user= new User();这时user对象就是一个静态变量,它就会在方法区中,存放一份user对象在堆中的地址;

本地方法栈

本地方法是指,JDK提供的,由java去调用其它语言的接口,当本地方法调用的时候,不管什么语言实现的,它都会在内存中分配内存空间,这个内存空间就是分配在本地方法栈;

也就是线程在调用本地方法的时候分配的内存空间;

总结

jvm虚拟机中,堆和方法区是所有线程共享的部分;

栈和程序计数器是线程创建的时候,分配的,如果要进行一些本地方法的调用,线程还会分配一个本地方法栈,这些在线程之间都不是共享的;

方法区中静态变量,如果这个静态变量刚好是我们创建的对象,也是只存放了堆中创建对象的地址;

JVM内存参数设置

-XX:MetaspaceSize

-XX:MaxMetaspaceSize

对于方法区,它存放了类信息,它直接使用的是物理内存,而且有一个内存空间的自动伸缩机制,我们不去设置值的时候,它的默认值是21M,当21M内存被放满后,就会触发FULL_GC,会根据FULL_GC之后,释放的空间大小,判断对它的值进行扩大还是缩小;所以合理设置元空间的初始内存和最大内存,能有效减少FULL_GC次数;

-Xss

它也是直接使用的是物理内存,是用来设置每个线程分配的栈空间的大小,默认为1M,当方法不停的嵌套,然后进栈不出栈的情况下,栈总会被放满的,合理设置可以减少程序出错,导致递归调用的次数,也能满足日常调用而尽可能少的出现栈溢出情况;

而且栈分配的小,同等情况下,理论上能开启的线程数也越多;

对象创建过程

内存分配:

划分内存的方法,

指针碰撞:在JVM的堆中,对象是有序存放的,空闲内存和占用内存是有一个指针作为分界点,当新建一个对象的时候,就把分界点挪动和新建对象大小一样的距离;

空闲列表:JVM堆中的对象并不是有序繁殖的,所以JVM维护一个列表,来标识哪些内存空间还是空闲状态,分配的时候从空闲里面划分一个能放置新建对象的内存,然后更新列表;

不管是哪种分配方式,当多个线程去创建对象,同时需要在堆中进行内存分配,就会存在并发的问题;

内存分配时并发问题的解决

CAS

几个线程同时抢占这一块空间,谁抢到谁使用,没抢到的加上重试机制;、

本地线程分配缓冲

-XX:+/-UsrTLAB 设置虚拟机是否开启本地线程分配缓冲

-XX:TLABSize 指定TLAB的大小;默认是伊甸园区的1%;

提前给线程在堆中分配好一块专属的内存空间;JDK8默认开启;

初始化

属于对象的成员变量,也会先给一个默认的初始值,

设置对象头

标记字段(Mark Word):

类型指针(Klass pointer):堆内存中对象头里边,存放的地址,指向方法区中的类信息;

类信息:粗略理解为存放了class代码信息,用c和c++实现的对象;而class对象是提供给java开发人员的一个java对象,放在堆中;

对象在内存中的分布,其中object header是对象头,除了后边有一个alignment是对其补充,其它的是实例数据;aliment/padding是实例数据内部的对其;为了满足对象大小为8bit的倍数,这样读取效率更高;

对象的指针压缩

将对象头中的类型指针,和数据进行指针压缩,减小对象占用内存的大小,JDK1.6之后默认开启的;

内存和操作系统位数的关系

内存是需要指针来进行表示的,32位指针,用二进制来表示就是能存2的32次方个数,也就是能代表2的32次方个内存地址,转换成内存大小就是4个G;

正常32位的指针,只能表示4个G的内存,如果是16G的内存,本来是用2的35次方也就是至少35位的操作系统,来表示,但是有了指针压缩过后,就能用32位地址,就能支持16G内存;

当堆内存大于32G的时候,指针压缩会失效,会强制使用64位,来堆java空对象寻址,

Init方法

给对象的成员变量真正赋值,并调用构造方法;

对象内存分配

TLAB指开启本地线程分配缓冲,在伊甸园区提前分配堆内存,来提高并发;

-XX:+DoEscapeAnalysis逃逸分析

JVM可以通过设置-XX:+DoEscapeAnalysis,来确定是否开启逃逸分析,在进行内存分配的时候,会先去进行对象逃逸分析;JDK7之后默认开启;

对象逃逸分析:也就是分析对象的作用域,当一个对象在方法中被定义,如果它可能会在别的地方引用了,那么就可以人为它的作用域逃出了当前方法;比如test1,当它不会再别的地方被引用,作用域很确定只在当前方法中,那么就认为它没有逃逸;

对于没有逃逸的对象,就可以在栈上的方法的栈帧内存里分配内存空间,当方法出栈的时候,对象占用的内存空间随着出栈就销毁了,减轻了GC的压力;

所以不是所有的创建的对象都在堆内存中;

XX:+EliminateAllocations标量替换:

通过逃逸分析,不会逃逸的对象,并且可以被进一步分解时,JVM不会创建该对象,可以在栈帧中分配内存空间,但是栈帧的内存空间本来就不够大,可能没有一块连续的空间来存储当前对象,它就把对象拆分,然后放置到栈帧中的不连续的内存空间中;

逃逸分析和标量替换同时打开的时候,对于会逃逸的对象,在栈上分配内存,对于不逃逸的对象,进行标量替换来分解对象,也放在栈上这样可能会大量减少GC次数;

对象在年轻代中的分配 -XXUserAdaptiveSizePolicy年轻代内存分配自动变化

Young GC:新生代发生的垃圾搜集动作,回收频繁,回收速度快;

Full GC:回收堆中老年代,年轻代和方法区中的垃圾;一般回收速度会慢10倍以上;

因为Eden区的对象大多都是朝生夕死,存活时间很短,所以年轻代中让Eden区尽量大是合理的,默认8:1:1,JVM默认开启-XXUserAdaptiveSizePolicy,会导致比例自动变化,不想自动变化可以关闭;

大对象直接进入老年代

大对象是指需要大量连续空间的对象,JVM参数 -XX:PretenureSizeThreshold可以定义大对象的大小,超过这个值,直接放到老年代中,这个参数只在Serial(-XX:UseSerialGC)和ParNew两个收集器中有效;

避免在年轻代中来回复制,大对象复制慢,降低性能;

长期存活的对象尽早放进老年代

可以通过设置对象年龄参数,来控制长期存活数据放入老年代的时间,如果大部分对象都是一次性的,或者是长期存活的对象,就可以适当调小年龄,来让对象尽早进入老年代;通过参数-XX:MaxTenuringThreshold来设置;

对象动态年龄判断机制

当一批对象从Eden区,或者从一个servivor区,放入另一个servivor区时,如果这一批对象的总大小,大于servivor区的50%,那么准备放入对象的这个servivor区中,年龄大于这一批对象中最大的年龄的数据,都会放入老年代;这就是动态年龄判断机制,它 一般实在有young GC之后触发

对象内存回收

如何确定对象是否位垃圾对象;

  1. 引用计数算法

当对象,在内存中,每有一个引用就对它进行引用计数+1;当引用失效就-1,当引用计数为0时,对象被视为垃圾对象;JVM一般不用这种算法,它比较难解决对象之间的相互引用的问题;

  1. 可达性分析算法

将GC-ROOT对象作为起点,从这个的节点向下搜索引用对象,找到的对象标为非垃圾对象,

GC-ROOT根节点:线程栈的本地变量,静态变量,本地方法栈的变量等;

常见的引用类型

方法区中回收主要回收无用的类

除了自定义加载器,JDK提供的Classloader 几乎不会被回收掉,所以如果是通过应用类加载器,拓展类加载器,和引导类加载器来加载的class,那么它只要加载过一次,方法区中的类就不会被回收;Tomcat热部署是通过卸载加载器,来实现的,类加载器是重新创建的,所以这种自定义加载器加载的类是可以进行回收的;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值