,在编译后都会变成 List 。
定义一个泛型类型,会自动提供一个对应原始类型,类型变量会被擦除。如果没有限定类型就会替换为 Object,如果有限定类型就会替换为第一个限定类型,例如 会使用 A 类型替换 T。
Q11:JDK8 新特性有哪些?
**lambda 表达式:**允许把函数作为参数传递到方法,简化匿名内部类代码。
**函数式接口:**使用 @FunctionalInterface 标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。
**方法引用:**可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。
**接口:**接口可以定义 default 修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。
**注解:**引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。
**类型推测:**加强了类型推测机制,使代码更加简洁。
**Optional 类:**处理空指针异常,提高代码可读性。
**Stream 类:**引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach 遍历、count 统计个数、filter 按条件过滤、limit 取前 n 个元素、skip 跳过前 n 个元素、map 映射加工、concat 合并 stream 流等。
**日期:**增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。
**JavaScript:**提供了一个新的 JavaScript 引擎,允许在 JVM上运行特定 JavaScript 应用。
Q12:异常有哪些分类?
所有异常都是 Throwable 的子类,分为 Error 和 Exception。Error 是 Java 运行时系统的内部错误和资源耗尽错误,例如 StackOverFlowError 和 OutOfMemoryError,这种异常程序无法处理。
Exception 分为受检异常和非受检异常,受检异常需要在代码中显式处理,否则会编译出错,非受检异常是运行时异常,继承自 RuntimeException。
受检异常:① 无能为力型,如字段超长导致的 SQLException。② 力所能及型,如未授权异常 UnAuthorizedException,程序可跳转权限申请页面。常见受检异常还有 FileNotFoundException、ClassNotFoundException、IOException等。
非受检异常:① 可预测异常,例如 IndexOutOfBoundsException、NullPointerException、ClassCastException 等,这类异常应该提前处理。② 需捕捉异常,例如进行 RPC 调用时的远程服务超时,这类异常客户端必须显式处理。③ 可透出异常,指框架或系统产生的且会自行处理的异常,例如 Spring 的 NoSuchRequestHandingMethodException,Spring 会自动完成异常处理,将异常自动映射到合适的状态码。
数据类型 5
Q1:Java 有哪些基本数据类型?

JVM 没有 boolean 赋值的专用字节码指令,boolean f = false 就是使用 ICONST_0 即常数 0 赋值。单个 boolean 变量用 int 代替,boolean 数组会编码成 byte 数组。
Q2:自动装箱/拆箱是什么?
每个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可。
自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。
自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
比较两个包装类数值要用 equals ,而不能用 == 。
Q3:String 是不可变类为什么值可以修改?
String 类和其存储数据的成员变量 value 字节数组都是 final 修饰的。对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象。只是修改 String 变量引用的对象,没有修改原 String 对象的内容。
Q4:字符串拼接的方式有哪些?
① 直接用 + ,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 + 拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。
② 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用 getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +。
③ 使用 StringBuilder 或 StringBuffer,两者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 确定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy 将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。
Q5:String a = “a” + new String(“b”) 创建了几个对象?
常量和常量拼接仍是常量,结果在常量池,只要有变量参与拼接结果就是变量,存在堆。
使用字面量时只创建一个常量池中的常量,使用 new 时如果常量池中没有该值就会在常量池中新创建,再在堆中创建一个对象引用常量池中常量。因此 String a = "a" + new String("b") 会创建四个对象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。
面向对象 10
Q1:谈一谈你对面向对象的理解
面向过程让计算机有步骤地顺序做一件事,是过程化思维,使用面向过程语言开发大型项目,软件复用和维护存在很大问题,模块之间耦合严重。面向对象相对面向过程更适合解决规模较大的问题,可以拆解问题复杂度,对现实事物进行抽象并映射为开发对象,更接近人的思维。
例如开门这个动作,面向过程是 open(Door door),动宾结构,door 作为操作对象的参数传入方法,方法内定义开门的具体步骤。面向对象的方式首先会定义一个类 Door,抽象出门的属性(如尺寸、颜色)和行为(如 open 和 close),主谓结构。
面向过程代码松散,强调流程化解决问题。面向对象代码强调高内聚、低耦合,先抽象模型定义共性行为,再解决实际问题。
Q2:面向对象的三大特性?
封装是对象功能内聚的表现形式,在抽象基础上决定信息是否公开及公开等级,核心问题是以什么方式暴漏哪些信息。主要任务是对属性、数据、敏感行为实现隐藏,对属性的访问和修改必须通过公共接口实现。封装使对象关系变得简单,降低了代码耦合度,方便维护。
迪米特原则就是对封装的要求,即 A 模块使用 B 模块的某接口行为,对 B 模块中除此行为外的其他信息知道得应尽可能少。不直接对 public 属性进行读取和修改而使用 getter/setter 方法是因为假设想在修改属性时进行权限控制、日志记录等操作,在直接访问属性的情况下无法实现。如果将 public 的属性和行为修改为 private 一般依赖模块都会报错,因此不知道使用哪种权限时应优先使用 private。
继承用来扩展一个类,子类可继承父类的部分属性和行为使模块具有复用性。继承是"is-a"关系,可使用里氏替换原则判断是否满足"is-a"关系,即任何父类出现的地方子类都可以出现。如果父类引用直接使用子类引用来代替且可以正确编译并执行,输出结果符合子类场景预期,那么说明两个类符合里氏替换原则。
多态以封装和继承为基础,根据运行时对象实际类型使同一行为具有不同表现形式。多态指在编译层面无法确定最终调用的方法体,在运行期由 JVM 动态绑定,调用合适的重写方法。由于重载属于静态绑定,本质上重载结果是完全不同的方法,因此多态一般专指重写。
Q3:重载和重写的区别?
重载指方法名称相同,但参数类型个数不同,是行为水平方向不同实现。对编译器来说,方法名称和参数列表组成了一个唯一键,称为方法签名,JVM 通过方法签名决定调用哪种重载方法。不管继承关系如何复杂,重载在编译时可以根据规则知道调用哪种目标方法,因此属于静态绑定。
JVM 在重载方法中选择合适方法的顺序:① 精确匹配。② 基本数据类型自动转换成更大表示范围。③ 自动拆箱与装箱。④ 子类向上转型。⑤ 可变参数。
重写指子类实现接口或继承父类时,保持方法签名完全相同,实现不同方法体,是行为垂直方向不同实现。
元空间有一个方法表保存方法信息,如果子类重写了父类的方法,则方法表中的方法引用会指向子类实现。父类引用执行子类方法时无法调用子类存在而父类不存在的方法。
重写方法访问权限不能变小,返回类型和抛出的异常类型不能变大,必须加 @Override 。
Q4:类之间有哪些关系?

Q5:Object 类有哪些方法?
**equals:**检测对象是否相等,默认使用 == 比较对象引用,可以重写 equals 方法自定义比较规则。equals 方法规范:自反性、对称性、传递性、一致性、对于任何非空引用 x,x.equals(null) 返回 false。
**hashCode:**散列码是由对象导出的一个整型值,没有规律,每个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。为了在集合中正确使用,一般需要同时重写 equals 和 hashCode,要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同,因此 hashCode 是对象相等的必要不充分条件。
toString:打印对象时默认的方法,如果没有重写打印的是表示对象值的一个字符串。
**clone:**clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方法会抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现 Cloneable 接口并指定访问修饰符为 public。
**finalize:**确定一个对象死亡至少要经过两次标记,如果对象在可达性分析后发现没有与 GC Roots 连接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行 finalize 方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。如果有必要执行,对象会被放置在 F-Queue 队列,由一条低调度优先级的 Finalizer 线程去执行。虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的 finalize 方法执行缓慢或发生死循环。只要对象在 finalize 方法中重新与引用链上的对象建立关联就会在第二次标记时被移出回收集合。由于运行代价高昂且无法保证调用顺序,在 JDK 9 被标记为过时方法,并不适合释放资源。
**getClass:**返回包含对象信息的类对象。
**wait / notify / notifyAll:**阻塞或唤醒持有该对象锁的线程。
Q6:内部类的作用是什么,有哪些分类?
内部类可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括 private 数据。
内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用 $ 分隔外部类名与内部类名,其中匿名内部类使用数字编号,虚拟机对此一无所知。
静态内部类: 属于外部类,只加载一次。作用域仅在包内,可通过 外部类名.内部类名 直接访问,类内只能访问外部类所有静态属性和方法。HashMap 的 Node 节点,ReentrantLock 中的 Sync 类,ArrayList 的 SubList 都是静态内部类。内部类中还可以定义内部类,如 ThreadLoacl 静态内部类 ThreadLoaclMap 中定义了内部类 Entry。
成员内部类: 属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容。
局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明类的代码块中。
匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。
Q7:访问权限控制符有哪些?

Q8:接口和抽象类的异同?
接口和抽象类对实体类进行更高层次的抽象,仅定义公共行为和特征。

