校招准备:(二):java高级知识(反射,动态代理,java和jvm内存模型,垃圾回收,类加载)

目录

3.1深入理解Java类型信息(Class对象)与反射机制

3.2、java的反射机制

3.3代理模式,静态代理,动态代理

3.3java注解的原理(拓展)

3.4java动态代理和cglib动态代理区别,Spring aop与aspectJ的区别:

4.jmm(java memory model)内存模型与垃圾回收:

4.1java四种引用:

4.2深入理解JVM(一)--Java 内存区域

4.3判断对象是否存活 

4.4垃圾回收算法 :

4.5GC是什么时候触发的

4.6知道哪些垃圾收集器?

4.7静态对象要成为垃圾被回收,要满足三个条件: 

4.8几种常量池:

5类加载:

5.1java类加载的方式分为两种:

5.2类初始化的时机

5.3类的加载机制:

5.2classloader讲解:

5.3关于静态内部类的问题。

6.Java的解释执行


3.1深入理解Java类型信息(Class对象)与反射机制

RTTI(Run-Time Type Identification)运行时类型识别,其作用是在运行时识别一个对象的类型和类的信息,这里分两种:传统的”RRTI”,它假定我们在编译期已知道了所有类型(在没有反射机制创建和使用类对象时,一般都是编译期已确定其类型,如new对象时该类必须已定义好),另外一种是反射机制,它允许我们在运行时发现和使用类型的信息。在Java中用来表示运行时类型信息的对应类就是Class类。

Java中每个类都有一个Class对象,每当我们编写并且编译一个新创建的类就会产生一个对应Class对象并且这个Class对象会被保存在同名.class文件里(编译后的字节码文件保存的就是Class对象)。jJVM将类加载后,就会根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。

  • Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载

  • Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要(关于反射稍后分析)。

获取class对象三种方式:
class.forname(),类名.class,对象.getclass()

在执行class.forname("classname")时,JVM会在classapth中去找对应的类并加载,这时JVM会执行该类的静态代码段。

//字面常量的方式获取Class对象
Class clazz = Gum.class;
通过字面量的方法获取Class对象的引用不会自动初始化该类,触发的应该是加载阶段个class.forname相同。更加有趣的是字面常量的获取Class对象引用方式不仅可以应用于普通的类,也可以应用用接口,数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助,

在Java SE5引入泛型后,使用我们可以利用泛型来表示Class对象更具体的类型,比如Class,即使在运行期间会被擦除,但编译期足以确保我们使用正确的对象类型。

强制转换,这得归功于RRTI,要知道在Java中,所有类型转换都是在运行时进行正确性检查的,利用RRTI进行判断类型是否正确从而确保强制转换的完成,如果类型转换失败,将会抛出类型转换异常。
除了强制转换外,在Java SE5中新增一种使用Class对象进行类型转换的方式

Animal animal= new Dog();
//这两句等同于Dog dog = (Dog) animal;
Class dogType = Dog.class;
Dog dog = dogType.cast(animal)

关于instanceof 关键字,它返回一个boolean类型的值,这样可以避免抛出类型转换的异常。

而isInstance方法则是Class类中的一个Native方法,也是用于判断对象类型的

3.2、java的反射机制

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。一直以来反射技术都是Java中的闪亮点,这也是目前大部分框架(如Spring/Mybatis等)得以实现的支柱。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。

在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。
在反射包中,我们常用的类主要有:
Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、
Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、
Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private),

java反射机制主要提供了以下功能:
在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具有的成员变量和方法;
在运行时调用任意一个对象的方法;生成动态代理

通过反射机制可以实现动态代理,基于此spring框架实现面相切面aop和拦截器以及mabtis框架。
反射可以实现动态编译,体现多态,降低类的藕合性。
缺点是对性能有影响。

newInstance和new创建对象的区别
在使用newInstance()方法的时候,必须保证这个类已经加载并且已经连接了,而这可以通过Class的静态方法forName()来完成的。
new关键字能调用任何构造方法。
newInstance()只能调用无参构造方法。
newInstance: 弱类型(GC是回收对象的限制条件很低,容易被回收)、低效率、只能调用无参构造,new 强类型(GC不会自动回收,只有所有的指向对象的引用被移除是才会被回收,若对象生命周期已经结束,但引用没有被移除,经常会出现内存溢出)

3.3代理模式,静态代理,动态代理

轻松学,Java 中的代理模式及动态代理

静态代理代码见上面给的链接。

动态代理:

public interface SellWine {
	 void mainJiu();

}

public class MaotaiJiu implements SellWine {
	@Override
	public void mainJiu() {
		// TODO Auto-generated method stub
		System.out.println("我卖得是茅台酒。");
	}
}

public class Wuliangye implements SellWine {
	@Override
	public void mainJiu() {
		// TODO Auto-generated method stub
		System.out.println("我卖得是五粮液。");
	}
}

