JVM快速初探

Posted HUTEROX

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM快速初探相关的知识,希望对你有一定的参考价值。

前言

本笔记基于狂神进行整理

JAVA基础回归

JVM所在位置

java 作为一个跨平台的计算机语言,其最主要的原因其实就是因为虚拟机的存在,这个虚拟机其实有点类似于Python的解释器

基本的结果和所处位置如下图:

我们发现JVM是完全在操作系统上面的,也就是JVM帮助我们抽象了 操作系统的不同而带来的问题。(操作系统抽象了机器带来的问题,上图是老早一起的了)

JAVA程序的执行

一个完整的java程序执行顺序如下

这里纠正一下,其实那个类加载器也是jvm的一部分,这里的 java 虚拟机 jvm 是指它执行程序的这一部分。

JVM 架构图

整个图非常直观。

这里简单解释一下啥是本地方法,由于java是C++写的,有些东西是没法用java实现的,而是用C++去实现,调用底层的系统API。那么这些东西就是本地方法。这个基本上是java程序员不会去触碰的东西,因为那玩意可能就是一个 private native method 然后没了。这个就交个虚拟机处理了。

Class Loader(类加载器)

现在我们把目光重新转移到我们的 类加载器 里面。

在此之前 请牢记一句话 类是模板,对象是具体的实例化 这句话在待会的草图当中非常直观。

看如下代码

public class Car 	
    public int age;
    public static void main(string[ ] args) 
        //类是模板,对象是具体的
        car car1 = new Car();
        car car2 = new Car();
        Car car3 = new Car();
    	system.out.print1n( car1.hashcode());
        system.out.println( car2.hashcode());
        system.out.println( car3.hashCode());
        
    	Class<?extends car> aclass1 = car1.getclass();
        Class< ?extends Car> aclass2 = car2.getclass() ;
        Class< ?extends Car> aclass3 = car3.getclass() ;
        
   		system.out.println(aclass1.hashcode());
    	system.out.println(aclass2.hashcode());
        system.out.println( aclass3.hashcode());
    


再结合如下图:

你会发现 类装载器的作用就是帮助我们 获取一个类,加载初始化 ,之后去实例化。那么大致的工作流程如上。

类加载器的分类(双亲委派机制)

之所以要说这个其实是因为涉及到一个东西,叫做双亲委派机制。这个机制说句人话,其实就是为了避免关键字重名的问题,当然也可以实现性能调优。我们都知道java是个面向对象的玩意。我们经常回去创建一个类。但是有些是官方制定的,就像关键字。那么类加载器其实就回去检测你创建的类,有木有重名是不是我们这个基本的类,如果重名那么就执行我这个官方有的类,这样一来你的程序就可能出错。那么这样做也就保证了一定的安全,因为有问题的程序跑不起来~

类加载器分为一下几个

用户加载器,也就是你自己的classloader

拓展加载器,在lib下

根加载器,lang包

所以双亲委派机制就是

所以,总结就是一句话 为了安全。

OK 那么接下来还有个点没说,那就是双亲委派,委派是什么意思。

其实完整的流程是这样的。

1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,知道启动类加载器Boot
3.启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常;
通知子加载器进行加载
4.重复步骤3

所以说 创建 new 一个类还是做了很多工作的。

沙箱安全机制

这个咋说呢,你可以和360的那个沙箱机制作类比。其实很想。总结一句话就是,权限设置,作用域。那么具体的话里面涉及到很多东西。

比如 java 规定你 不能实现那些操作,能够实现那些操作,那么这个叫做权限。那么作用域的话,就是你能够操作那些层面的东西,就像Linux的用户管理一样 ,你普通用户能够去直接删除其他用户嘛,你的修改是不是只对你生效,换个用户登录就不同了。

组成沙箱的基本组件:
·字节码校验器(bytecode verifier):确保lava类文件遵循lava语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
·类装载器(class loader) :其中类装载器在3个方面对Java沙箱起作用
。它防止恶意代码去干涉善意的代码;/双亲委派机制,
。它守护了被信任的类库边界;
。它将代码归入保护域,确定了代码可以进行哪些操作。

模型:

这个是 jdk1.6以后的模型,也是我们现在的一个模型。

那么总的来说那个 委派/沙箱还是为了安全

Native 本地方法(JNI)

这个是java本身无法实现的玩意,是去调用了C / C ++ 后才能够被实现的方法。

例如 :在Thread 里面 的方法

private native void start0();

作用:

拓展 JAVA 融合不同的编程语言为java 调用,一开始是 C、C++

不过这个东西太底层了,虽然我很想尝试一下用Python来为它加入新功能,但是这个显然我做不到。那么这个并不是意味着不能做。程序设计里面不是说了嘛,数据耦合是最好的嘛。最典型的例子,就是 这个