Q9:接口和抽象类应该怎么选择?
抽象类体现 is-a 关系,接口体现 can-do 关系。与接口相比,抽象类通常是对同类事物相对具体的抽象。
抽象类是模板式设计,包含一组具体特征,例如某汽车,底盘、控制电路等是抽象出来的共同特征,但内饰、显示屏、座椅材质可以根据不同级别配置存在不同实现。
接口是契约式设计,是开放的,定义了方法名、参数、返回值、抛出的异常类型,谁都可以实现它,但必须遵守接口的约定。例如所有车辆都必须实现刹车这种强制规范。
接口是顶级类,抽象类在接口下面的第二层,对接口进行了组合,然后实现部分接口。当纠结定义接口和抽象类时,推荐定义为接口,遵循接口隔离原则,按维度划分成多个接口,再利用抽象类去实现这些,方便后续的扩展和重构。
例如 Plane 和 Bird 都有 fly 方法,应把 fly 定义为接口,而不是抽象类的抽象方法再继承,因为除了 fly 行为外 Plane 和 Bird 间很难再找到其他共同特征。
Q10:子类初始化的顺序
① 父类静态代码块和静态变量。② 子类静态代码块和静态变量。③ 父类普通代码块和普通变量。④ 父类构造方法。⑤ 子类普通代码块和普通变量。⑥ 子类构造方法。
集合 7
Q1:说一说 ArrayList
ArrayList 是容量可变的非线程安全列表,使用数组实现,集合扩容时会创建更大的数组,把原有数组复制到新数组。支持对元素的快速随机访问,但插入与删除速度很慢。ArrayList 实现了 RandomAcess 标记接口,如果一个类实现了该接口,那么表示使用索引遍历比迭代器更快。
elementData是 ArrayList 的数据域,被 transient 修饰,序列化时会调用 writeObject 写入流,反序列化时调用 readObject 重新赋值到新对象的 elementData。原因是 elementData 容量通常大于实际存储元素的数量,所以只需发送真正有实际值的数组元素。
size 是当前实际大小,elementData 大小大于等于 size。
**modCount **记录了 ArrayList 结构性变化的次数,继承自 AbstractList。所有涉及结构变化的方法都会增加该值。expectedModCount 是迭代器初始化时记录的 modCount 值,每次访问新元素时都会检查 modCount 和 expectedModCount 是否相等,不相等就会抛出异常。这种机制叫做 fail-fast,所有集合类都有这种机制。
Q2:说一说 LinkedList
LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。除继承 AbstractList 外还实现了 Deque 接口,这个接口具有队列和栈的性质。成员变量被 transient 修饰,原理和 ArrayList 类似。
LinkedList 包含三个重要的成员:size、first 和 last。size 是双向链表中节点的个数,first 和 last 分别指向首尾节点的引用。
LinkedList 的优点在于可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率较高。
Q3:Set 有什么特点,有哪些实现?
Set 不允许元素重复且无序,常用实现有 HashSet、LinkedHashSet 和 TreeSet。
HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,所有 Key 都使用相同的 Value ,一个名为 PRESENT 的 Object 类型常量。使用 Key 保证元素唯一性,但不保证有序性。由于 HashSet 是 HashMap 实现的,因此线程不安全。
HashSet 判断元素是否相同时,对于包装类型直接按值比较。对于引用类型先比较 hashCode 是否相同,不同则代表不是同一个对象,相同则继续比较 equals,都相同才是同一个对象。
LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。
TreeSet 通过 TreeMap 实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。
Q4:TreeMap 有什么特点?
TreeMap 基于红黑树实现,增删改查的平均和最差时间复杂度均为 O(logn) ,最大特点是 Key 有序。Key 必须实现 Comparable 接口或提供的 Comparator 比较器,所以 Key 不允许为 null。
HashMap 依靠 hashCode 和 equals 去重,而 TreeMap 依靠 Comparable 或 Comparator。 TreeMap 排序时,如果比较器不为空就会优先使用比较器的 compare 方法,否则使用 Key 实现的 Comparable 的 compareTo 方法,两者都不满足会抛出异常。
TreeMap 通过 put 和 deleteEntry 实现增加和删除树节点。插入新节点的规则有三个:① 需要调整的新节点总是红色的。② 如果插入新节点的父节点是黑色的,不需要调整。③ 如果插入新节点的父节点是红色的,由于红黑树不能出现相邻红色,进入循环判断,通过重新着色或左右旋转来调整。TreeMap 的插入操作就是按照 Key 的对比往下遍历,大于节点值向右查找,小于向左查找,先按照二叉查找树的特性操作,后续会重新着色和旋转,保持红黑树的特性。
Q5:HashMap 有什么特点?
JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树,节点类型从Entry 变更为 Node。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor。
table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。
HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上,为使查询效率尽可能高,键的 hash 值要尽可能分散。
HashMap 默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。
Q6:HashMap 相关方法的源码?
JDK8 之前
hash:计算元素 key 的散列值
① 处理 String 类型时,调用 stringHash32 方法获取 hash 值。
② 处理其他类型数据时,提供一个相对于 HashMap 实例唯一不变的随机值 hashSeed 作为计算初始量。
③ 执行异或和无符号右移使 hash 值更加离散,减小哈希冲突概率。
indexFor:计算元素下标
将 hash 值和数组长度-1 进行与操作,保证结果不会超过 table 数组范围。
get:获取元素的 value 值
① 如果 key 为 null,调用 getForNullKey 方法,如果 size 为 0 表示链表为空,返回 null。如果 size 不为 0 说明存在链表,遍历 table[0] 链表,如果找到了 key 为 null 的节点则返回其 value,否则返回 null。
② 如果 key 为 不为 null,调用 getEntry 方法,如果 size 为 0 表示链表为空,返回 null 值。如果 size 不为 0,首先计算 key 的 hash 值,然后遍历该链表的所有节点,如果节点的 key 和 hash 值都和要查找的元素相同则返回其 Entry 节点。
③ 如果找到了对应的 Entry 节点,调用 getValue 方法获取其 value 并返回,否则返回 null。
put:添加元素
① 如果 key 为 null,直接存入 table[0]。
② 如果 key 不为 null,计算 key 的 hash 值。
③ 调用 indexFor 计算元素存放的下标 i。
④ 遍历 table[i] 对应的链表,如果 key 已存在,就更新 value 然后返回旧 value。
⑤ 如果 key 不存在,将 modCount 值加 1,使用 addEntry 方法增加一个节点并返回 null。
resize:扩容数组
① 如果当前容量达到了最大容量,将阈值设置为 Integer 最大值,之后扩容不再触发。
② 否则计算新的容量,将阈值设为 newCapacity x loadFactor 和 最大容量 + 1 的较小值。
③ 创建一个容量为 newCapacity 的 Entry 数组,调用 transfer 方法将旧数组的元素转移到新数组。
transfer:转移元素
① 遍历旧数组的所有元素,调用 rehash 方法判断是否需要哈希重构,如果需要就重新计算元素 key 的 hash 值。
② 调用 indexFor 方法计算元素存放的下标 i,利用头插法将旧数组的元素转移到新数组。
JDK8
hash:计算元素 key 的散列值
如果 key 为 null 返回 0,否则就将 key 的 hashCode 方法返回值高低16位异或,让尽可能多的位参与运算,让结果的 0 和 1 分布更加均匀,降低哈希冲突概率。
put:添加元素
① 调用 putVal 方法添加元素。
② 如果 table 为空或长度为 0 就进行扩容,否则计算元素下标位置,不存在就调用 newNode 创建一个节点。
③ 如果存在且是链表,如果首节点和待插入元素的 hash 和 key 都一样,更新节点的 value。
④ 如果首节点是 TreeNode 类型,调用 putTreeVal 方法增加一个树节点,每一次都比较插入节点和当前节点的大小,待插入节点小就往左子树查找,否则往右子树查找,找到空位后执行两个方法:balanceInsert 方法,插入节点并调整平衡、moveRootToFront 方法,由于调整平衡后根节点可能变化,需要重置根节点。
⑤ 如果都不满足,遍历链表,根据 hash 和 key 判断是否重复,决定更新 value 还是新增节点。如果遍历到了链表末尾则添加节点,如果达到建树阈值 7,还需要调用 treeifyBin 把链表重构为红黑树。
⑥ 存放元素后将 modCount 加 1,如果 ++size > threshold ,调用 resize 扩容。
get :获取元素的 value 值
① 调用 getNode 方法获取 Node 节点,如果不是 null 就返回其 value 值,否则返回 null。
② getNode 方法中如果数组不为空且存在元素,先比较第一个节点和要查找元素的 hash 和 key ,如果都相同则直接返回。
③ 如果第二个节点是 TreeNode 类型则调用 getTreeNode 方法进行查找,否则遍历链表根据 hash 和 key 查找,如果没有找到就返回 null。
resize:扩容数组
重新规划长度和阈值,如果长度发生了变化,部分数据节点也要重新排列。
重新规划长度
① 如果当前容量 oldCap > 0 且达到最大容量,将阈值设为 Integer 最大值,return 终止扩容。
② 如果未达到最大容量,当 oldCap << 1 不超过最大容量就扩大为 2 倍。
③ 如果都不满足且当前扩容阈值 oldThr > 0,使用当前扩容阈值作为新容量。
④ 否则将新容量置为默认初始容量 16,新扩容阈值置为 12。
重新排列数据节点
① 如果节点为 null 不进行处理。
② 如果节点不为 null 且没有next节点,那么通过节点的 hash 值和 新容量-1 进行与运算计算下标存入新的 table 数组。
③ 如果节点为 TreeNode 类型,调用 split 方法处理,如果节点数 hc 达到6 会调用 untreeify 方法转回链表。
④ 如果是链表节点,需要将链表拆分为 hash 值超出旧容量的链表和未超出容量的链表。对于hash & oldCap == 0 的部分不需要做处理,否则需要放到新的下标位置上,新下标 = 旧下标 + 旧容量。
Q7:HashMap 为什么线程不安全?
JDK7 存在死循环和数据丢失问题。
数据丢失:
- 并发赋值被覆盖: 在 createEntry 方法中,新添加的元素直接放在头部,使元素之后可以被更快访问,但如果两个线程同时执行到此处,会导致其中一个线程的赋值被覆盖。
- 已遍历区间新增元素丢失: 当某个线程在 transfer 方法迁移时,其他线程新增的元素可能落在已遍历过的哈希槽上。遍历完成后,table 数组引用指向了 newTable,新增元素丢失。
- 新表被覆盖: 如果 resize 完成,执行了 table = newTable,则后续元素就可以在新表上进行插入。但如果多线程同时 resize ,每个线程都会 new 一个数组,这是线程内的局部对象,线程之间不可见。迁移完成后resize 的线程会赋值给 table 线程共享变量,可能会覆盖其他线程的操作,在新表中插入的对象都会被丢弃。
死循环: 扩容时 resize 调用 transfer 使用头插法迁移元素,虽然 newTable 是局部变量,但原先 table 中的 Entry 链表是共享的,问题根源是 Entry 的 next 指针并发修改,某线程还没有将 table 设为 newTable 时用完了 CPU 时间片,导致数据丢失或死循环。
JDK8 在 resize 方法中完成扩容,并改用尾插法,不会产生死循环,但并发下仍可能丢失数据。可用 ConcurrentHashMap 或 Collections.synchronizedMap 包装成同步集合。
IO 流 6
Q1:同步/异步/阻塞/非阻塞 IO 的区别?
同步和异步是通信机制,阻塞和非阻塞是调用状态。
同步 IO 是用户线程发起 IO 请求后需要等待或轮询内核 IO 操作完成后才能继续执行。异步 IO 是用户线程发起 IO 请求后可以继续执行,当内核 IO 操作完成后会通知用户线程,或调用用户线程注册的回调函数。
阻塞 IO 是 IO 操作需要彻底完成后才能返回用户空间 。非阻塞 IO 是 IO 操作调用后立即返回一个状态值,无需等 IO 操作彻底完成。
Q2:什么是 BIO?
BIO 是同步阻塞式 IO,JDK1.4 之前的 IO 模型。服务器实现模式为一个连接请求对应一个线程,服务器需要为每一个客户端请求创建一个线程,如果这个连接不做任何事会造成不必要的线程开销。可以通过线程池改善,这种 IO 称为伪异步 IO。适用连接数目少且服务器资源多的场景。
Q3:什么是 NIO?
NIO 是 JDK1.4 引入的同步非阻塞 IO。服务器实现模式为多个连接请求对应一个线程,客户端连接请求会注册到一个多路复用器 Selector ,Selector 轮询到连接有 IO 请求时才启动一个线程处理。适用连接数目多且连接时间短的场景。
同步是指线程还是要不断接收客户端连接并处理数据,非阻塞是指如果一个管道没有数据,不需要等待,可以轮询下一个管道。
核心组件:
- Selector: 多路复用器,轮询检查多个 Channel 的状态,判断注册事件是否发生,即判断 Channel 是否处于可读或可写状态。使用前需要将 Channel 注册到 Selector,注册后会得到一个 SelectionKey,通过 SelectionKey 获取 Channel 和 Selector 相关信息。
- Channel: 双向通道,替换了 BIO 中的 Stream 流,不能直接访问数据,要通过 Buffer 来读写数据,也可以和其他 Channel 交互。
- Buffer: 缓冲区,本质是一块可读写数据的内存,用来简化数据读写。Buffer 三个重要属性:position 下次读写数据的位置,limit 本次读写的极限位置,capacity 最大容量。
- flip 将写转为读,底层实现原理把 position 置 0,并把 limit 设为当前的 position 值。
- clear 将读转为写模式(用于读完全部数据的情况,把 position 置 0,limit 设为 capacity)。
- compact 将读转为写模式(用于存在未读数据的情况,让 position 指向未读数据的下一个)。
- 通道方向和 Buffer 方向相反,读数据相当于向 Buffer 写,写数据相当于从 Buffer 读。
使用步骤:向 Buffer 写数据,调用 flip 方法转为读模式,从 Buffer 中读数据,调用 clear 或 compact 方法清空缓冲区。
Q4:什么是 AIO?
AIO 是 JDK7 引入的异步非阻塞 IO。服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操作系统先完成 IO 操作后再通知服务器应用来直接使用准备好的数据。适用连接数目多且连接时间长的场景。
异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。
实现方式包括通过 Future 的 get 方法进行阻塞式调用以及实现 CompletionHandler 接口,重写请求成功的回调方法 completed 和请求失败回调方法 failed。
Q5:java.io 包下有哪些流?
主要分为字符流和字节流,字符流一般用于文本文件,字节流一般用于图像或其他文件。
字符流包括了字符输入流 Reader 和字符输出流 Writer,字节流包括了字节输入流 InputStream 和字节输出流 OutputStream。字符流和字节流都有对应的缓冲流,字节流也可以包装为字符流,缓冲流带有一个 8KB 的缓冲数组,可以提高流的读写效率。除了缓冲流外还有过滤流 FilterReader、字符数组流 CharArrayReader、字节数组流 ByteArrayInputStream、文件流 FileInputStream 等。
Q6:序列化和反序列化是什么?
Java 对象 JVM 退出时会全部销毁,如果需要将对象及状态持久化,就要通过序列化实现,将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化。
常见的序列化有三种:
实现 Serializabale 标记接口,Java 序列化保留了对象类的元数据(如类、成员变量、继承类信息)以及对象数据,兼容性最好,但不支持跨语言,性能一般。序列化和反序列化必须保持序列化 ID 的一致,一般使用 private static final long serialVersionUID 定义序列化 ID,如果不设置编译器会根据类的内部实现自动生成该值。如果是兼容升级不应该修改序列化 ID,防止出错,如果是不兼容升级则需要修改。
Hessian 序列化是一种支持动态类型、跨语言、基于对象传输的网络协议。Java 对象序列化的二进制流可以被其它语言反序列化。Hessian 协议的特性:① 自描述序列化类型,不依赖外部描述文件,用一个字节表示常用基础类型,极大缩短二进制流。② 语言无关,支持脚本语言。③ 协议简单,比 Java 原生序列化高效。Hessian 会把复杂对象所有属性存储在一个 Map 中序列化,当父类和子类存在同名成员变量时会先序列化子类再序列化父类,因此子类值会被父类覆盖。
JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好,方便调试。
序列化通常会使用网络传输对象,而对象中往往有敏感数据,容易遭受攻击,Jackson 和 fastjson 等都出现过反序列化漏洞,因此不需要进行序列化的敏感属性传输时应加上 transient 关键字。transient 的作用就是把变量生命周期仅限于内存而不会写到磁盘里持久化,变量会被设为对应数据类型的零值。
JVM 32
内存区域划分 8
Q1:运行时数据区是什么?
虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干不同的数据区,这些区域有各自的用途、创建和销毁时间。
线程私有:程序计数器、Java 虚拟机栈、本地方法栈。
线程共享:Java 堆、方法区。
Q2:程序计数器是什么?
程序计数器是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。字节码解释器工作时通过改变计数器的值选取下一条执行指令。分支、循环、跳转、线程恢复等功能都需要依赖计数器完成。是唯一在虚拟机规范中没有规定内存溢出情况的区域。
如果线程正在执行 Java 方法,计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法,计数器值为 Undefined。
Q3:Java 虚拟机栈的作用?
Java 虚拟机栈来描述 Java 方法的内存模型。每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和方法出口等信息。每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程。
有两类异常:① 线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError。② 如果 JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出 OutOfMemoryError(HotSpot 不可动态扩展,不存在此问题)。
Q4:本地方法栈的作用?
本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为虚本地方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法。
虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如 HotSpot 将虚拟机栈和本地方法栈合二为一。
本地方法栈在栈深度异常和栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError。
Q5:堆的作用是什么?
堆是虚拟机所管理的内存中最大的一块,被所有线程共享的,在虚拟机启动时创建。堆用来存放对象实例,Java 里几乎所有对象实例都在堆分配内存。堆可以处于物理上不连续的内存空间,逻辑上应该连续,但对于例如数组这样的大对象,多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间。
堆既可以被实现成固定大小,也可以是可扩展的,可通过 -Xms 和 -Xmx 设置堆的最小和最大容量,当前主流 JVM 都按照可扩展实现。如果堆没有内存完成实例分配也无法扩展,抛出 OutOfMemoryError。
Q6:方法区的作用是什么?
方法区用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK8 之前使用永久代实现方法区,容易内存溢出,因为永久代有 -XX:MaxPermSize 上限,即使不设置也有默认大小。JDK7 把放在永久代的字符串常量池、静态变量等移出,JDK8 中永久代完全废弃,改用在本地内存中实现的元空间代替,把 JDK 7 中永久代剩余内容(主要是类型信息)全部移到元空间。
虚拟机规范对方法区的约束宽松,除和堆一样不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收。垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载。如果方法区无法满足新的内存分配需求,将抛出 OutOfMemoryError。
Q7:运行时常量池的作用是什么?
运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。
运行时常量池相对于 Class 文件常量池的一个重要特征是动态性,Java 不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是 String 的 intern 方法。
运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError。
Q8:直接内存是什么?
直接内存不属于运行时数据区,也不是虚拟机规范定义的内存区域,但这部分内存被频繁使用,而且可能导致内存溢出。
JDK1.4 中新加入了 NIO 这种基于通道与缓冲区的 IO,它可以使用 Native 函数库直接分配堆外内存,通过一个堆里的 DirectByteBuffer 对象作为内存的引用进行操作,避免了在 Java 堆和 Native堆来回复制数据。
直接内存的分配不受 Java 堆大小的限制,但还是会受到本机总内存及处理器寻址空间限制,一般配置虚拟机参数时会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使内存区域总和大于物理内存限制,导致动态扩展时出现 OOM。
由直接内存导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果发现内存溢出后产生的 Dump 文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是 NIO),那么就可以考虑检查直接内存方面的原因。
内存溢出 5
Q1:内存溢出和内存泄漏的区别?
内存溢出 OutOfMemory,指程序在申请内存时,没有足够的内存空间供其使用。
内存泄露 Memory Leak,指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。
Q2:堆溢出的原因?
堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会 OOM,例如在 while 死循环中一直 new 创建实例。
堆 OOM 是实际应用中最常见的 OOM,处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存泄漏还是内存溢出。
如果是内存泄漏,通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联才导致无法回收,一般可以准确定位到产生内存泄漏代码的具体位置。
如果不是内存泄漏,即内存中对象都必须存活,应当检查 JVM 堆参数,与机器内存相比是否还有向上调整的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
Q3:栈溢出的原因?
由于 HotSpot 不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss 参数来设定,存在两种异常:
StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。
OutOfMemoryError: 如果 JVM 栈可以动态扩展,当扩展无法申请到足够内存时会抛出 OutOfMemoryError。HotSpot 不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OOM,否则在线程运行时是不会因为扩展而导致溢出的。
Q4:运行时常量池溢出的原因?
String 的 intern 方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此 String 对象的字符串,则返回池中这个字符串的 String 对象的引用,否则将此 String 对象包含的字符串添加到常量池并返回此 String 对象的引用。
在 JDK6 及之前常量池分配在永久代,因此可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制永久代大小,间接限制常量池。在 while 死循环中调用 intern 方法导致运行时常量池溢出。在 JDK7 后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中。
Q5:方法区溢出的原因?
方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用 JDK 反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib 这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。
JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施,例如 -XX:MetaspaceSize 指定元空间初始大小,达到该值会触发 GC 进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。
创建对象 5
Q1:创建对象的过程是什么?
字节码角度
- NEW: 如果找不到 Class 对象则进行类加载。加载成功后在堆中分配内存,从 Object 到本类路径上的所有属性都要分配。分配完毕后进行零值设置。最后将指向实例对象的引用变量压入虚拟机栈顶。
- **DUP: ** 在栈顶复制引用变量,这时栈顶有两个指向堆内实例的引用变量。两个引用变量的目的不同,栈底的引用用于赋值或保存局部变量表,栈顶的引用作为句柄调用相关方法。
- INVOKESPECIAL: 通过栈顶的引用变量调用 init 方法。
执行角度
① 当 JVM 遇到字节码 new 指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
② 在类加载检查通过后虚拟机将为新生对象分配内存。
③ 内存分配完成后虚拟机将成员变量设为零值,保证对象的实例字段可以不赋初值就使用。
④ 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。
⑤ 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
Q2:对象分配内存的方式有哪些?
对象所需内存大小在类加载完成后便可完全确定,分配空间的任务实际上等于把一块确定大小的内存块从 Java 堆中划分出来。
指针碰撞: 假设 Java 堆内存规整,被使用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。
空闲列表: 如果 Java 堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
选择哪种分配方式由堆是否规整决定,堆是否规整由垃圾收集器是否有空间压缩能力决定。使用 Serial、ParNew 等收集器时,系统采用指针碰撞;使用 CMS 这种基于清除算法的垃圾收集器时,采用空间列表。
Q3:对象分配内存是否线程安全?
对象创建十分频繁,即使修改一个指针的位置在并发下也不是线程安全的,可能正给对象 A 分配内存,指针还没来得及修改,对象 B 又使用了指针来分配内存。
解决方法:① CAS 加失败重试保证更新原子性。② 把内存分配按线程划分在不同空间,即每个线程在 Java 堆中预先分配一小块内存,叫做本地线程分配缓冲 TLAB,哪个线程要分配内存就在对应的 TLAB 分配,TLAB 用完了再进行同步。
Q4:对象的内存布局了解吗?
对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。
对象头占 12B,包括对象标记和类型指针。对象标记存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等,这部分占 8B,称为 Mark Word。Mark Word 被设计为动态数据结构,以便在极小的空间存储更多数据,根据对象状态复用存储空间。
类型指针是对象指向它的类型元数据的指针,占 4B。JVM 通过该指针来确定对象是哪个类的实例。
实例数据是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。
对齐填充不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是 8B 的倍数,对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐,需要对齐填充补全。
Q5:对象的访问方式有哪些?
Java 程序会通过栈上的 reference 引用操作堆对象,访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针。
句柄: 堆会划分出一块内存作为句柄池,reference 中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。优点是 reference 中存储的是稳定句柄地址,在 GC 过程中对象被移动时只会改变句柄的实例数据指针,而 reference 本身不需要修改。
直接指针: 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 存储对象地址,如果只是访问对象本身就不需要多一次间接访问的开销。优点是速度更快,节省了一次指针定位的时间开销,HotSpot 主要使用直接指针进行对象访问。
垃圾回收 7
Q1:如何判断对象是否是垃圾?
**引用计数:**在对象中添加一个引用计数器,如果被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。原理简单,效率高,但是在 Java 中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。
**可达性分析:**主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。
Q2:Java 的引用有哪些类型?
JDK1.2 后对引用进行了扩充,按强度分为四种:
强引用: 最常见的引用,例如 Object obj = new Object() 就属于强引用。只要对象有强引用指向且 GC Roots 可达,在内存回收时即使濒临内存耗尽也不会被回收。
软引用: 弱于强引用,描述非必需对象。在系统将发生内存溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。用来缓存服务器中间计算结果及不需要实时保存的用户行为等。
弱引用: 弱于软引用,描述非必需对象。弱引用关联的对象只能生存到下次 YGC 前,当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。由于 YGC 具有不确定性,因此弱引用何时被回收也不确定。
虚引用: 最弱的引用,定义完成后无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,垃圾回收时如果出现虚引用,就会在回收对象前把这个虚引用加入引用队列。
Q3:有哪些 GC 算法?
标记-清除算法
分为标记和清除阶段,首先从每个 GC Roots 出发依次标记有引用关系的对象,最后清除没有标记的对象。
执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低。
存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易触发 Full GC。
标记-复制算法
为了解决内存碎片问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于进行新生代。
实现简单、运行高效,解决了内存碎片问题。 代价是可用内存缩小为原来的一半,浪费空间。
HotSpot 把新生代划分为一块较大的 Eden 和两块较小的 Survivor,每次分配内存只使用 Eden 和其中一块 Survivor。垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor。HotSpot 默认Eden 和 Survivor 的大小比例是 8:1,即每次新生代中可用空间为整个新生代的 90%。
标记-整理算法
标记-复制算法在对象存活率高时要进行较多复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保,应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法。
老年代使用标记-整理算法,标记过程与标记-清除算法一样,但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。
标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式的。如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作,而且移动必须全程暂停用户线程。如果不移动对象就会导致空间碎片问题,只能依赖更复杂的内存分配器和访问器解决。
Q4:你知道哪些垃圾收集器?
Serial
最基础的收集器,使用复制算法、单线程工作,只用一个处理器或一条线程完成垃圾收集,进行垃圾收集时必须暂停其他所有工作线程。
Serial 是虚拟机在客户端模式的默认新生代收集器,简单高效,对于内存受限的环境它是所有收集器中额外内存消耗最小的,对于处理器核心较少的环境,Serial 由于没有线程交互开销,可获得最高的单线程收集效率。
ParNew
Serial 的多线程版本,除了使用多线程进行垃圾收集外其余行为完全一致。
ParNew 是虚拟机在服务端模式的默认新生代收集器,一个重要原因是除了 Serial 外只有它能与 CMS 配合。自从 JDK 9 开始,ParNew 加 CMS 不再是官方推荐的解决方案,官方希望它被 G1 取代。
Parallel Scavenge
新生代收集器,基于复制算法,是可并行的多线程收集器,与 ParNew 类似。
特点是它的关注点与其他收集器不同,Parallel Scavenge 的目标是达到一个可控制的吞吐量,吞吐量就是处理器用于运行用户代码的时间与处理器消耗总时间的比值。
Serial Old
Serial 的老年代版本,单线程工作,使用标记-整理算法。
Serial Old 是虚拟机在客户端模式的默认老年代收集器,用于服务端有两种用途:① JDK5 及之前与 Parallel Scavenge 搭配。② 作为CMS 失败预案。
Parellel Old
Parallel Scavenge 的老年代版本,支持多线程,基于标记-整理算法。JDK6 提供,注重吞吐量可考虑 Parallel Scavenge 加 Parallel Old。
CMS
以获取最短回收停顿时间为目标,基于标记-清除算法,过程相对复杂,分为四个步骤:初始标记、并发标记、重新标记、并发清除。
初始标记和重新标记需要 STW(Stop The World,系统停顿),初始标记仅是标记 GC Roots 能直接关联的对象,速度很快。并发标记从 GC Roots 的直接关联对象开始遍历整个对象图,耗时较长但不需要停顿用户线程。重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那部分记录。并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象,该阶段也可与用户线程并发。
缺点:① 对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量。② 无法处理浮动垃圾,有可能出现并发失败而导致 Full GC。③ 基于标记-清除算法,产生空间碎片。
G1
开创了收集器面向局部收集的设计思路和基于 Region 的内存布局,主要面向服务端,最初设计目标是替换 CMS。
G1 之前的收集器,垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆。而 G1 可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代,而是哪块内存中存放的垃圾数量最多,回收受益最大。
跟踪各 Region 里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region。这种方式保证了 G1 在有限时间内获取尽可能高的收集效率。
G1 运作过程:
- **初始标记:**标记 GC Roots 能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象。需要 STW 但耗时很短,在 Minor GC 时同步完成。
- **并发标记:**从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理 SATB 记录的在并发时有变动的对象。
- **最终标记:**对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量 SATB 记录。
- **筛选回收:**对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成。
可由用户指定期望停顿时间是 G1 的一个强大功能,但该值不能设得太低,一般设置为100~300 ms。
Q5:ZGC 了解吗?
JDK11 中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。
基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。
ZGC 的 Region 具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。
Q6:你知道哪些内存分配与回收策略?
对象优先在 Eden 区分配
大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。
大对象直接进入老年代
大对象指需要大量连续内存空间的对象,典型是很长的字符串或数量庞大的数组。大对象容易导致内存还有不少空间就提前触发垃圾收集以获得足够的连续空间。
HotSpot 提供了 -XX:PretenureSizeThreshold 参数,大于该值的对象直接在老年代分配,避免在 Eden 和 Survivor 间来回复制。
长期存活对象进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头。如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1。对象在 Survivor 中每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。对象晋升老年代的阈值可通过 -XX:MaxTenuringThreshold 设置。
动态对象年龄判定
为了适应不同内存状况,虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。
空间分配担保
MinorGC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。
如果不满足,虚拟机会查看 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次 Minor GC,否则改成一次 FullGC。
冒险是因为新生代使用复制算法,为了内存利用率只使用一个 Survivor,大量对象在 Minor GC 后仍然存活时,需要老年代进行分配担保,接收 Survivor 无法容纳的对象。
Q7:你知道哪些故障处理工具?
jps:虚拟机进程状况工具
功能和 ps 命令类似:可以列出正在运行的虚拟机进程,显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(LVMID)。LVMID 与操作系统的进程 ID(PID)一致,使用 Windows 的任务管理器或 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,必须依赖 jps 命令。
jstat:虚拟机统计信息监视工具
用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。
参数含义:S0 和 S1 表示两个 Survivor,E 表示新生代,O 表示老年代,YGC 表示 Young GC 次数,YGCT 表示 Young GC 耗时,FGC 表示 Full GC 次数,FGCT 表示 Full GC 耗时,GCT 表示 GC 总耗时。
jinfo:Java 配置信息工具
实时查看和调整虚拟机各项参数,使用 jps 的 -v 参数可以查看虚拟机启动时显式指定的参数,但如果想知道未显式指定的参数值只能使用 jinfo 的 -flag 查询。
jmap:Java 内存映像工具
用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。和 jinfo 一样,部分功能在 Windows 受限,除了生成堆转储快照的 -dump 和查看每个类实例的 -histo 外,其余选项只能在 Linux 使用。
jhat:虚拟机堆转储快照分析工具
JDK 提供 jhat 与 jmap 搭配使用分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后可以在浏览器查看。
jstack:Java 堆栈跟踪工具
用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。线程出现停顿时通过 jstack 查看各个线程的调用堆栈,可以获知没有响应的线程在后台做什么或等什么资源。
类加载机制 7
Q1:Java 程序是怎样运行的?
首先通过 Javac 编译器将 .java 转为 JVM 可加载的 .class 字节码文件。
Javac 是由 Java 编写的程序,编译过程可以分为: ① 词法解析,通过空格分割出单词、操作符、控制符等信息,形成 token 信息流,传递给语法解析器。② 语法解析,把 token 信息流按照 Java 语法规则组装成语法树。③ 语义分析,检查关键字使用是否合理、类型是否匹配、作用域是否正确等。④ 字节码生成,将前面各个步骤的信息转换为字节码。
字节码必须通过类加载过程加载到 JVM 后才可以执行,执行有三种模式,解释执行、JIT 编译执行、JIT 编译与解释器混合执行(主流 JVM 默认执行的方式)。混合模式的优势在于解释器在启动时先解释执行,省去编译时间。
之后通过即时编译器 JIT 把字节码文件编译成本地机器码。
Java 程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会认定其为"热点代码",热点代码的检测主要有基于采样和基于计数器两种方式,为了提高热点代码的执行效率,虚拟机会把它们编译成本地机器码,尽可能对代码优化,在运行时完成这个任务的后端编译器被称为即时编译器。
还可以通过静态的提前编译器 AOT 直接把程序编译成与目标机器指令集相关的二进制代码。
Q2:类加载是什么?
Class 文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程称为虚拟机的类加载机制。
与编译时需要连接的语言不同,Java 中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的。
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持 Java 的动态绑定。
Q3:类初始化的情况有哪些?
① 遇到 new、getstatic、putstatic 或 invokestatic 字节码指令时,还未初始化。典型场景包括 new 实例化对象、读取或设置静态字段、调用静态方法。
② 对类反射调用时,还未初始化。
③ 初始化类时,父类还未初始化。
④ 虚拟机启动时,会先初始化包含 main 方法的主类。
⑤ 使用 JDK7 的动态语言支持时,如果 MethodHandle 实例的解析结果为指定类型的方法句柄且句柄对应的类还未初始化。
⑥ 接口定义了默认方法,如果接口的实现类初始化,接口要在其之前初始化。
其余所有引用类型的方式都不会触发初始化,称为被动引用。被动引用实例:① 子类使用父类的静态字段时,只有父类被初始化。② 通过数组定义使用类。③ 常量在编译期会存入调用类的常量池,不会初始化定义常量的类。
接口和类加载过程的区别:初始化类时如果父类没有初始化需要初始化父类,但接口初始化时不要求父接口初始化,只有在真正使用父接口时(如引用接口中定义的常量)才会初始化。
Q4:类加载的过程是什么?
加载
该阶段虚拟机需要完成三件事:① 通过一个类的全限定类名获取定义类的二进制字节流。② 将字节流所代表的静态存储结构转化为方法区的运行时数据区。③ 在内存中生成对应该类的 Class 实例,作为方法区这个类的数据访问入口。
验证
确保 Class 文件的字节流符合约束。如果虚拟机不检查输入的字节流,可能因为载入有错误或恶意企图的字节流而导致系统受攻击。验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证重要但非必需,因为只有通过与否的区别,通过后对程序运行期没有任何影响。如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验证缩短类加载时间。
准备
为类静态变量分配内存并设置零值,该阶段进行的内存分配仅包括类变量,不包括实例变量。如果变量被 final 修饰,编译时 Javac 会为变量生成 ConstantValue 属性,准备阶段虚拟机会将变量值设为代码值。
解析
将常量池内的符号引用替换为直接引用。
符号引用以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。
直接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局相关,引用目标必须已在虚拟机的内存中存在。
初始化
直到该阶段 JVM 才开始执行类中编写的代码。准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是执行类构造方法中的 方法,该方法是 Javac 自动生成的。
Q5:有哪些类加载器?
自 JDK1.2 起 Java 一直保持三层类加载器:
在 JVM 启动时创建,负责加载最核心的类,例如 Object、System 等。无法被程序直接引用,如果需要把加载委派给启动类加载器,直接使用 null 代替即可,因为启动类加载器通常由操作系统实现,并不存在于 JVM 体系。
从 JDK9 开始从扩展类加载器更换为平台类加载器,负载加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等。
也称系统类加载器,负责加载用户类路径上的类库,可以直接在代码中使用。如果没有自定义类加载器,一般情况下应用类加载器就是默认的类加载器。自定义类加载器通过继承 ClassLoader 并重写 findClass 方法实现。
Q6:双亲委派模型是什么?
类加载器具有等级制度但非继承关系,以组合的方式复用父加载器的功能。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父加载器。
一个类加载器收到了类加载请求,它不会自己去尝试加载,而将该请求委派给父加载器,每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器,只有当父加载器反馈无法完成请求时,子加载器才会尝试。
类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性。
Q7:如何判断两个类是否相等?
任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性。
两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载,只要类加载器不同,这两个类就必定不相等。
并发 39
JMM 8
Q1:JMM 的作用是什么?
Java 线程的通信由 JMM 控制,JMM 的主要目的是定义程序中各种变量的访问规则。变量包括实例字段、静态字段,但不包括局部变量与方法参数,因为它们是线程私有的,不存在多线程竞争。JMM 遵循一个基本原则:只要不改变程序执行结果,编译器和处理器怎么优化都行。例如编译器分析某个锁只会单线程访问就消除锁,某个 volatile 变量只会单线程访问就把它当作普通变量。
JMM 规定所有变量都存储在主内存,每条线程有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。不同线程间无法直接访问对方工作内存中的变量,线程通信必须经过主内存。
关于主内存与工作内存的交互,即变量如何从主内存拷贝到工作内存、从工作内存同步回主内存,JMM 定义了 8 种原子操作:

