JVM入门

类加载过程

Java源代码文件会被Java编译器编译为字节码文件(.class后缀)
然后由JVM中的类加载器加载各个类的字节码文件
加载完毕之后,交由JVM执行引擎执行。

类加载器

作用:加载.class文件。
1)启动类(根)加载器 Bootstrap ClassLoader
2)扩展类加载器 Extension ClassLoader
3)应用程序(系统类)加载器 Application ClassLoader

1)启动类加载器,负责加载jre\lib目录下的rt.jar包
2)扩展类加载器:负责加载jre\lib\ext目录下的所有jar包
3)应用程序类加载器:负责加载用户类路径上所指定的类库,如果应用程序中没有自定义加载器,那么次加载器就为默认加载器

双亲委派机制

(1)类加载器收到类加载的请求,先判断此类是否已经被加载,如果类已经被加载则返回;

(2)如果类没有被加载,则先委托父类加载(父类加载时会判断该类有没有被自己加载过),如果父类加载过则返回;如果没被加载过则继续向上委托;

(3)如果一直委托都无法加载,子类加载器才会尝试自己加载。

作用
为了避免重复加载(确保Class对象的唯一性)以及JVM的安全性,需要使用某一种方式来实现只加载一次,加载过就不能被修改或再次加载。
两个类路径名称相同时,优先加载jdk中的类,自定义的类可能会破坏java程序运行。

沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox)
什么是沙箱?
沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源访问,那系统资源包括什么?
CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。

目前最新的安全模型引入了域 (Domain) 的概念。JVM会把所有代码加载到不同的系统域应用域,系统域部分专门负责与关键资源系统进行交互,而每个应用域部分则通过系统域的部分代理来对各种需要的资源进行精细划分然后可以进行访问。JVM虚拟机中不同的受保护域 (Protected Domain)对应不一样的权限 (Permission)。存在于不同域中的类文件就拥有了它所包含应用域所有可访问资源之和。
在这里插入图片描述

沙箱的基本组件

  • 字节码校验器(bytecode verifier)
    确保java类文件遵循java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。

  • 类加载器(class loader)
    防止恶意代码去干涉善意的代码,比如:双亲委派机制
    守护了被信任的类库边界;
    将代码归入保护域,确定了代码的权限范围可以进行哪些资源操作

  • 存取控制器(access controller)
    存取控制器可以控制核心API对操作系统的存取权限,用户可以设定控制策略。

  • 安全管理器(security manager)
    安全管理器主要是核心API和操作系统之间的主要接口。比如实现权限控制,比存取控制器优先级高。

  • 安全软件包(security package)
    java.security下的类和扩展包下的类,允许用户为应用增加所需要安全特性:安全提供者、消息摘要、数字签名keytools、加密、鉴别。

栈、本地方法栈、程序计数器、方法区

在这里插入图片描述
新建对象创建:

  • 分配内存空间:首先,Java 虚拟机(JVM)会在堆(Heap)中分配一块内存空间,用于存储对象的实例变量。
  • 初始化对象:一旦内存空间分配完成,JVM 就会调用对象的构造函数来初始化对象。构造函数是用于初始化对象的特殊方法。在初始化过程中,可以执行一些初始化操作,比如初始化实例变量。
  • 设置对象的引用:一旦对象初始化完成,JVM 将返回一个对该对象的引用(即对象的内存地址)。这个引用可以赋值给一个变量,从而允许程序访问和操作对象。

下图中,new Person() 这行代码执行了对象的创建过程。JVM 在堆中分配了一块内存空间来存储 Person类的实例变量,然后调用了 Person 类的构造函数来初始化对象。最后,JVM 返回了指向这个对象的引用,使我们能够通过 person 这个变量来操作对象。
在这里插入图片描述
JVM的内存结构包括5大区域:方法区,堆区,虚拟机栈,本地方法栈,程序计数器。