这个网站 SpringBoot搭建的,这个功能 用 Pytroch实现 Django 部署。

方法区

这个也是基础部分

Method Area 方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
static final,Class,常量池

如图:

现在把我们的目光移植到栈里面。

栈是啥就不要说了把 stack

这个其实主要还是想要解释为什么那个方法的放置就是以这个方式放的。这里提一下那个Go语言的defer也是一样的,我们经常用的函数递归其实就是用这个语言本身帮助我们实现的栈,比如那个DFS我可以直接递归也可以用栈来写。这个没啥好说的,都有体会,那么这里就拽几个名词。那么由于栈本身的结构所以这里的话不存在垃圾回收机制,不需要要。

栈溢出

就是咱们的栈又有空间大小的,一旦超过就崩溃了。比如

public class Test()
    
    public static void main(String[] args)
        
        new Test().Test();
        
    
    public void test()
        this.a();
    
    public void a()
        this.test();
    

这个就是递归死循环,所有语言都一样的。

def b():
    a()
def a():
    b()
if __name__=="__main__":
    b()

栈内存

主管程序的运行,生命周期线程同步,线程接触栈内存释放,也就是弹栈。那个栈内存就是这里的栈。存放:8大基本类型+实例方法+对象引用。

栈帧

其实就是一个双链表栈

堆/栈/常量池/方法区的关系

堆区

这个是比较重要的,由于虚拟机 不同所以堆区也不同,这里的话我们是以sun公司的为准,用的也是这个。

Sun公司HotSpot 3ava Hotspot™64-Bit server vw (build 25.181-b13,mixed mode)

BEA JRockit

IBM 9 vM

一个jvm只有一个堆,这个堆可以设置大小

我们前面知道那个栈只是存地址,引用,我们的堆区存放了那个对象的所有东西。

堆区也分几个区域

新生区,养老区,永久区

这个概念是比较重要的里面又涉及到 轻GC 重GC

在new一个对象的时候在我们的堆区是有指向引用地址的,当没有引用的地址时就会视为垃圾数据

GC垃圾回收,主要是在伊甸园区和养老区~假设内存满了,OOM,堆内存不够!|

不过上次我写出这个样的程序还是在做Flink跑PSO的时候,维度太高跑了一会直接撑爆内存。

那么更加形象的图是这样的

永久区

永久区,这里重点说一下
这个区域常驻内存的。用来存放IDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息~,这个区域不存在垃圾回收!这个咱们可以忽略。虚拟机关了就没了。

只有当一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,才会出现OOM;

逻辑上存在,实际上不在。其他的其实大致关系就是和上图一样,只是后面的GC是我们关注的重点。

这部分内容的变化在后面的性能调优部分在说明,当前只是快速浏览。

GC(垃圾回收机制)

前面铺垫了那么久那么现在就来说说那个GC。

这里分几种。也是对应前面的图。

那么这里对应几种算法:标记清除法,标记压缩,复制算法,引用计数法。

引用计数算法

复制算法

这个一般在新生区里面使用。针对存活率低的情况。

原因很简单。

复制算法是把 幸存区 0/1 也就是 From和to区的东西复制交换的,也就是当触发GC的时候(当区域满了)那么这个时候From里面的区域内的元素就会全部给To区也就是把其中一个区域内的元素全部给另一个幸存区,那么此时你会发现 此时为空的幸存区叫做 To 区,To区和伊甸园区为空。而有元素的幸存区叫做 From区,默认情况下 当一个对象在触发了15次GC的时候还活着(有引用)那么就会进入养老区。

·好处:没有内存的碎片~
·坏处:浪费了内存空间~:多了一半空间永远是空to。假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候;新生区~I

标记清除

这个是两个步骤,先标记然后清除。

触发GC 先对有 引用的标记一下,清除 对没有标记的删掉。

缺点:多了几个碎片,你把这个理解成一个数组,有数据的位置不连续,到时候我还是要全部扫描一下才能拿到有用的值。

标记压缩

这个就是对标记清除的进一步优化,那就是再扫描一遍。

所以这个叫做,标记清除压缩算法。

先标记清除几次,后面再压缩呗~

小结

内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法

那么对于不同的区域使用了不同的算法。

年轻代:
·存活率低

​ ·复制算法!

老年代:
·区域大:存活率
·标记清除+标记压缩混合实现

以上是关于JVM快速初探的主要内容,如果未能解决你的问题,请参考以下文章

初探JVM

JVM初探

JVM初探(一):双亲委派机制

JVM之JVM初探

[Java代码审计]——远程调试初探

java 简单的代码片段,展示如何将javaagent附加到运行JVM进程