InvocationHandler:
InvocationHandler 是一个接口,官方文档解释说,每个代理的实例都有一个与之关联的 InvocationHandler 实现类,如果代理的方法被调用,那么代理便会通知和转发给内部的 InvocationHandler 实现类,由它决定处理。
InvocationHandler 内部只是一个 invoke() 方法,正是这个方法决定了怎么样处理代理传递过来的方法调用。

  • proxy 代理对象
  • method 代理对象调用的方法
  • args 调用的方法中的参数
public class GuitaiA implements InvocationHandler {
	
	private Object pingpai;
	
	public GuitaiA(Object pingpai) {
		this.pingpai = pingpai;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		// TODO Auto-generated method stub
		System.out.println("销售开始  柜台是: "+this.getClass().getSimpleName());
		method.invoke(pingpai, args);
		System.out.println("销售结束");
		return null;
	}
}
public class Test {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MaotaiJiu maotaijiu = new MaotaiJiu();
	
		Wuliangye wu = new Wuliangye();
		
		InvocationHandler jingxiao1 = new GuitaiA(maotaijiu);
		InvocationHandler jingxiao2 = new GuitaiA(wu);
		
		SellWine dynamicProxy = (SellWine) Proxy.newProxyInstance(MaotaiJiu.class.getClassLoader(),
				MaotaiJiu.class.getInterfaces(), jingxiao1);
		SellWine dynamicProxy1 = (SellWine) Proxy.newProxyInstance(MaotaiJiu.class.getClassLoader(),
				MaotaiJiu.class.getInterfaces(), jingxiao2);
		
		dynamicProxy.mainJiu();
		
		dynamicProxy1.mainJiu();
	}
}

 Proxy 的静态方法 newProxyInstance 才会动态创建代理。

public static Object newProxyInstance(ClassLoader loader,Class[] interfaces, InvocationHandler h)

  • loader 自然是类加载器
  • interfaces 代码要用来代理的接口
  • h 一个 InvocationHandler 对象

结果:

销售开始  柜台是: GuitaiA
我卖得是茅台酒。
销售结束
销售开始  柜台是: GuitaiA
我卖得是五粮液。
销售结束

原理:
校招准备:(二):java高级知识(反射,动态代理,java和jvm内存模型,垃圾回收,类加载)_第1张图片

红框中 $Proxy0就是通过 Proxy 动态生成的。
$Proxy0实现了要代理的接口。
$Proxy0通过调用 InvocationHandler来执行任务。

动态代理的应用:

还是在不修改被代理对象的源码上,进行功能的增强。

 AOP 面向切面编程:日志记录,性能统计,安全控制,事务处理,异常处理等等。

3.3java注解的原理(拓展)

我就不摘内容了,直接看这两篇文章就够了:

秒懂,Java 注解 (Annotation)你可以这样学

深入理解Java注解类型(@Annotation)

注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。

我之前看到的注解在javaweb应用:https://github.com/JinBinPeng/springboot-jwt

功能:通过注解来判断是否对接口方法进行拦截验证。

首先定义两个注解,一个需要验证,一个不需要验证的注解。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
    boolean required() default true;
}

然后将注解放到controller中的方法上

@UserLoginToken
    @GetMapping("/getMessage")
    public String getMessage(){
        return "你已通过验证";
    }

然后在拦截器中,通过method.isAnnotationPresent()来判断注解,进行逻辑处理。

 public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        //检查有没有需要用户权限的注解
        if (method.isAnnotationPresent(UserLoginToken.class)) {
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if (userLoginToken.required()) {
                // 执行认证
                if (token == null) {
                    throw new RuntimeException("无token,请重新登录");
                }
                ******
                return true;
            }
        }
        return true;
    }

 

3.4java动态代理和cglib动态代理区别,Spring aop与aspectJ的区别:

JDK动态代理类实现了InvocationHandler接口,重写的invoke方法。基础是反射机制(method.invoke(对象,参数))Proxy.newProxyInstance()
cglib动态代理:
原理是对指定的目标生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
注意:jdk的动态代理只可以为接口去完成操作,而cglib它可以为没有实现接口的类去做代理,也可以为实现接口的类去做代理。

1. JDK 动态代理用于对接口的代理,动态产生一个实现指定接口的类,注意动态代理有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理,只能为接口创建动态代理实例,而不能对类创建动态代理。
2. CGLIB 用于对类的代理,把被代理对象类的 class 文件加载进来,修改其字节码生成一个继承了被代理类的子类。注意,修改了字节码,所以需要依赖 ASM 包,使用 cglib 就是为了弥补动态代理的不足。

1.JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象。
2.JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。
3.JDK调用代理方法,是通过反射机制调用,Cglib是通过FastClass机制直接调用方法,Cglib执行效率更高。(Cglib动态代理实现原理)