方法区(Method Area):存储存储类的信息、运行时常量池、静态变量等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。方法区是所有线程共享的

  • 元空间(MetaSpace),元空间是方法区的实现,它的出现是为了替代永久代(在JDK1.8版本废弃了永久代,用元空间替代)。元空间的最大好处是它不在虚拟机中,而是在本地内存中
  • 在JDK 7之前,方法区通常被实现为堆的一个逻辑部分,称为永久代(PermGen space)。从JDK 8开始,HotSpot VM移除了永久代,并引入了元空间(Metaspace)作为方法区的实现

堆(Heap):存储java对象、字符串常量池(jdk1.7后从运行时常量池中分离)的地方。这块是GC的主要区域。从存储的内容我们可以很容易知道,堆是所有线程共享的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

栈 / 虚拟机栈(Stack):主管java线程的运行,存储方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果、并参与方法的调用和返回。栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧并放入栈顶部,用于存储局部变量表、操作栈、方法返回值等。Java虚拟机栈是Java方法执行的内存模型,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。所以栈是线程私有的,生命周期和线程一致。

程序计数器(PC寄存器/PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。程序计数器记录【当前线程所执行的Java字节码的地址】。当执行的是Native方法时,程序计数器为空。

本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。HotSpot虚拟机直接把本地方法栈和java栈合二为一。

堆(heap)

GC的主要区域

  1. JVM内存划分为堆内存和非堆内存,
  • 堆内存分为年轻代(Young Generation)、老年代(Old Generation),
  • 非堆内存就一个永久代(Permanent Generation)。(这个非堆,严格意义上来说也是堆,但逻辑操作上又不是堆…)
  1. 年轻代又分为Eden和Survivor区。
  • Survivor区由FromSpace和ToSpace组成。
  • Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
  • Eden满了就触发轻GC,
    经过轻GC存活下来的就到了幸存者区,
    幸存者区满之后意味着新生区也满了,则触发轻GC,
    经过多次轻GC之后存活下来的就到了老年代。
  1. 堆内存用途:
    存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
  2. 老年代:
  • 在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。
  • 老年代中的对象生命周期较长,存活率比较高,
  • 在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
  1. 非堆内存用途:永久代,也叫方法区存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

GC(Garbage Collection)

GC是垃圾收集的意思。GC是将java的无用的堆对象进行清理,释放内存,以免发生内存泄露。

JVM的内存回收过程

新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor from区,Survivor from区满后触发执行Minor GC,Survivor from区存活对象移动到Suvivor to区,这样保证了一段时间内总有一个survivor区为空,from区和to区互换。经过多次Minor GC仍然存活的对象移动到老年代。老年代存储长期存活的对象,占满时会触发Major GC。若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC。GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时

Minor GC

新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

MajorGC

MajorGC发生在老年代(采用标记—清除算法 ),老年代的对象比较稳定,对象存活的时间比较长。所以 MajorGC 不会频繁执行。在进行 MajorGC(也叫老年代GC) 前一般都先进行了一次 MinorGC(新生代GC),使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

Full GC

清理整个堆空间,包括年轻代和永久代

OOM如何处理

  1. 首先通过内存映射分析工具 对 dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory )
  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与 GC Roots 相关联并导致垃圾收集器无法自动回收他们的。掌握了泄漏对象的类型信息,以及 GC Roots 引用链的信息,就可以比较准确定位出泄露代码的位置
  3. 如果不存在内存泄漏,那就应该检查虚拟机的堆参数(-Xmx 与 -Xms), 与机器物理内存对比看是否还需要调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗
//返回虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory();
//返回jvm初始化的总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("max:"+maxMemory+"字节,\t"+(maxMemory/(double)1024/1024)+"MB");
System.out.println("total:"+totalMemory+"字节,\t"+(totalMemory/(double)1024/1024)+"MB");
# 扩大maxMemory和totalMemory的堆内存大小,打印GC信息
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值