Q2:as-if-serial 是什么?
不管怎么重排序,单线程程序的执行结果不能改变,编译器和处理器必须遵循 as-if-serial 语义。
为了遵循 as-if-serial,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执行结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
as-if-serial 把单线程程序保护起来,给程序员一种幻觉:单线程程序是按程序的顺序执行的。
Q3:happens-before 是什么?
先行发生原则,JMM 定义的两项操作间的偏序关系,是判断数据是否存在竞争的重要手段。
JMM 将 happens-before 要求禁止的重排序按是否会改变程序执行结果分为两类。对于会改变结果的重排序 JMM 要求编译器和处理器必须禁止,对于不会改变结果的重排序,JMM 不做要求。
JMM 存在一些天然的 happens-before 关系,无需任何同步器协助就已经存在。如果两个操作的关系不在此列,并且无法从这些规则推导出来,它们就没有顺序性保障,虚拟机可以对它们随意进行重排序。
- **程序次序规则:**一个线程内写在前面的操作先行发生于后面的。
- 管程锁定规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
- **volatile 规则:**对 volatile 变量的写操作先行发生于后面的读操作。
- **线程启动规则:**线程的 start 方法先行发生于线程的每个动作。
- **线程终止规则:**线程中所有操作先行发生于对线程的终止检测。
- **对象终结规则:**对象的初始化先行发生于 finalize 方法。
- **传递性:**如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C 。
Q4:as-if-serial 和 happens-before 有什么区别?
as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。
这两种语义的目的都是为了在不改变程序执行结果的前提下尽可能提高程序执行并行度。
Q5:什么是指令重排序?
为了提高性能,编译器和处理器通常会对指令进行重排序,重排序指从源代码到指令序列的重排序,分为三种:① 编译器优化的重排序,编译器在不改变单线程程序语义的前提下可以重排语句的执行顺序。② 指令级并行的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。③ 内存系统的重排序。
Q6:原子性、可见性、有序性分别是什么?
原子性
基本数据类型的访问都具备原子性,例外就是 long 和 double,虚拟机将没有被 volatile 修饰的 64 位数据操作划分为两次 32 位操作。
如果应用场景需要更大范围的原子性保证,JMM 还提供了 lock 和 unlock 操作满足需求,尽管 JVM 没有把这两种操作直接开放给用户使用,但是提供了更高层次的字节码指令 monitorenter 和 monitorexit,这两个字节码指令反映到 Java 代码中就是 synchronized。
可见性
可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。JMM 通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新的方式实现可见性,无论普通变量还是 volatile 变量都是如此,区别是 volatile 保证新值能立即同步到主内存以及每次使用前立即从主内存刷新。
除了 volatile 外,synchronized 和 final 也可以保证可见性。同步块可见性由"对一个变量执行 unlock 前必须先把此变量同步回主内存,即先执行 store 和 write"这条规则获得。final 的可见性指:被 final 修饰的字段在构造方法中一旦初始化完成,并且构造方法没有把 this 引用传递出去,那么其他线程就能看到 final 字段的值。
有序性
有序性可以总结为:在本线程内观察所有操作是有序的,在一个线程内观察另一个线程,所有操作都是无序的。前半句指 as-if-serial 语义,后半句指指令重排序和工作内存与主内存延迟现象。
Java 提供 volatile 和 synchronized 保证有序性,volatile 本身就包含禁止指令重排序的语义,而 synchronized 保证一个变量在同一时刻只允许一条线程对其进行 lock 操作,确保持有同一个锁的两个同步块只能串行进入。
Q7:谈一谈 volatile
JMM 为 volatile 定义了一些特殊访问规则,当变量被定义为 volatile 后具备两种特性:
当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。volatile 变量在各个线程的工作内存中不存在一致性问题,但 Java 的运算操作符并非原子操作,导致 volatile 变量运算在并发下仍不安全。
使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,后面的指令不能重排到内存屏障之前。
使用 lock 前缀引发两件事:① 将当前处理器缓存行的数据写回系统内存。②使其他处理器的缓存无效。相当于对缓存变量做了一次 store 和 write 操作,让 volatile 变量的修改对其他处理器立即可见。
静态变量 i 执行多线程 i++ 的不安全问题
自增语句由 4 条字节码指令构成的,依次为 getstatic、iconst_1、iadd、putstatic,当 getstatic 把 i 的值取到操作栈顶时,volatile 保证了 i 值在此刻正确,但在执行 iconst_1、iadd 时,其他线程可能已经改变了 i 值,操作栈顶的值就变成了过期数据,所以 putstatic 执行后就可能把较小的 i 值同步回了主内存。
适用场景
① 运算结果并不依赖变量的当前值。② 一写多读,只有单一的线程修改变量值。
内存语义
写一个 volatile 变量时,把该线程工作内存中的值刷新到主内存。
读一个 volatile 变量时,把该线程工作内存值置为无效,从主内存读取。
指令重排序特点
第二个操作是 volatile 写,不管第一个操作是什么都不能重排序,确保写之前的操作不会被重排序到写之后。
第一个操作是 volatile 读,不管第二个操作是什么都不能重排序,确保读之后的操作不会被重排序到读之前。
第一个操作是 volatile 写,第二个操作是 volatile 读不能重排序。
JSR-133 增强 volatile 语义的原因
在旧的内存模型中,虽然不允许 volatile 变量间重排序,但允许 volatile 变量与普通变量重排序,可能导致内存不可见问题。JSR-133 严格限制编译器和处理器对 volatile 变量与普通变量的重排序,确保 volatile 的写-读和锁的释放-获取具有相同的内存语义。
Q8:final 可以保证可见性吗?
final 可以保证可见性,被 final 修饰的字段在构造方法中一旦被初始化完成,并且构造方法没有把 this 引用传递出去,在其他线程中就能看见 final 字段值。
在旧的 JMM 中,一个严重缺陷是线程可能看到 final 值改变。比如一个线程看到一个 int 类型 final 值为 0,此时该值是未初始化前的零值,一段时间后该值被某线程初始化,再去读这个 final 值会发现值变为 1。
为修复该漏洞,JSR-133 为 final 域增加重排序规则:只要对象是正确构造的(被构造对象的引用在构造方法中没有逸出),那么不需要使用同步就可以保证任意线程都能看到这个 final 域初始化后的值。
写 final 域重排序规则
禁止把 final 域的写重排序到构造方法之外,编译器会在 final 域的写后,构造方法的 return 前,插入一个 Store Store 屏障。确保在对象引用为任意线程可见之前,对象的 final 域已经初始化过。
读 final 域重排序规则
在一个线程中,初次读对象引用和初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作。编译器在读 final 域操作的前面插入一个 Load Load 屏障,确保在读一个对象的 final 域前一定会先读包含这个 final 域的对象引用。
内容太多了,由于平台的限制无法全部发不出来,可以点赞这篇文章+关注小编后私信 “面试”免费获取这套293页java核心知识点的文档。