Spring aop与aspectJ的区别:
首先说说静态织入:作用于编译期,字节码加载前,性能优于动态织入。AspectJ 用一种特定语言编写切面,通过自己的语法编译工具 ajc 编译器来编译,生成一个新的代理类,该代理类增强了业务类。

第二说下动态织入:作用于运行期,字节码加载后,使用了反射,所以性能较差。Spring 底层的动态代理分为 Cglib 和 JDK 代理,为什么要分两种?

AspectJ 属于静态织入,原理是静态代理
AspectJ会通过生成新的AOP代理类来对目标类进行增强,有兴趣的同学可以去查看经过ajc编译前后的代码,比照一下就会发现,假设我们要切入一个方法,那么AspectJ会重构一个新的方法,并且将原来的方法替代为这个新的方法,这个新的方法就会根据配置在调用目标方法的前后等指定位置插入特定代码,这样系统在调用目标方法的时候,其实是调用的被AspectJ增强后的代理方法,而这个代理类会在编译结束时生成好,所以属于静态织入的方式。
AspectJ实际上是对AOP编程思想的一个实践。AspectJ提供了一套全新的语法实现,完全兼容Java(其实跟Java之间的区别,只是多了一些关键词而已)。同时,还提供了纯Java语言的实现,通过注解的方式,完成代码编织的功能。因此我们在使用AspectJ的时候有以下两种方式:
使用AspectJ的语言进行开发
通过AspectJ提供的注解在Java语言上开发
因为最终的目的其实都是需要在字节码文件中织入我们自己定义的切面代码,不管使用哪种方式接入AspectJ,都需要使用AspectJ提供的代码编译工具ajc进行编译

Aspectj与Spring AOP比较

Spring AOP AspectJ
在纯 Java 中实现 使用 Java 编程语言的扩展实现
不需要单独的编译过程 除非设置 LTW,否则需要 AspectJ 编译器 (ajc)
只能使用运行时织入 运行时织入不可用。支持编译时、编译后和加载时织入
功能不强-仅支持方法级编织 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等......。
只能在由 Spring 容器管理的 bean 上实现 可以在所有域对象上实现
仅支持方法执行切入点 支持所有切入点
代理是由目标对象创建的, 并且切面应用在这些代理上 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入
比 AspectJ 慢多了 更好的性能
易于学习和应用 相对于 Spring AOP 来说更复杂

4.jmm(java memory model)内存模型与垃圾回收:

4.0jmm(java memory model)内存模型

java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。

Java内存模型的主要目标是定义程序中变量的访问规则。即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。这里的变量是指实例字段,静态字段,构成数组对象的元素,但是不包括局部变量和方法参数(因为这是线程私有的)。这里可以简单的认为主内存是java虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。但是在堆中的变量如果在多线程中都使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了。

Java内存模型中涉及到的概念有:

  • 主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
  • 工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。

这里需要说明一下:主内存、工作内存与java内存区域中的java堆、虚拟机栈、方法区并不是一个层次的内存划分。这两者是基本上是没有关系的,上文只是为了便于理解,做的类比

校招准备:(二):java高级知识(反射,动态代理,java和jvm内存模型,垃圾回收,类加载)_第2张图片

物理机高速缓存和主内存之间的交互有协议,同样的,java内存中线程的工作内存和主内存的交互是由java虚拟机定义了如下的8种操作来完成的。

一共定义了八种操作,还有许多操作限制。以及happen-before原则和volatile关键字等。具体的看原文。

4.1java四种引用:

java强、弱、软、虚四种引用:
指向通过new得到的内存空间的引用称为强引用,

软引用堆空间不够时会回收SoftReference<**>***,适用于缓存比如博客文章:HashMap

弱引用在垃圾回收时一定回收WeakReference<>,也可以通过WeakHashMap来使用,使用场景:优惠券Coupan,List保存优惠券强引用,List保存用户的强引用,WeakHashMap>>优惠券对应的用户。这样优惠券失效或者用户失效时直接从list删去,对应的weakhashmap也会更新。

虚引用必须和引用队列ReferencQueue一起用,无法通过虚引用得到指向的值。用处为:从refQueue队列中取出虚引用,执行析构动作。例子:

ReferenceQueue refQueue=new ReferenceQueue<>();
PhantomReference ohantom=new PhantomReferece<>(obj,refQueue);
Object obj=refQueue.poll();
if(obj!=null){
    Field referenceVal=Reference.class.getDeclaredField("referent");
    referenceVal.setAccessible(true);
    referenceVal.get(obj);
}

可以在销毁对象前销毁其中的敏感信息。
和finalize很相似,但finalize是在对象被回收前执行,而虚引用在对象回收后,如果不做任何动作,引用队列中存放的对象的内存是无法回收的。

4.2深入理解JVM(一)--Java 内存区域

