
1. 在Java的虚拟机模型里, 字节码解释器工作时就是通过改变这个计数器的值来选取下一个需要执行的指令。(分支,循环, 跳转, 异常处理, 线程恢复)
2. 在多线程的情况下, 一个处理器只能执行一条线程, 那么每个线程是轮流切换, 通过分配处理器执行时间的方式实现, 即在线程切换的时候, 每个线程需要自己记录自己的程序计数器, 所有是线程私有的, 各个线程之间互不干扰
Java虚拟机栈1. 栈也是线程私有的, 在Java方法执行的时候, 会创建一个栈帧, 用于存储
局部变量表,是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基 本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)
*** 作数栈,会将局部变量表或对象实例的字复制常量或变量写入到 *** 作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者 返回给方法调用者,也就是出栈/入栈 *** 作
动态连接,将符号引用转换为直接引用
方法出口, 无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
2. 本地方法栈: 调用native方法服务
Java堆1. 几乎所有的实例对象, 数组都存储在堆中, ‘几乎’ 指, 随着即时编译技术的进步, 尤其是逃逸分析技术逐渐强大, 栈上分配, 标量替换等优化手段以及导致一些微妙的变化悄然发生!
2. 特点:线程共享,内存最大,唯一目的存放对象实例, 堆中也有私有线程缓冲区(TLAB)
3. java堆又被称为GC堆,常用的分代收集算法,在堆中可以体现为, 年轻代,老年代
4. java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xm控制)
5. 配置新生代与老年代的占比:
默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3
修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5
Eden空间和另外两个Survivor空间占比分别为8:1:1 -XX:SurvivorRatio=8
对象分配的过程:
首先对象会被防战eden区, 当eden区满时, 触发Minor GC, 这个时候存活下来的幸存者对象将被移动到from区,年龄加一 再次触发Minor GC时, eden区和from都会进行垃圾回收,这时候所有的幸存者将移动到to区, 一直这样来回, 直到年龄达到15移动到老年代。 其实这就是复制算法的体现, 在这个朝生夕死的年轻代, 复制存活的成本大大降低, 并且8:1:1 , 仅仅浪费了2的内存比列, 带来了很多的好处。
方法区1. 线程共享的区域, 它用于存储虚拟机加载的类型信息, 常量, 静态变量, 即时编译器编译后的代码缓存等数据。是元空间,永久代的落地实现
2. 在JDK8之前, 我们称之为永久代, 但是两者之间并不是等价的,只是在HotSpot设计团队, 把使用永久代来实现方法区而已, 这样就可以像管理堆一样管理方法区。 但BEA, JRockit, IBM J9等来说, 是不存在永久代概念的。 最后的实现, 把字符串常量池,静态变量, 类型信息等全部移到元空间(本地内存)
区别:
a. 永久代存在内存溢出的问题, 永久代有上限, 即是不设置,也有默认大小, 而J9的方法区, 只要没有触碰到进程可用内存的上限, 就不会有问题。
b. 有少数方法存在差异,(String :: inner()) 会应为永久代的原因在不同的虚拟机上有不同的表现。
3. 类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。
类型信息(域信息、方法信息),运行时常量池
3. 运行时常量池:
1. 字节码文件中含有内部常量池, 方法区内部含有运行时常量池,
常量池: 存放编译期间生成的各种字面量和符号引用
运行时: 常量池表在运行时的表现形式
编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过 ClassLoader 将 字节码文件的常量池 中的信息加载到内存中,存储在了方法区的运行时常量池 中。 通俗来说就是, 字节码中的常量信息,要被使用必须先加载到运行时常量池运行时常量池具备一个重要的特征, 动态性, Java语言不要求常量一定只要编译期产生, 就是并非只要预置入Class文件中的常量池内容才能进入运行时常量池, 运行期间也可以将新的常量放入其中, 比如: String的 inner()
直接内存
在加入NIO类后, 引入了通道Channel, 以及缓冲区Buffer的方式, 它可以使用Native函数库直接分配堆外内存, 然后通过存储在Java堆的DirectByteBuffer对象作为这块内存的应用进行 *** 作。 这样能在一些场景中显著提高性能, 因为避免了Java推和Native堆的来回切换
对象的创建 (普通对象)
当Java虚拟机需要一条new指令时, 首先回去检查这个指令是否被加载,解析, 初始化, 否则先完成类加载, 在加载后确定对象大小, 分配堆内存。
分配内存的方式:
1. 指针碰撞: 指堆中内存规整, 只需要中间放一个指针,使用过的内存放一边, 没使用的放一边, 那么分配内存只需要指针从使用这边到空闲那边移动指定的对象大小即可。
2. 空闲列表, 指空闲和使用过的内存交错在一起, 那么就需要维护一个列表, 记录那个内存块是可用使用的 .
那么Java堆是否规整, 又是由垃圾收集器是否带有空间整理的能力决定。
即Serial, ParNew ==> 指针碰撞 CMS ==> 空闲列表
如果对象创建的非常频繁, 线程是不安全的, 可能出正在A对象分配内存, 指针还没来及修改, 对象B右同时使用了原来的指针处理。
解决方法: 1. 采用CAS配上失败重试的方式, 保证 *** 作原子性; 2. 另外一个就是把内存分配的动作按照线程划分在不同的空间之中进行, 即每个线程在Java堆中预先分配一个本地线程缓冲区(TLAB), 内存分配完成之后, 必须把分配道德内存空间初始化为0, 这部 *** 作保证对象实例不被赋初始值就可以执行。
到此一个对象就产生了, 但是还没有进行
概念:
1. 由类加载子系统从文件系统中或者去他地方加载class文件
2. 把加载的class信息存放在方法区
3. classLoader只负责class文件的加载,至于是否可运行由Execution Engine决定;
4. 调用构造函数, 实例对象将存储在堆中
类加载的ClassLoader角色:
1. class文件相当于一份模板, 由jvm构造出多个实例, 被称为DNA模板
2. class文件-->jvm-->DNA模板, 需要classLoader进行传输
字节码在变成一个Class对象模板前,会进行7个阶段:加载, 验证, 准备, 解析, 初始化, 使用, 卸载。 (加载、验证、准备、初始化、卸载这5个阶段的顺序是确定)
加载:
虚拟机启动加载: 会加载非常常用的一些包, util, io......
运行时加载: 如果内存中没用, 会按照全限定名去加载class文件
1. 获取class文件的二进制流 (可以从任何地方获取)
2. 将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
3. 生成一个Class对象模板,作为方法区这个类的各种数据的访问入口。
连接:
1. 验证: 文件格式验证,元数据验证, 字节码验证, 符号引用验证
2. 准备, 正式为类变量分配内存及初始化阶段, 这些变量都将在方法区分配。注意: 类变量指static修饰的变量, 不是类实例变量。 final修饰的变量:eg: static int value = 123 这个时候准备阶段是0, 初始化后才是123, 而final ...... 这个时候已经是123了。
3. 解析, 是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用: 是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。 符号引用的三类常量: 类和接口的全限定名, 字段的名称和描述符, 方法的名称和描述符 通过javap可以反编译class文件:不管是类名还是字段名都会由#~ = UTF8 对应着具体的描述 直接引用: 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用 和虚拟机实现的 内存布局相关的, 如果有了直接引用,那引用的目标必定已经存在在内存中了 解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段都做了哪些工作呢?大 体可以分为: 类或接口的解析, 类方法的解析, 接口方法解析, 字段解析 初始化: 是类加载的最后一步,在之前几个步骤中, 除了用户自定义加载器可局部参加外, 其余都是由JVM控制, 只要初始化阶段, jvm才会区调用编写的java程序代码, 将主导权交给应用程序 1. 初始化执行()构造方法, 并不是程序员直接写的(), 它是javac自动生成物, () 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块( static{} 块) 中的 语句合并产 生的, 编译集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问 到定义在静态语句块之 前的变量, 定义在它之后的变量, 在前面的静态语句块可以 值, 但是不能访问。public class Main {
static {
i = 0 ; // 可以编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
2. Java 虚拟机会保证在子类的()方法执行前, 父类的()方法已经执行 完毕。 因此在Java虚拟机一个被执行的()方法的 类型肯定是java.lang.Object。
3.
static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。并且只会执行一次, 对象实例化需要先调用父类构造,在执行自己的构造。
类加载器的作用:
类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建 一个java.lang.Class 对象,用来封装类在方法区内的数据结构。分类: 引导类加载器(c/c++实现) BootStrapClassLoader 和 自定义加载器(派生于classLoader)(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
启动类加载器: 加载核心类库, 由jvm提供自身需要的类, 不继承classLoader
Extension Class Loader: 扩展类加载器: 从java.ext.dirs系统属性所指定的目录中加载类库, 或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。派生于classLoader, 父类是:BootStrapClassLoader
System Class Loader: 系统加载器: 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库 父类是: 扩展类加载器
用户自定义加载器: 继承classLoader, 实现findClass, 由defineClass(name, b, off, len, null)传入字节码二进制流的字节数组即可构建class对象
双亲委派机制:就是不管是那个类, 都需先委派给父类,看有没有, 如果父类存在,则由父类完成。
避免了有人恶意写一个与核心类库一样的包名和类名加载入内存中后, 存在病毒代码, 产生危害。 而有了双亲委派, 永远都不会执行到这份恶意的代码到内存。
如何实现双亲委派:
ClassLoaderh中有loadClass(String name, boolean resolve) 函数:
1. 首先判断是否被加载:findLoadedClass(name), 被加载返回
2. 判断是否有父类加载器, 有调用父类的loadClass, 否则调用自己的findClass
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)