java虚拟机 jvm
主要介绍jvm运行时, 内存都用在哪些地方了
堆
java所有对象在堆分配内存. 堆的大小可以动态扩容.可以通过 -Xms 和 -Xmx这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
注意类的信息存储在方法区, 但是根据类的信息创建的class对象是在堆里面的
垃圾回收机制gc主要就是针对堆
栈
栈区域属于线程独有的, 不同的线程拥有不同的栈区域, 栈区域主要包括程序计数器, 虚拟机栈, 本地方法栈三个部分
程序计数器
记录当前线程执行到的虚拟机字节码的指令地址(也就是代码执行到哪一行了)
虚拟机栈
可以通过 -Xss 这个虚拟机参数来指定每个线程占据的内存大小, 栈所使用的内存属于堆外内存, 每次创建线程都需要跟操作系统申请内存
虚拟机栈存储的是栈帧, 每执行一个方法, 就会添加一个栈帧. 从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
这里的栈就是栈结构, 先进后出的, 方法的递归就对应了栈先进后出的特点, 执行A方法, A入栈, A调用B方法, B入栈, B执行完毕, B出栈, 然后A执行完毕, A出栈.
栈帧
栈帧包含了一个方法执行所需要的所有元素, 存储了局部变量表、操作数栈、常量池引用等信息
本地方法栈
本地方法栈结构和功能与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
方法区(jdk 1.8之前叫永久代)
(1) jdk1.8之前是单独的一块内存区域,和堆内存的内存区域相连, 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
(2)通过-XX:MaxPermSize来设定永久代最大可分配空间,当JVM加载的类信息容量超过了这个值,会报OOM:PermGen错误
(3) 永久代的GC是和老年代(old generation)捆绑在一起的,无论谁满了,都会触发永久代和老年代的垃圾收集。
(4) 在JDK1.8之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息(也就class常量池里的符号引用),静态变量和常量池等放入堆中。
反射对于方法区的影响
方法区主要存储类加载后的结构信息, 因为jvm对于类结构的加载并不是一次性全部加载的, 而反射在使用的时候类如果没有加载, 会触发类的加载, 相当于会使得更多类的信息存在方法区, 可能会导致内存溢出(虽然也只是加载了项目中的类, 但是相当于在不合适的时机加载了)
动态代理对于方法区的影响
(1) 动态代理会大量用到反射, 因此和反射带来的影响一致.
(2) 动态代理会创建新的类结构. 加载更多的类信息
各种常量池
常量
用final修饰的成员变量表示常量,值一旦给定就无法改变!
final修饰的变量有三种:静态变量、实例变量和局部变量(定义在方法中的),分别表示三种类型的常量。
class常量池(静态常量池)
class常量池就是java文件编译成class文件后文件的一部分.
java文件被编译成 class文件,class文件中除了包含类的版本还有一项就是常量池(Constant Pool),用于存放编译器生成的各种字面量( Literal )和 符号引用(Symbolic References)。
字面量就是: 文本字符串, 常量值, 基本属性类型值等.
符号引用: 类结构, 属性类型和名, 方法名和相关信息
运行时常量池(通常所说的常量池)
而当类加载到内存中后,jvm就会将 class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池包含每一个类的信息。
jdk 1.8之后, class常量池中的符号引用存入到元空间, 字面量存入堆空间里
字符串常量池
字符串常量池可以认为属于运行时常量池, 它里面的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到字符串常量池中. (也就是字符串常量池是在类加载完成后填充的.)
可以手动添加字符串常量 利用String.intern()方法
整型常量池, 浮点型常量池
jvm自己创建的常量池, 无法手动添加常量.
元空间
存储到就是class常量池中的符号引用的部分.
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)
本地内存
本地内存(Native memory),也称为C-Heap,是供JVM自身进程使用的, 本地内存不属于堆内存, 但是这部分内存是jvm能够访问到的。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC。
元空间为啥替换永久代
主要是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。
当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。
元空间何时发生GC
如果元空间设置了MaxMetaspaceSize参数, 那么在达到这个值后会触发关于类加载器和无用类信息的回收.
对象的创建过程
new 一个对象的时候
(1) 首先判断类是否被加载过, 如果加载过了, 直接通过堆里的class对象创建实例. 如果没有加载过, 触发类的加载, 类信息被加载到方法区后,就会产生一个全局唯一的class对象存在堆里.所有的实例都是通过class对象创建的(利用class对象的构造函数)
(2) 在堆中分配内存空间(分配方式为指针碰撞或者空间列表)
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump thePointer)。
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(FreeList)。
(3) 设置对象头,例如这个对象是哪个类的实例(即所属类)、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中。
(4) 对象属性初始化, 调用init()方法, 也就是根据构造函数初始化
(5) 将对象的引用指向该对象
类的加载过程
类加载器
四种引用类型
垃圾回收机制
栈的生命周期和线程一致, 因此栈空间无需回收, 而作为内存主要消耗者的堆内存, 需要通过一种机制来使得无用的内存得到回收, jvm代替程序员实现了堆的垃圾回收机制, 因此程序员无需主动的去操心这部分.
如何发现无用的对象?
采用引用计数法的jvm虚拟机:
在对象创建之初, 会为对象赋予一个引用计数器(可以放在对象头中), 当引用计数为0的时候代表该对象是无用的对象.
如果两个对象互相引用, 那么这两个对象永远不会被回收, 造成内存泄漏.
采用可达性分析法的jvm虚拟机:
可达性分析法认为只有从所有的GC roots出发, 一层层寻找引用, 然后能够到达的对象才是有意义的, 除此之外的对象都可以被回收.
GC Roots
那么有哪些对象可以认为是GC Roots, jvm就会以这些对象作为图的起点进行寻找.
- (1) 虚拟机栈中引用的对象(本地变量表, 即线程正在使用的对象
- (2) 方法区中静态属性引用的对象, 即类的静态变量
- (3) 方法区中常量引用的对象 (在运行时常量池中)
- (4) 本地方法栈中引用的对象(Native对象)
垃圾回收算法
不同的jvm虚拟机, 会选择不同的垃圾回收算法
标记-回收
标记阶段:标记出需要被回收的对象。
清除阶段:回收被标记的可回收对象的内部空间。b
缺点:
- (1) 算法过程需要暂停整个应用,效率不高。
- (2) 标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
复制算法
复制算法将可用内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。
缺点: 内存空间缩减为原来的一半;算法的效率和存活对象的数目有关,存活对象越多,效率越低。
标记-整理
综合标记-回收和复制算法, 将存活了的对象移动到一边, 然后将另一边删除
分代收集算法
分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。
核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation.
对象如何记录年龄
既然分了年纪, 那么就代表每个对象都有年龄, 如果jvm采用分代收集算法, 在创建对象实例的时候, 就会在头像头中添加一个年龄信息
对象如何增长年龄
该对象实例每经历过一次GC, 年龄就 +1.
分代的作用是啥?
老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
年轻代(使用标记清除 + 复制算法)
标记清除算法: 是对年代代整体GC的时候采用的, 因此会有内存碎片
复制算法: 是eden区, survivor区, 老年代的复制移动体现的, 充分消除了内存碎片.
- 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区.
- 8 : 1 : 1
- eden : survivor0 : survivor1
(1) 所有新产生的对象都在eden区.
(2) 每次GC后: eden中存活的对象移动到survivor0, 然后清空eden. 这样eden永远在gc后为空, 新对象放进来时不会有内存碎片
(3) 当某一次GC后, 如果将eden放入survivor0的时候, 发现放不下了, 则将eden和survivor0所有存活对象一起移动到survivor1中().
当survivor1能放下的时候: (虽然survivor0和survivor1内存大小一致)
survivor0在每次GC的时候, 也会有对象被回收, 也就存在内存碎片, 此时将两部分内容移动的时候, 是有可能放入survivor1的. 然后清空eden和survivor0, 再将survivor1放回survivor0(相当于一次内存碎片的去除了)
当survivor1不能放下的时候:直接移动到老年代.
年轻代GC触发 MinorGC
当eden区放不下的时候, 就会触发一次年轻代的GC
老年代(使用标记-清除或者标记-整理算法)
进入老年代的方式有三种:
(1) 足够老的情况:
当对象的年龄达到一定阈值的时候, 对象就会进入年老代, 对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
(2) 足够大的情况:
如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个 -XX:PretenureSizeThreadhold参数,令大于这个参数值的对象直接在老年代中分配
(3) 年轻代Survivor1区满了的时候
会将年轻代中eden区和survivor1区的当次GC后的所有存活对象, 一次性的移动到老年代
老年代触发GC的时刻 Full GC
老年代满的时候触发一次Full GC
反射
本质就是java允许你在得到class对象后, 能通过获取属性的方式, 得到类的一切相关信息. 因此 一定要得到它的class对象.
反射指的就是:
- (1) 在jvm运行状态下, 对于任何一个类都可以得到它的属性, 方法, 构造函数
- (2) 在jvm运行状态下, 对于任意一个实例都能调用它的属性和方法
很重要的一点因为每个实例子都持有class对象的引用, 因此可以通过实例得到class对象.
如何获得class对象
(1) 由实例得到class对象
基类Object有getClass方法, 可以得到该实例的class对象
(2) 任何数据类型(包括基本数据类型)都有一个“静态”的class属性
例如Class aa = Integer.Class, Class bb = String.Calss 就直接就是Class对象. 属于硬编码
(3) 由类名得到class对象 Class.forName(String name);
可以直接通过包名 + 类名直接得到class对象. 通常可以扫描某个包下面的所有类, 来得到所有类名
在反射调用方法的时候, 为啥要传入一个目标对象?
java中, 除了静态方法外, 其它的方法都是实例执行的, 反射调用方法的时候method.invoke(obj, args), 一定要传入一个obj对象, 该对象就是你期望执行该方法的对象(可以是本身你得到的对象, 也可以是你新建的对象.)
动态代理
动态代理的目的, 就是为了不用提前为目标类进行编码而创建新的类.
静态代理
只能代理目标类所实现接口的方法, 私有方法无法代理.
前提: 目标类实现了某个接口
- (1) 创建一个代理类, 实现该接口
- (2) 为代理类增添一个构造函数, 参数为目标类
- (3) 编写代理类的期望代理的方法, 可以利用目标类的引用, 来控制目标类方法的执行
- (4) 在本该使用目标类的地方, 利用目标类构建代理类, 然后创建实例来使用
jdk动态代理
要求目标类必须实现某个接口, 同时能代理的也是接口中的方法.
(1) 利用Proxy类的getProxyClass(ClassLoader, interfaces)方法 ,只要你给它传入类加载器和一组接口,它就给你返回代理Class对象。 代理class对象拥有接口中的所有方法和参数列表. 这是通过反射实现的.
(2) 利用代理class对象, 创建一个有参构造器, 得到一个Constructor的实例.参数为InvocationHandler.class
(3) 创建一个InvocationHandler.class的实例, 实现invoke()方法, 通过该方法可以对所有方法, 或者是指定方法进行代理增强. 注意invoke()方法中, 需要通过反射来执行原对象的方法, 因此原对象也必须传入.
(4) 然后利用constructor.newInstance(invocationHandler)方法, 得到代理对象的实例.
综上所述, 创建一个jdk动态代理, 需要三个内容, 1. 接口的类加载器 2. 接口类 3. InvocationHandler类
public class ProxyTest {
public static void main(String[] args) throws Throwable {
CalculatorImpl target = new CalculatorImpl();
//传入目标对象
//目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
Calculator calculatorProxy = (Calculator) getProxy(target);
calculatorProxy.add(1, 2);
calculatorProxy.subtract(2, 1);
}
private static Object getProxy(final Object target) throws Exception {
//参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
Object proxy = constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
});
return proxy;
}
}
动态代理类是一个全新的类吗
没错, 相比于被代理的类, 动态代理类多了一个InvocationHandler的属性, 正是通过这个属性实现的代理功能. 因此是一个全新的类结构了.
jdk动态代理第一步为什么要用接口去生成代理class对象, 而不能用目标class对象本身?
(1) 因为如果用接口去生成, 那么同一组接口在调用getProxyClass()方法时, 因为带有缓存, 创建过的class对象不需要重新创建了, 因此虽然每次得到动态代理的实例的时候都需要传入目标类, 但是构造的代理类的时候, 其实没有再重复创建代理类的class对象, 而只是新建了代理类对象的实例.
(2) 在判断某个实例是否需要通过代理使用的时候, 接口也方便判断.
CgLib动态代理
这种方式的动态代理不需要代理类实现某个接口, 而是通过创建代理类的子类方式
- (1) 创建代理类的子类, 目的是能够通过子类调用父类的原始方法
- (2) 利用asm技术, 为子类织入增强方法, 生成代理类的二进制字节码.
- (3) 加载二进制字节码,生成Class对象
- (4) 利用class对象得到构造函数, 然后创建一个代理类的实例返回
增强方法的写法:
public class MyMethodInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] arg, MethodProxy proxy) throws Throwable {
System.out.println("Before:" + method);
Object object = proxy.invokeSuper(obj, arg);
System.out.println("After:" + method);
return object;
}
}
intercept方法的参数分别为1、代理对象;2、委托类方法;3、方法参数;4、代理方法的MethodProxy对象。
创建代理对象的写法:
Enhancer enhancer = new Enhancer();
// 继承目标类
enhancer.setSuperclass(UserServiceImpl.class);
// 织入增强方法
enhancer.setCallback(new MyMethodInterceptor());
UserServiceImpl userService = (UserServiceImpl)enhancer.create();
cglib asm织入了什么?
就是将intercept方法织入到代理类中, 这样在使用代理类的代理方法时, 相应的增强逻辑也会被调用, 并且intercept方法可以编写如何增强, 并且原方法如何被调用. 也就是代理类中对应的代理方法, 已经被改变了(这个jdk动态代理对方法做出的改变结果一致).
cglib需要原对象吗
jdk 动态代理, 在调用目标类的原方法时, 使用的是传统的反射, 需要通过原对象来调用. 而cglib动态代理, 是通过代理对象调用父类对象的方法实现, 因此不需要原对象
fastClass机制
就是说正常我们通过实例调用方法都是a.add()这么调用的.
但是反射就是先获取方法列表, 找到里面和add名称一致的method对象, 然后再method.invoke(obj, args)使用该方法, 这个过程比较复杂.
fastClass呢就是根据你的方法名 + 入参, 直接返回给你a.add()的调用, 而不需要通过method.invoke(obj, args)的方式了, 也就是类似下面的写法, 直接根据index进行方法的调用. 就相当于你直接调用方法一样.
switch (index) {
case 1:
a.add();
return null;
case 2:
a.delete();
return null;
}