校招准备:(二):java高级知识(反射,动态代理,java和jvm内存模型,垃圾回收,类加载)_第3张图片

   1. 程序计数器

    1)程序计数器(Program CounterRegister) 是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器. 在虚拟机的概念模型里, 字节码解释器工作时就是通过改变这个计数器的值来选去吓一跳需要执行的字节码指令, 分支, 循环, 跳转, 异常处理, 线程恢复等基础功能都需要依赖这个计数器来完成.

    2)由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的, 在任何一个确定的时刻, 一个处理器(对于多核处理器来说是一个内核) 只会执行一条线程中的指令. 因此, 为了线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器, 各条线程之间的计数器互不影响, 独立存储, 我们称这类内存区域为"线程私有内存".

    2)如果线程正在执行的是一个Java方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是Native方法, 这个计数器值则为空(Undefined). 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.

2. Java虚拟机栈

    1)与程序计数器一样, Java虚拟机栈(Java Virtual Machine Stacks) 也是线程私有的, 它的生命周期与线程相同. 虚拟机栈描述的是Java方法执行的内存模型: 每个方法被执行的时候都会同时创建一个栈帧(Stack Frame) 用于存储局部变量表, 操作栈, 动态链接, 方法出口等信息. 每一个方法被调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程.

    2)局部变量表存放了编译期可知的各种基本数据类型(Boolean, byte , char, short, int, float , long , double), 对象引用(reference类型, 它不等同于对象本身, 根据不同的虚拟机实现, 他可能是一个指向对象起始地址的引用指针, 也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址).

    3)其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot), 其余的数据类型只占用一个, 局部变量表所需的内存空间在编译期间完成分配, 当进入一个方法时, 这个方法需要在帧中分配多大的局部变量空间是完全确定的, 在方法运行期间不会改变局部变量表的大小.

  •   如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出StackOverflowError异常; 
  •   如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可动态拓展, 只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当拓展时无法申请到足够的内存时会抛出OutOfMemoryEoor异常.

3.本地方法栈

    1)本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务, 而本地方法栈则是为虚拟机使用到Native方法服务. 虚拟机规范中对本地方法栈中的方法使用的语言, 使用方式与数据结构并没有强制规定, 因此具体的虚拟机可以自由实现它. 甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一. 与虚拟机栈一样, 本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常.

4.Java 堆

    1) 对于大多数应用来说, Java堆(Java Heap) 是Java虚拟机所管理的内存中最大的一块. Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建. 此内存区域的唯一目的就是存放对象实例, 几乎所有的对象实例都在这里分配内存. 这一点在Java虚拟机规范中描述的是: 所有的对象实例以及数组都要在堆上分配, 但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟, 栈上分配, 标量替换优化技术将会导致一些微妙的变化发生, 所有的对象都分配在堆上也逐渐变得不是那么"绝对"了.    2) Java 堆是垃圾收集器管理的主要区域, 因此很多时候也被称做"GC堆"(Garbage Collected Heap), 如果从内存回收的角度看, 由于现在收集器基本都是采用的分代收集算法, 所以Java堆中还可以细分为: 新生代和老年代; 在细致一点的有Eden空间, From Survivor空间, To Survivor空间等. 如果从内存分配的角度看, 线程共享的Java对中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB). 不过, 无论如何划分, 都与存放内容无关, 无论哪个区域, 存储的都仍然是对象实例, 进一步划分的目的是为了更好的回收内存, 或者更快的分配内存.。主要用于存放对象为了更好、更快的回收内存。老年代:占 2/3,新生代: 占 1/3,Eden: 占比 8,To: 占比 1,From: 占比 1。

    3) 根据Java虚拟机规范的规定, Java堆上可以处于物理上不连续的内存空间中, 只要逻辑上是连续的即可, 就像我们的磁盘空间一样. 在实现时, 既可以实现成固定大小的, 也可以是可拓展的, 不过当前主流的虚拟机都是按照可拓展来实现的( 通过-Xms 初始化堆, -Xmx 最大堆空间), 如果在堆中没有内存完成实例分配, 并且堆也无法在拓展时, 将会抛出OutOfMemoryError异常.

5. 方法区

    1) 方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码等数据. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却有一个别名叫做Non-Heap非堆, 目的应该是与Java Heap 区分开来.

6.运行时常量池

    1) 运行时常量池(Runtime Constant Pool) 是方法区的一部分. Class文件中除了有类的版本, 字段,方法, 接口等描述信息外, 还有一项信息是常量池(Constant Pool Table), 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后存放到方法区的运行时常量池中. 

    2) 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性, Java语言并不要求常量一定只能在编译期产生, 也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入翅中, 这种特性被开发人员利用的比较多的便是String类的intern() 方法.

Java 8 内存区域详解

JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时在 jdk 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域。