你可能感兴趣的:(java)
- Java程序员教你春招如何一击即中
小韩学长yyds
java求职春招
✨✨✨这里是小韩学长yyds的BLOG(喜欢作者的点个关注吧)✨✨✨想要了解更多内容可以访问我的主页小韩学长yyds-CSDN博客目录春招“战场”,投递是“第一枪”知己知彼,百战不殆剖析春招形势解读企业需求打造吸睛简历明确求职意向突出专业技能优化项目经验其他要点投递策略与技巧选择合适渠道把握投递时间注意事项面试准备与应对基础知识复习项目经验梳理模拟面试面试技巧案例分析成功案例失败案例春招“战场”,
- 为什么需要用stringbuffer或者StringBuilder去拼接字符串——而不用string——以及stringbuffer的基本了解
long_far
javastring
前言今天博主将为大家分享:Java基础之String:为什么需要用stringbuffer或者StringBuilder去拼接字符串——而不用string——以及stringbuffer的基本了解!不喜勿喷,如有异议欢迎讨论!简述首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder>StringBuffer>StringString最慢的原因:String为字符串常
- Geojson大文件读取
fishinair123
javajsonjvm
背景:在项目中有一个geojson文件47G多;使用将geojson加载到内存方法,对内存要求较大,导致加载失败解决:使用Jackson库读取大型GeoJSON文件的步骤:导入Jackson库:在您的Java项目中,首先需要导入Jackson库的相关依赖项。您可以使用Maven或Gradle等构建工具来管理这些依赖项,或手动下载和添加库文件。创建ObjectMapper对象:ObjectMappe
- Java直通车系列24【Spring Boot】(项目创建)
浪九天
Java直通车springbootspring开发语言java
目录使用SpringInitializr创建项目1.概念2.详细步骤步骤1:访问SpringInitializr网站步骤2:配置项目元数据步骤3:添加依赖步骤4:生成项目3.场景示例使用IDE插件创建项目1.概念2.详细步骤(以IntelliJIDEA为例)步骤1:打开IntelliJIDEA步骤2:选择项目类型步骤3:配置项目元数据步骤4:添加依赖步骤5:完成项目创建3.场景示例手动创建项目1.
- 分片(sharding)和复制(replication)是怎样工作的?思维导图 代码示例(java 架构)
用心去追梦
java架构开发语言
分片(Sharding)和复制(Replication)是MongoDB中用于提升性能、可用性和数据冗余的关键特性。它们各自解决不同的问题,并且可以一起使用以提供更强大的数据库解决方案。分片与复制的工作原理-思维导图概要您可以创建一个以“MongoDB分片与复制”为核心节点的思维导图,并根据以下分类展开:分片(Sharding)定义将数据分布在多个服务器上目的水平扩展存储容量提高读写吞吐量组件配置
- 计算机毕业设计springboot晋中学院失物招领系统的设计与实现unst3源码+系统+程序+lw文档+部署
呦呦网络
springbootjavamysql
计算机毕业设计springboot晋中学院失物招领系统的设计与实现unst3源码+系统+程序+lw文档+部署计算机毕业设计springboot晋中学院失物招领系统的设计与实现unst3源码+系统+程序+lw文档+部署本源码技术栈:项目架构:B/S架构开发语言:Java语言开发软件:ideaeclipse前端技术:Layui、HTML、CSS、JS、JQuery等技术后端技术:JAVA运行环境:Wi
- Hutool DateUtil 工具类用法详解:简化Java日期操作
嘵奇
提升自己java开发语言
精心整理了最新的面试资料和简历模板,有需要的可以自行获取点击前往百度网盘获取点击前往夸克网盘获取HutoolDateUtil工具类用法详解:简化Java日期操作Hutool是一个Java工具库,其DateUtil类封装了日期处理的常用操作,能够极大简化开发中对日期时间的转换、计算和格式化等需求。以下是DateUtil的核心功能及示例代码。一、快速开始引入Hutool依赖(Maven):cn.hut
- 深度解析前端页面性能优化
冬冬小圆帽
前端性能优化
1.优化页面加载性能1.1减少HTTP请求问题:过多的HTTP请求会增加页面加载时间。解决方案:合并CSS和JavaScript文件。使用CSSSprites合并小图标。使用字体图标(如FontAwesome)代替图片图标。代码示例:合并CSS和JavaScript1.2使用CDN加速静态资源加载问题:静态资源加载速度受服务器地理位置影响。解决方案:将静态资源(如图片、CSS、JavaScript
- 计算机毕业设计JAVA人职匹配推荐系统mybatis+源码+调试部署+系统+数据库+lw
诺诺网络
javamybatis开发语言
计算机毕业设计JAVA人职匹配推荐系统mybatis+源码+调试部署+系统+数据库+lw计算机毕业设计JAVA人职匹配推荐系统mybatis+源码+调试部署+系统+数据库+lw本源码技术栈:项目架构:B/S架构开发语言:Java语言开发软件:ideaeclipse前端技术:Layui、HTML、CSS、JS、JQuery等技术后端技术:JAVA运行环境:Win10、JDK1.8数据库:MySQL5
- 第八课:性能优化与高并发处理方案
deming_su
Nodejs性能优化node.jsnginx
Node.js作为一种基于事件驱动、非阻塞I/O模型的JavaScript运行环境,广泛应用于高并发、实时性强的应用开发。然而,随着业务需求的不断增长,如何在Node.js中高效地处理大量并发请求、优化性能,成为了开发者必须面对的重要课题。本文将详细介绍Node.js性能优化与高并发处理的几种关键方案,包括使用Cluster模块多进程优化、Redis缓存加速数据查询、负载均衡与Nginx反向代理,
- Java集合_八股&场景题
吃海鲜的骆驼
面试专题java开发语言
Java集合在Java开发中,集合框架是面试和实际开发中非常重要的内容。以下是一些常见的Java集合八股文问题和场景题,以及详细答案和示例代码。1.Java集合框架的结构是什么?答案:Java集合框架主要分为三大接口:Collection、Map和Queue。Collection:是集合的顶级接口,它又分为List和Set。List:有序集合,允许重复元素,如ArrayList、LinkedLis
- Java多线程和锁_八股&场景题
吃海鲜的骆驼
面试专题java开发语言
Java多线程_八股&场景题Java多线程是面试和实际开发中非常重要的内容。以下是一些常见的Java多线程八股文问题和场景题,以及详细答案和示例代码。1.Java中创建线程的几种方式?答案:主要有以下几种方式:继承Thread类:重写run()方法,通过start()启动线程。实现Runnable接口:实现run()方法,通过Thread类启动线程。实现Callable接口:通过FutureTas
- JVM常见八股问题
学Java的skyyyyyyyy
jvmjava
1.什么是JVM?回答:JVM是Java虚拟机,负责将Java字节码转换为机器码并执行。它提供了内存管理、垃圾回收、线程管理等功能,使得Java程序能够在不同操作系统上运行而无需修改。2.JVM的内存结构是什么?JVM内存结构主要包括以下几个部分:方法区:存储类的结构信息,如类名、访问修饰符、常量池、字段描述等。堆区:存储对象实例,几乎所有的对象都在这里分配内存。栈区:每个线程都有一个栈帧,用于存
- Java集合
ok!不当人
javawindowspython
集合1.Collection接口1.1List接口1.1.1ArrayList常用方法CollectionListArrayList常用方法:add(Ee)添加元素添加在末尾add(intindex,Ee)在指定位置添加元素clear()从列表中删除所有元素。contains(Objecto)如果此列表包含指定的元素,则返回true。get(intindex)返回此列表中指定位置的元素。index
- JVM如何判断一个对象可以被回收
代码小白%
jvm
JVM是Java虚拟机(JavaVirtualMachine)的缩写,它是一个能够运行Java字节码的虚拟计算机。JVM是Java平台的一部分,它提供了平台独立性,即Java程序可以一次编写,在任何支持JVM的操作系统上运行。JVM主要有以下功能:加载代码:JVM加载编译好的字节码文件(.class文件)并将其转换为内存中的类。验证字节码:JVM会验证字节码,确保它遵循Java规范,并不会破坏系统
- 手把手教你将 DeepSeek 集成到 Java 的 Spring Boot 项目中
Java_young
AIjavaspringboot开发语言
前面阳仔给大家介绍了如何将deepseek结合进入办公软件中,今天阳仔将详细介绍如何将DeepSeek集成到Java的SpringBoot项目中,让你的项目具备智能交互能力。一、准备工作1.注册DeepSeek账号并获取APIKey访问DeepSeek官网,点击右上角的“开放平台”,注册或登录账号。在左侧菜单中点击“APIKeys”,点击“创建APIKey”,为APIKey命名(如“test”),
- java栈的实现
晴天ノBye~
java数据结构
目录栈的介绍两种方法实现栈1.数组栈数组的构造:入栈操作出栈:打印数组栈2.链栈链栈的结点构造链栈入栈操作链表的出栈操作链栈的打印总结:附录栈的介绍栈是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。”特点:先进后出。如图入栈:第一个元素在栈底,最后以个入栈的在栈顶出栈出栈是移出栈顶元素两种方
- 面试__八股
big muscle
Java面试linuxjvm
java解释执行,即时编译(编译成本地代码,效率高,内存占用多,C1,C2)编译优化技术:方法内联,复写传播,无用代码删除字节码就是虚拟机所使用的指令集类加载:加载(获取类的二进制流文件,方法区的数据结构,内存生成这个类的class对象),链接(验证,准备(类变量初始化),解析(符号引用替换为直接引用)),初始化(常量初始化)栈帧主要是局部变量区和操作数栈,方法返回地址,对象头的markword;
- 【Javascript网页设计】在线图片画板案例
Netsuite + 影刀
Javascript案例javascript
代码如下:在线画板body{text-align:center;font-family:Arial,sans-serif;margin:20px;}.toolbar{margin-bottom:10px;}canvas{border:2pxsolid#000;cursor:crosshair;}button{margin:5px;padding:8px15px;border:none;backgr
- 第十一届蓝桥杯总结(广东省赛区一等奖、全国总决赛二等奖)
可乐学算法
思考-总结-感悟蓝桥杯ACM算法
其实这本来是上半年的比赛,由于疫情就拖到了下半年,一共本来有四五场比赛的,好多都参加不了,就只剩下了蓝桥杯和天梯赛,今年真的太难了,一个疫情打乱了好多计划。本来是抱着拿javab组国特去的,无奈最后拿了个国二,省赛发挥得不好,但省一的排名还是比较前,国赛感觉发挥一般般,没想到拿了个国二。 接下来说下备赛,大四的时候基本没怎么备赛,不过还是经常上leetcode刷题,刷那些经典算法的题目,比如
- 探索 Java 中的线程池自定义技巧:高效、灵活地管理并发任务!
魔道不误砍柴功
Java使用与案例分享java开发语言
文章目录为什么需要自定义线程池?1.基本步骤:使用`ThreadPoolExecutor`构建线程池2.合理设置核心线程数与最大线程数实例:创建一个适合IO密集型任务的线程池3.选择合适的任务队列实例:使用`ArrayBlockingQueue`防止任务过多时占用过多内存4.巧妙利用线程工厂自定义线程5.选择合适的拒绝策略实例:使用`CallerRunsPolicy`降低拒绝任务的发生6.监控线程
- 从 “12.3” 的崩溃到完美的解决方案:一场类型选择的冒险之旅
小丁学Java
产品资质管理系统javamysqlbigdecimaldecimal
从“12.3”的崩溃到完美的解决方案:一场类型选择的冒险之旅“程序员的代码就像人生的旅途,总有那么几个坑等着你跳进去,然后爬出来时满身泥泞却笑得灿烂。”——某不愿透露姓名的开发者今天,我们要聊聊一个看似不起眼却能让人抓狂的问题:参数解析失败,以及它背后引发的类型选择大戏。从Java的Integer到MySQL的VARCHAR,再到BigDecimal的登场,这不仅是一次技术debug的过程,更是一
- 快来收藏,欢迎打卡,编程自学成长指南
猫咪薄荷
算法数据结构线性回归链表排序算法动态规划leetcode
自学编程是一段充满挑战和成就感的旅程。为了帮助初学者顺利起步,以下是一个全面的编程自学成长指南,涵盖从语言选择到实践项目的各个方面,助您在编程之路上稳步前行。1.确定学习目标首先,明确您学习编程的目的。这有助于选择合适的编程语言和学习路径。•Web开发:如果您对构建网站感兴趣,建议学习HTML、CSS和JavaScript等语言。•数据分析:对于数据处理和分析,Python是一个强大的工具。•移动
- 计算机学习的五大避坑指南新手必藏
编程诗人华仔
架构设计学习java软件工程golang开发语言c语言python
避坑一:盲目跟风学习编程语言技术性深化:在选择编程语言时,要考虑语言的特点、适用场景以及未来发展趋势。例如,Java适合企业级应用开发,Python在数据分析和人工智能领域有广泛应用,而JavaScript则是前端开发的基础。明确自己的职业发展方向,选择与之匹配的语言进行深入学习。避坑二:忽视基础知识的掌握技术性深化:深入理解算法如排序、搜索等,熟悉常见的数据结构如链表、栈、队列、树等,并掌握设计
- 我与Java注解的二十年:从手写标签到架构艺术的蜕变之旅
双囍菜菜
Javajava架构python
我与Java注解的二十年:从手写标签到架构艺术的蜕变之旅题记:“好的注解不是代码的累赘,而是程序员留给未来的情书”——一个用坏七把机械键盘的老码农文章目录我与Java注解的二十年:从手写标签到架构艺术的蜕变之旅一、开篇:注解的本质是时间管理二、筑基篇:手把手打造你的第一个注解2.1从生活到代码:注解的DNA解析2.2解剖注解:元注解是基因编码三、进阶篇:让注解动起来3.1反射:注解的神经系统3.2
- Java 中操作 R:深度整合与高效应用
froginwe11
开发语言
Java中操作R:深度整合与高效应用引言随着大数据和机器学习的快速发展,R语言在数据分析和可视化方面扮演着越来越重要的角色。而Java作为一种广泛应用于企业级应用开发的语言,其强大的功能和稳定性使其成为构建高性能应用的首选。本文将探讨Java如何操作R语言,实现高效的数据分析应用。一、Java操作R的背景R语言优势:R语言拥有丰富的统计分析、数据可视化工具和机器学习算法库,是数据分析领域的首选语言
- SOAP与NETCONF:协议特性、场景与应用全景解析
leo·li
SOAPNETCONF
在分布式系统和网络管理领域,SOAP与NETCONF是两类关键协议,它们看似都与“数据传输”相关,但设计理念和应用场景截然不同一、协议定位:跨平台信使与网络配置专家1.SOAP:异构系统的“标准化信使”核心角色SOAP(简单对象访问协议)如同一位精通多国语言的翻译官,专注于解决异构系统间的通信难题。它基于XML定义了一套严格的通信规则,允许Java、.NET、Python等不同技术栈的系统无缝交互
- PTA里面怎么寻找JAVA题目_PTA基础题目集
weixin_39955829
PTA里面怎么寻找JAVA题目
Java代码在上面,下面空两行是python3A/_5T$|#c-t4`%@,S9I7-1厘米换算英尺英寸(15分)0y;n0J%u7p如果已知英制长度的英尺foot和英寸inch的值,那么对应的米是(foot+inch/12)×0.3048。现在,如果用户输入的是厘米数,那么对应英制长度的英尺和英寸是多少呢?别忘了1英尺等于12英寸。0v:C6Z1W/T输入格式:/`"x^7l5J4x1i#{1
- JAVA反射
m0_74015467
javapython开发语言
一、反射概述Java反射(Reflection)是Java语言中一项极为强大的特性,它犹如一把神奇的钥匙,允许程序在运行时查询、访问和修改类、接口、字段和方法的信息。简单来说,反射赋予了程序动态操作类的能力,打破了传统编程中在编译期就确定所有行为的限制,让程序能够根据运行时的情况灵活地做出响应。二、类信息详解(一)类信息的构成类信息主要存储在JVM的方法区中,它是类的元数据集合,具体包含以下几个重
- 力扣热题 100:图论专题经典题解析
剑走偏锋o.O
leetcode图论算法java学习笔记
文章目录一、岛屿数量(题目200)1.题目描述2.示例3.解题思路4.代码实现(Java)5.复杂度分析二、腐烂的橘子(题目994)1.题目描述2.示例3.解题思路4.代码实现(Java)5.复杂度分析三、课程表(题目207)1.题目描述2.示例3.解题思路4.代码实现(Java)5.复杂度分析四、实现Trie(前缀树)(题目208)1.题目描述2.示例3.解题思路4.代码实现(Java)5.复杂
- 开发者关心的那些事
圣子足道
ios游戏编程apple支付
我要在app里添加IAP,必须要注册自己的产品标识符(product identifiers)。产品标识符是什么?
产品标识符(Product Identifiers)是一串字符串,它用来识别你在应用内贩卖的每件商品。App Store用产品标识符来检索产品信息,标识符只能包含大小写字母(A-Z)、数字(0-9)、下划线(-)、以及圆点(.)。你可以任意排列这些元素,但我们建议你创建标识符时使用
- 负载均衡器技术Nginx和F5的优缺点对比
bijian1013
nginxF5
对于数据流量过大的网络中,往往单一设备无法承担,需要多台设备进行数据分流,而负载均衡器就是用来将数据分流到多台设备的一个转发器。
目前有许多不同的负载均衡技术用以满足不同的应用需求,如软/硬件负载均衡、本地/全局负载均衡、更高
- LeetCode[Math] - #9 Palindrome Number
Cwind
javaAlgorithm题解LeetCodeMath
原题链接:#9 Palindrome Number
要求:
判断一个整数是否是回文数,不要使用额外的存储空间
难度:简单
分析:
题目限制不允许使用额外的存储空间应指不允许使用O(n)的内存空间,O(1)的内存用于存储中间结果是可以接受的。于是考虑将该整型数反转,然后与原数字进行比较。
注:没有看到有关负数是否可以是回文数的明确结论,例如
- 画图板的基本实现
15700786134
画图板
要实现画图板的基本功能,除了在qq登陆界面中用到的组件和方法外,还需要添加鼠标监听器,和接口实现。
首先,需要显示一个JFrame界面:
public class DrameFrame extends JFrame { //显示
- linux的ps命令
被触发
linux
Linux中的ps命令是Process Status的缩写。ps命令用来列出系统中当前运行的那些进程。ps命令列出的是当前那些进程的快照,就是执行ps命令的那个时刻的那些进程,如果想要动态的显示进程信息,就可以使用top命令。
要对进程进行监测和控制,首先必须要了解当前进程的情况,也就是需要查看当前进程,而 ps 命令就是最基本同时也是非常强大的进程查看命令。使用该命令可以确定有哪些进程正在运行
- Android 音乐播放器 下一曲 连续跳几首歌
肆无忌惮_
android
最近在写安卓音乐播放器的时候遇到个问题。在MediaPlayer播放结束时会回调
player.setOnCompletionListener(new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mp.reset();
Log.i("H
- java导出txt文件的例子
知了ing
javaservlet
代码很简单就一个servlet,如下:
package com.eastcom.servlet;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.Resu
- Scala stack试玩, 提高第三方依赖下载速度
矮蛋蛋
scalasbt
原文地址:
http://segmentfault.com/a/1190000002894524
sbt下载速度实在是惨不忍睹, 需要做些配置优化
下载typesafe离线包, 保存为ivy本地库
wget http://downloads.typesafe.com/typesafe-activator/1.3.4/typesafe-activator-1.3.4.zip
解压r
- phantomjs安装(linux,附带环境变量设置) ,以及casperjs安装。
alleni123
linuxspider
1. 首先从官网
http://phantomjs.org/下载phantomjs压缩包,解压缩到/root/phantomjs文件夹。
2. 安装依赖
sudo yum install fontconfig freetype libfreetype.so.6 libfontconfig.so.1 libstdc++.so.6
3. 配置环境变量
vi /etc/profil
- JAVA IO FileInputStream和FileOutputStream,字节流的打包输出
百合不是茶
java核心思想JAVA IO操作字节流
在程序设计语言中,数据的保存是基本,如果某程序语言不能保存数据那么该语言是不可能存在的,JAVA是当今最流行的面向对象设计语言之一,在保存数据中也有自己独特的一面,字节流和字符流
1,字节流是由字节构成的,字符流是由字符构成的 字节流和字符流都是继承的InputStream和OutPutStream ,java中两种最基本的就是字节流和字符流
类 FileInputStream
- Spring基础实例(依赖注入和控制反转)
bijian1013
spring
前提条件:在http://www.springsource.org/download网站上下载Spring框架,并将spring.jar、log4j-1.2.15.jar、commons-logging.jar加载至工程1.武器接口
package com.bijian.spring.base3;
public interface Weapon {
void kil
- HR看重的十大技能
bijian1013
提升能力HR成长
一个人掌握何种技能取决于他的兴趣、能力和聪明程度,也取决于他所能支配的资源以及制定的事业目标,拥有过硬技能的人有更多的工作机会。但是,由于经济发展前景不确定,掌握对你的事业有所帮助的技能显得尤为重要。以下是最受雇主欢迎的十种技能。 一、解决问题的能力 每天,我们都要在生活和工作中解决一些综合性的问题。那些能够发现问题、解决问题并迅速作出有效决
- 【Thrift一】Thrift编译安装
bit1129
thrift
什么是Thrift
The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and s
- 【Avro三】Hadoop MapReduce读写Avro文件
bit1129
mapreduce
Avro是Doug Cutting(此人绝对是神一般的存在)牵头开发的。 开发之初就是围绕着完善Hadoop生态系统的数据处理而开展的(使用Avro作为Hadoop MapReduce需要处理数据序列化和反序列化的场景),因此Hadoop MapReduce集成Avro也就是自然而然的事情。
这个例子是一个简单的Hadoop MapReduce读取Avro格式的源文件进行计数统计,然后将计算结果
- nginx定制500,502,503,504页面
ronin47
nginx 错误显示
server {
listen 80;
error_page 500/500.html;
error_page 502/502.html;
error_page 503/503.html;
error_page 504/504.html;
location /test {return502;}}
配置很简单,和配
- java-1.二叉查找树转为双向链表
bylijinnan
二叉查找树
import java.util.ArrayList;
import java.util.List;
public class BSTreeToLinkedList {
/*
把二元查找树转变成排序的双向链表
题目:
输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。
要求不能创建任何新的结点,只调整指针的指向。
10
/ \
6 14
/ \
- Netty源码学习-HTTP-tunnel
bylijinnan
javanetty
Netty关于HTTP tunnel的说明:
http://docs.jboss.org/netty/3.2/api/org/jboss/netty/channel/socket/http/package-summary.html#package_description
这个说明有点太简略了
一个完整的例子在这里:
https://github.com/bylijinnan
- JSONUtil.serialize(map)和JSON.toJSONString(map)的区别
coder_xpf
jqueryjsonmapval()
JSONUtil.serialize(map)和JSON.toJSONString(map)的区别
数据库查询出来的map有一个字段为空
通过System.out.println()输出 JSONUtil.serialize(map): {"one":"1","two":"nul
- Hibernate缓存总结
cuishikuan
开源sshjavawebhibernate缓存三大框架
一、为什么要用Hibernate缓存?
Hibernate是一个持久层框架,经常访问物理数据库。
为了降低应用程序对物理数据源访问的频次,从而提高应用程序的运行性能。
缓存内的数据是对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻或事件会同步缓存和物理数据源的数据。
二、Hibernate缓存原理是怎样的?
Hibernate缓存包括两大类:Hib
- CentOs6
dalan_123
centos
首先su - 切换到root下面1、首先要先安装GCC GCC-C++ Openssl等以来模块:yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel2、再安装ncurses模块yum -y install ncurses-develyum install ncurses-devel3、下载Erang
- 10款用 jquery 实现滚动条至页面底端自动加载数据效果
dcj3sjt126com
JavaScript
无限滚动自动翻页可以说是web2.0时代的一项堪称伟大的技术,它让我们在浏览页面的时候只需要把滚动条拉到网页底部就能自动显示下一页的结果,改变了一直以来只能通过点击下一页来翻页这种常规做法。
无限滚动自动翻页技术的鼻祖是微博的先驱:推特(twitter),后来必应图片搜索、谷歌图片搜索、google reader、箱包批发网等纷纷抄袭了这一项技术,于是靠滚动浏览器滚动条
- ImageButton去边框&Button或者ImageButton的背景透明
dcj3sjt126com
imagebutton
在ImageButton中载入图片后,很多人会觉得有图片周围的白边会影响到美观,其实解决这个问题有两种方法
一种方法是将ImageButton的背景改为所需要的图片。如:android:background="@drawable/XXX"
第二种方法就是将ImageButton背景改为透明,这个方法更常用
在XML里;
<ImageBut
- JSP之c:foreach
eksliang
jspforearch
原文出自:http://www.cnblogs.com/draem0507/archive/2012/09/24/2699745.html
<c:forEach>标签用于通用数据循环,它有以下属性 属 性 描 述 是否必须 缺省值 items 进行循环的项目 否 无 begin 开始条件 否 0 end 结束条件 否 集合中的最后一个项目 step 步长 否 1
- Android实现主动连接蓝牙耳机
gqdy365
android
在Android程序中可以实现自动扫描蓝牙、配对蓝牙、建立数据通道。蓝牙分不同类型,这篇文字只讨论如何与蓝牙耳机连接。
大致可以分三步:
一、扫描蓝牙设备:
1、注册并监听广播:
BluetoothAdapter.ACTION_DISCOVERY_STARTED
BluetoothDevice.ACTION_FOUND
BluetoothAdapter.ACTION_DIS
- android学习轨迹之四:org.json.JSONException: No value for
hyz301
json
org.json.JSONException: No value for items
在JSON解析中会遇到一种错误,很常见的错误
06-21 12:19:08.714 2098-2127/com.jikexueyuan.secret I/System.out﹕ Result:{"status":1,"page":1,&
- 干货分享:从零开始学编程 系列汇总
justjavac
编程
程序员总爱重新发明轮子,于是做了要给轮子汇总。
从零开始写个编译器吧系列 (知乎专栏)
从零开始写一个简单的操作系统 (伯乐在线)
从零开始写JavaScript框架 (图灵社区)
从零开始写jQuery框架 (蓝色理想 )
从零开始nodejs系列文章 (粉丝日志)
从零开始编写网络游戏
- jquery-autocomplete 使用手册
macroli
jqueryAjax脚本
jquery-autocomplete学习
一、用前必备
官方网站:http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
当前版本:1.1
需要JQuery版本:1.2.6
二、使用
<script src="./jquery-1.3.2.js" type="text/ja
- PLSQL-Developer或者Navicat等工具连接远程oracle数据库的详细配置以及数据库编码的修改
超声波
oracleplsql
在服务器上将Oracle安装好之后接下来要做的就是通过本地机器来远程连接服务器端的oracle数据库,常用的客户端连接工具就是PLSQL-Developer或者Navicat这些工具了。刚开始也是各种报错,什么TNS:no listener;TNS:lost connection;TNS:target hosts...花了一天的时间终于让PLSQL-Developer和Navicat等这些客户
- 数据仓库数据模型之:极限存储--历史拉链表
superlxw1234
极限存储数据仓库数据模型拉链历史表
在数据仓库的数据模型设计过程中,经常会遇到这样的需求:
1. 数据量比较大; 2. 表中的部分字段会被update,如用户的地址,产品的描述信息,订单的状态等等; 3. 需要查看某一个时间点或者时间段的历史快照信息,比如,查看某一个订单在历史某一个时间点的状态, 比如,查看某一个用户在过去某一段时间内,更新过几次等等; 4. 变化的比例和频率不是很大,比如,总共有10
- 10点睛Spring MVC4.1-全局异常处理
wiselyman
spring mvc
10.1 全局异常处理
使用@ControllerAdvice注解来实现全局异常处理;
使用@ControllerAdvice的属性缩小处理范围
10.2 演示
演示控制器
package com.wisely.web;
import org.springframework.stereotype.Controller;
import org.spring