元空间里面保存的就是类的元数据,如方法、字段、类、包的描述信息,这些信息可以用于创建文档、跟踪代码中的依赖性、执行编译时检查

元空间如何提高性能

  1. 永久代里面的常量池都移到堆里面,只保存元数据,从而让 Full GC 不再关心方法区
  2. 元空间使用直接内存,理论上系统内存有多大,元空间就可以有多大,不存在 OOM
  3. 元空间有单独的元空间虚拟机执行内存分配与垃圾回收

类加载器存储的位置就是元空间,每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。

  • 1.7 之前的方法区:存储已被加载的类信息、常量、静态变量、JIT 编译后的代码。包含运行时常量池。
    • 从 JDK 1.7 开始 方法区不含运行时常量池,运行时常量池放到  里面去了
    • 字符串常量池也放到堆里面去了
    • 并且,加载的类信息...等被转移到 堆 里面的运行时常量池中了
  • 1.8 开始不存在永久代了
  • 元空间:使用直接内存,理论上不会出现 OOM 了,当然内存要足。可以使用参数:-XX:MetaspaceSize来指定元数据区的大小,若不规定大小,会耗尽全部机器内存。

7. 直接内存

    1) 直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分, 也不是Java虚拟机规范中定义的内存区域, 但是这部分内存也被频繁地使用, 而且也可能导致OutOfMemoryError异常出现. 显然, 本机直接内存的分配不会受到Java堆大小的限制, 但是, 既然是内存, 则肯定还是会受到本机总内存的大小及处理器寻址空间的限制. 服务器管理员配置虚拟机参数时, 一般会根据实际内存-Xmx等参数信息, 但经常会忽略到直接内存, 使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制), 从而导致动态扩展时出现OutOfMemoryError异常. JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据

方法区(method area)只是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。而永久代Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。(方法区的Class信息,又称为永久代,是否属于Java堆?)

4.3判断对象是否存活 

1.引用计数法:
堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。 
优点:实现简单,判断效率高 
缺点:很难解决对象之间的相互循环引用,所以java语言并没有选用引用计数法管理内存

2.可达性分析算法(根搜索算法) 
从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。 
可以作为root的对象:
虚拟机栈中引用的对象,方法去中静态属性引用的对象,方法区中常量引用的对象,本地方法栈引用中的对象。

4.4垃圾回收算法 :

1、标记-清除算法 
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,此算法一般没有虚拟机采用。 
优点1:解决了循环引用的问题 
优点2:与复制算法相比,不需要对象移动,效率较高,而且还不需要额外的空间 
不足1:每个活跃的对象都要进行扫描,而且要扫描两次,效率较低,收集暂停的时间比较长。 
不足2:产生不连续的内存碎片 

2、复制算法 
将内存分成两块容量大小相等的区域,每次只使用其中一块,当这一块内存用完了,就将所有存活对象复制到另一块内存空间,然后清除前一块内存空间。这样一来就不容易出现内存碎片的问题。 
1、复制的代价较高,所以适合新生代,因为新生代的对象存活率较低,需要复制的对象较少; 
2、需要双倍的内存空间,而且总是有一块内存空闲,浪费空间 

3、标记-整理算法 
思想:在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。 
不会产生内存碎片,但是依旧移动对象的成本。 

4、分代收集算法 
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。 
原文链接:https://blog.csdn.net/weixin_41835916/article/details/81530733

新生代的回收算法 
包含有Enden、form survicor space、to survivor space三个区,绝大多数最新被创建的对象会被分配到这里,大部分对象在创建之后会变得很快不可达。 
① 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。 
② 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。 
③ 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。 
④ 新生代发生的GC也叫做Minor GC,Minor GC发生频率比较高(不一定等Eden区满了才触发)。 
老年代的回收算法 
① 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。 
② 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。 

4.5GC是什么时候触发的

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GC和Full GC。 
Minor GC 
  一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。 
Full GC 
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC: 
a) 年老代(Tenured)被写满; 
b) 持久代(Perm)被写满; 
c) System.gc()被显示调用; 
d) 上一次GC之后Heap的各域分配策略动态变化; 
这时会发生stop the world:在一些特定指令位置设置一些“安全点”,当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程(Stop The World 所以叫STW),暂停后再找到“GC Roots”进行关系的组建,进而执行标记和清除。这些特定的指令位置主要在: 
        1、循环的末尾 
        2、方法临返回前 / 调用方法的call指令后 
        3、可能抛异常的位置

4.6知道哪些垃圾收集器?

Serial收集器(复制算法) 
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。 
Serial Old收集器(标记-整理算法) 
老年代单线程收集器,Serial收集器的老年代版本。 
ParNew收集器(停止-复制算法)  
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。 
Parallel Scavenge收集器(停止-复制算法)  
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。 
Parallel Old收集器(停止-复制算法) 
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。 
CMS(Concurrent Mark Sweep)收集器(标记-清理算法) 
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。 
 CMS收集器在收集老年代的时候分为以下几个阶段:初始标记、并发标记、预清理、可中断预清理、最终标记、并发清除、并发重置

4.7静态对象要成为垃圾被回收,要满足三个条件: 

1. 这个类的对象变成了垃圾 
2. 加载这个类的类加载器变成了垃圾 
3. 关于这个对象的class对象也变成了垃圾 
只有满足这三个条件,静态对象才会变成垃圾被回收,要不然静态对象会一直存在于永久带中.

4.8几种常量池:

Class 文件常量池:存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
字面量:指 字符串 字面量和声明为 final 的(基本数据类型)常量值。字符串包括打引号的 string 以及类名、方法名、字段的名称和描述符,当我们的代码访问了类中的常量时,该常量才会在编译时被放入对应类class文件的常量池,否则不会被放入
符号引用:以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用;哪些是符号引用:指向字面量的引用。
符号引用在类被加载解析阶段会被转化成符号引用
该常量池会被加载到运行时常量池,而 Java 7中运行时常量池又改到堆里面了

运行时常量池:每个类私有的,Class 文件常量池将在类加载后进入方法区的运行时常量池中存放,因为class文件常量池存放的是符号引用。运行时常量池可以在运行期间将符号引用解析为直接引用
一个类加载到 JVM 中后对应一个运行时常量池
所有的常量池均放到堆里面了。

字符串常量池,全局共享的,jdk1.7也放到堆中了。
Java里String a = new String("abc");这会在堆和栈里面分别创建对象abc吗?

这里面讲的字符串常量池存放的是引用。

String.intern()方法:返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用。(Java-String.intern的深入研究)

4.9java性能优化:

  1. 物理对象如connection和io用了之后要及时close
  2. 大的对象用好之后及时设置为null。
  3. 集合对象及时clear
  4. 减少使用string
  5. 今肯呢个的使用软和弱引用
  6. 不要重写finalize方法
  7. 通过调整命令行参数来调整堆内存的性能。如-Xms和-Xmx设置启动初始堆大小和能获得的堆最大内存。

如何排查内存问题:

什么时候需要排查:

  1. 日志中经常看到oom或stackoverflowerror异常。
  2. 程序运行时间大大超过预期
  3. 日志上程序无故卡在某个地方,且位置不同
  4. 运行时内存和cpu占用率居高不下,超过预期

使用jdk自带的jconsole来观察内存使用量

使用gc日志来观察,来判断堆内存空间分配是否合适。

在代码中打印当前内存使用量

出现oom后获取和分析dump文件:定位类和方法问题。

 

5类加载:

5.1java类加载的方式分为两种:

显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。

而隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。在日常开发以上两种方式一般会混合使用,这里我们知道有这么回事即可。

5.2类初始化的时机

所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化 。下面这几种情形必须立即对类进行“初始化”:

1)遇到 new、 getstatic、 putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化, 则需要先触发其初始化, 生成这4条指令的最常见的 Java代码场景是:

使用 new关键字实例化对象的时候
读取或设置一个类的静态字段的时候(即在字节码中,执行getstalic或putstatic指令时),被final修饰、已在编译期把结果放入常量池的静态字段除外
调用一个类的静态方法的时候(即在字节码中执行invokestatic指令时)。
2 ) 当调用Java API中的某些反射方法时, 比如类Class中的方法或者java.lang.reflect包的方法对类进行反射调用的时候, 如果类没有进行过初始化 , 则需要先触发其初始化。

3 ) 当初始化一个类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化。

4) 当虚拟机启动时, 用户需要指定一个要执行的主类(包合 main()方法的那个类) . 虚拟机会先初始化这个主类。

5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对于这五种会触发类进行初始化的场景, 虚拟机规范中使用了一个很强烈的限定语:“有且只有 '', 这5种场景中的行为称为对一个类进行主动引用 。 除此之外,所有引用类的方式都不会触发初始化, 称为被动引用。

5.3类的加载机制:

在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:
类的完整类名必须一致,包括包名。

加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中,但前提是覆写loadclass方法。

Java执行那些事——类加载机制( 上)

类加载(Class Loading)是一种机制,他描述的是将字节码以文件形式加载到内存再经过连接、初始化后,最终形成可以被虚拟机直接使用的Java类型地过程。

JVM采用这种在运行期才去加载、连接、初始化的策略会稍微增加一些的性能开销,导致例如程序启动慢的这样的缺点。但是它却可以为程序提供高度的灵活性,这是因为JVM的字节码执行引擎不需要提前了解关于文件和文件系统的任何信息,我们完全可以等到运行期才指定实际的实现方法,让一个本地程序通过网络加载任何地方的字节码文件。Java的动态拓展性正是赖于JVM类加载机制实现的。

当Java程序需要使用某个类时,如果该类还未被加载到内存中,JVM会通过加载、连接(验证、准备和解析)、初始化三个步骤来对该类进行初始化。

类的加载

1)通过一个类的全限定名来获取定义此类的二进制字节流。

2 )将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。(将静态常量池生成动态常量池)

3 ) 将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。Class对象还不完整,所以此时的类还不可用。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源(详细介绍可以看下面classloader):

从本地文件系统加载class文件;
从一个ZIP、 JAR、 CAB或者其他某种归档文件中提取Java class文件,JDBC编程时使用到的数据库驱动就是放在JAR文件中,JVM可以直接从JAR包中加载class文件;
通过网络加载class文件,这种场景最典型的应用就是 Applet;
把一个java源文件动态编译、并执行加载
运行时计算生成, 这种场景使用得最多的就是动态代理接术, 在 java.lang.reflect.Proxy中 , 就是用了 ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。

类的连接

连接阶段将会负责把类的二进制文件合并到JRE中。类连接分为如下三个阶段:

验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致;会完成下面四个阶段的检验过程: 文件格式验证、 元数据验证、 字节码验证、符号引用验证。
准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;这些变量所使用的内存都将在方法区中进行分配 。
解析:将类的二进制数据中的符号引用替换成直接引用(符号引用以一组符号来描述所引用的日标,符号可以是任何形式的字面量, 只要使用时能无歧义地定位到目标即可, 特号引用与配組机实现的内存1布.局11i-美 , 引用的日标并不一定已组加裁到内存中;直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。有了直接引用, 那引用的目标必定已经在内存中存在)
JVM里的符号引用如何存储?
符号引用通常是设计字符串的——用文本形式来表示引用关系。
直接饮用JVM是否能“直接使用”这种形式的数据。

初始化:

初始化阶段是类加载过程的最后一步 , 前面的几个阶段, 除了在加载阶段用户应用程序可以通过自定 义类加载器參与之外, 其余动作完全由虚拟机主导和控制。到了初始化阶段, 才真正开始执行类中定义的 Java程序代码。

从代码角度,初始化阶段是执行类构造器()方法的过程。

()方法是由编译器自动收集类中的所有类变量的赋值动作和静志语句块(static{}块)中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序所决定的, 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在它之后的変量 , 在前面的静态语句块可以赋值 , 但是不能访问
()方法与类的构造函数 (或者说实例构造器()方法)不同,它不需要显式地调用父类构造器, 虚期机会保证在子类的()方法执行之前, 父类的()方法已经执行完毕, 因此在虚期机中第一个被执行的()方法的类肯定是 java,lang.Object
由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
()方法对于类或接口来说并不是必须的, 如果一个类中没有静态语句块,也没有对变量的赋值操作, 那么编译器可以不为这个类生成()方法
接口中不能使用静态语句块(1.8之前不行,1.8之后可以了),但仍然有变量初始化的赋值操作, 因此接口与类一样都会生成()方法。 但接口与类不同的是, 执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量被使用时, 父接口才会被初始化。 另外, 接口的实现类在初始化时也一样不会执行接口的()方法
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()法,其他线程部需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作, 那就可能造成多个进程阻塞, 在实际应用中这种阻塞往往是隐蔽的。

JVM初始化一个类一般包括如下几个步骤:

假如这个类还没有被加载和连接,程序先加载并连接该类;
假如该类的直接父类还没有被初始化,则先初始化其直接父类;
假如类中有初始化语句,则系统依次执行这些初始化语句
当执行第二步时,系统对直接父类的初始化也遵循此1、2、3步骤,如果该直接父类又有直接父类,系统再次重复这三步,所以JVM最先初始化的总是java.lang.Object类。

通过以上讲解可以明白类的加载顺序

静态方法,静态块可以继承的吗?

静态方法、静态属性可以被继承。
静态块最终是写到类或接口初始化方法()里的
静态方法没有多态,不能被重写,只能被隐藏

java执行顺序

若类还未被加载
1. 先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关。
2. 执行子类的静态代码块和静态变量初始化。
3. 执行父类的实例变量初始化
4. 执行父类的构造函数
5. 执行子类的实例变量初始化
6. 执行子类的构造函数

若类已经加载
则静态代码块和静态变量就不用重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法

类加载的动态性体现:

程序时由很多个类来组成的,当程序启动的时候,JVM会先把保障程序运行的最基本的类一次性加载进来,其余的类在运行时随用随加载,这种方法好处是节省了内存开销。(因为最早java是为嵌入式系统而设计的,内存宝贵。用到时再加载也是java动态性的一种体现)

5.2classloader讲解:

一看你就懂,超详细java中的ClassLoader详解(强烈推荐这篇,讲的非常好)

深入理解Java类加载器(ClassLoader)

ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。
class文件是字节码格式文件,java虚拟机并不能直接识别我们平常编写的.java源文件,所以需要javac这个命令转换成.class文件。

java语言系统自带有三个类加载器:
Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的Object.class、Ststem.clss、int.class,String.class都是由它加载。

Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。加载一些扩展的系统类,比如XML、加密、解压相关的功能类等;
(C:\JDK\jre\lib\ext目录有什么用呀?C:\JDK\jre\lib\ext是沿袭了以前的jdk版本的包的寻找、存放路径,当指定的JAVA_HOME时,有一个默认的CLASSPAH就会指向JAVA_HOME\jre\lib\ext中,所以把servlet.jar拷入其中是可以的,但是一般的做法只需要在你的系统环境变量中指定CLASSPATH到你的实际的servlet.jar就行了,或者一般在设定CLASSPATH为JAVA_HOME\lib,你也可以把servlet.jar拷入JAVA_HOME\lib中,以上的做法只是提供给你在命令行中进行编译的需要。
而在IDE里都会有一个项设置库的路径,把需要的包加入就行了
如果仅是在tomcat中使用,则需要把相应的包拷在TOMCAT_HOME\common\lib中,或是你的应用的WEB-INF\classes\lib中
因为tomcat也会重设classpath,环境变量中对它也没作用)

Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。主要是加载用户定义的CLASSPATH路径下的类

BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.path、java.ext.dirs和java.class.path来加载资源文件的。

校招准备:(二):java高级知识(反射,动态代理,java和jvm内存模型,垃圾回收,类加载)_第4张图片
每个类加载器都有一个父加载器,父加载器不是父类。除了bootstrap外,其他的不一定有。
一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

这样设计的原因:

1、 出于安全考虑,这么做保证了java核心库的安全性,确保基础类永远都是由java提供的跟加载器来加载。
2、 可以避免重复加载,当父加载器已经加载了该类后,子类就没有必要再加载一次。
从以上两点出发,如果有人恶意篡改了基础类的代码(例如:java.lang.string)那他自己定义的java.lang.string将永远不会被加载进来,因为原始的String类已经在启动的时候就被加载进来了。

自定义ClassLoader可以做什么?
不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。
我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,

其实一个已经加载的类是无法被更新的,如果你试图用同一个ClassLoader再次加载同一个类,就会得到异常(java.lang.LinkageError: duplicate classdefinition),我们只能够重新创建一个新的ClassLoader实例来再次加载新类。至于原来已经加载的类,开发人员不必去管它,因为它可能还有实例正在被使用,只要相关的实例都被内存回收了,那么JVM就会在适当的时候把不会再使用的类卸载。

int.class的讲解:int只是有对应的java.lang.Class对象作为反射系统的一部分,以便实现反射系统的完整性。但int是一个Java的基本类型,不是一个类。

5.3关于静态内部类的问题。

Java中静态内部类的加载时机

  1. 外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类。
  2. 实例化外部类,调用外部类的静态方法、静态变量,则外部类必须先进行加载,但只加载一次。
  3. 直接调用静态内部类时,外部类不会加载。

java涉及内部类加载的问题

“内部类”是一个基本上只存在于Java语言层面,而不存在于JVM层面的一个概念。JVM并不关心一个类是顶层类还是内部类;在VM层面上唯一能表现类的嵌套关系的就是在Class文件里有记录的很少量的反射信息,会通过Java的反射API暴露出来(例如 java.lang.Class.getEnclosingClass()),但JVM自身在运行程序的时候并不需要也不使用这个信息。

所以这是怎么做到的?其实是Java语言编译器(例如javac、ECJ)在将Java源码编译到Class文件的过程中,将内部类做了“解糖”,给其添加一些必要的转换之后将其提升为跟顶层类一样的形式,然后后面就不再有内部类与否的区别了。于是JVM就不用关心什么是内部类了。

既然如此,JVM加载内部类的时机其实就跟加载任何类(包括顶层类与内部类)的时机一样:要表现为是在第一次主动使用时加载。主动使用包括new、getstatic、putstatic、invokestatic,以及Class.forName()作为一种特例。

6.Java的解释执行

.java文件,通过编译器后(Javac)编程成JVM所认识的bytecode字节码,然后在运行时吗,JVM内嵌的解释器再通过逐一解释,最终将字节码(bytecode)转换为计算机所认识的机器码。

JIT(Just-In-Time)即时编译。这是一种动态编译技术,它能够在运行时,将热点代码编译成机器码,来提升程序的性能。JIT是方法级的,它会缓存解释过的字节码在CodeCache,当下次执行时直接使用,而不需要重复解释。从而提升性能。

而AOT直接从字节码编译成机器码,更为彻底,避免了JIT的各种预热开销,性能提升更显著(JDK9已经在使用)。

你可能感兴趣的:(java笔记)