2018-10-13

מה *לא* לימדו אותנו באוניברסיטה על מבני-נתונים?

קורס מבני-נתונים היה אחד מהקורסים פוקחי העיניים ביותר עבורי באוניברסיטה.
לימדו אותי שם להסתכל על בעיות בצורה, שכנראה שלא הייתי מסוגל להסיק בעצמי. זה היה פשוט מצוין!

עם השנים, גיליתי שהדברים בפועל עובדים קצת אחרת. שלא תבינו לא נכון: התאוריה היא חשובה מאוד, בלי לפשט את הדברים - קשה להתמקד בעיקר.

עדיין היה חסר לי רק שיעור אחד בקורס, שיעור שיכין אותי לעולם האמיתי. השיעור הזה היה יכול כנראה להיות שיעור חשוב מכל אחד מהשיעורים האחרים בקורס - ולכן חבל לי מאוד שהוא לא ניתן.

לאחר כמעט 20 שנה מהיום שבו התחלתי את קורס "מבני-נתונים" אני חוזר ומגיש לכם את השיעור הזה בקצרה. אני נתקל שוב ושוב באנשים שעדיין לא למדו אותו, ומקווה שהפוסט יעזור לצמצם את פער-הידע.


הכלל הראשון שארצה להדגיש הוא זה:

כלל חשוב לחיים המקצועיים!

כדי להסביר את הכלל, אפתח בדוגמה משעשעת של אלגוריתם חיפוש בשם Sleep Sort:

ע״פ האלגוריתם הזה, יש רשימת מקור (הקלט) ורשימת יעד (התוצאה). בעת ההרצה אנו סורקים את רשימת המקור ולכל איבר מפעילים פונקציה (למשל: co-routine) שתכניס את האיבר לרשימת היעד בעוד n שניות, כאשר n הוא גודל האיבר.

אם רשימת המקור היא 2, 4, ו 3 אזי האיבר 2 יכנס לרשימת היעד לאחר שתי שניות, האיבר ארבע לאחר 4 שניות, והאיבר 3 - לאחר 3 שניות. והנה ביצענו מיון!

ע״פ גישה תאורטית פשטנית, זמן הריצה של האלגוריתם הוא (O(n - כי בחנו כל איבר רק פעם אחת. לא התייחסנו למחיר זמן ההמתנה (sleep) - מה שבעצם הופך את היוצרות.

למשל, עבור קלט קלט של המספרים 2 ו 1,000,000,000 האלגוריתם ירוץ במשך  כמעט 32 שנים - מה שבהחלט פחות יעיל אפילו מ Bubble Sort. 


מה היה לנו כאן? מחיר אמיתי מאוד, ומשמעותי מאוד, שלא לקחנו בחשבון בהערכת הזמן של האלגוריתם. יכולנו בהתאם לשכלל את התאוריה שלנו ולנסח זמן ריצה בנוסח:
(O(max(item_size) * sec) +n - כאשר בעיקרון אפשר להזניח את n.



באופן דומה (והרבה פחות קיצוני) ניתן לומר שיש מחירים נוספים ומשמעותיים שלא נלקחים בחשבון בחלק גדול ממבני-הנתונים שלמדנו עליהם:
  • HashTable
  • LinkedList
  • Binary Search Tree
  • Graphs
בהמשך הפוסט אסביר את המחיר הנוסף, ועוד כמה תובנות שכדאי להכיר בהקשר למבני-נתונים.



חוק המספרים הקטנים


עיקרון חשוב ומעשי מאוד הוא חוק המספרים הקטנים (המצאתי את השם כרגע - אבל העיקרון קיים מאז ומעולם).


אם אנחנו מסתכלים על זמני הריצה התאורטיים שאנו נוהגים להסתכל עליהם, תחת ההקשר ש CPU בימנו מבצע מיליארדי cycles בשנייה, אזי עבור מאות או אולי אלפי איברים - סיבוכיות האלגוריתם עד לרמת nlogn - לא ממש משנה:


בהמשך נראה, כשאנו מדברים על המחירים הנוספים של זמני ביצוע - הם בד"כ מחזקים את חוק המספרים הקטנים.

כלומר: אם אתם עוסקים בעשרות, מאות, או אפילו אלפי איברים - סיבוכיות האלגוריתם לא ממש משנה. כל גישה בודדת לדיסק או לרשת - תעלה הרבה יותר.

את הכלל הבסיסי הזה, מפתחים נוהגים לשכוח תוך כדי שהם מבזבזים זמן פיתוח יקר וקריאות (readability) של קוד - על מנת לשפר, גם עבור עשרות איברים, את סיבוכיות האלגוריתם. 


חשוב לציין שלא כל "פקודת מחשב" מתבצעת ע"י cycle בודד של מעבד. ליהפך: תנאי if פשוט לכאורה, עלול בקלות להתתרגם לעשרות ומאות CPU cycles. כאמצעי ביטחון אפשר לחשוב על ה CPU כמבצע עשרות-מיליוני פעולות בשנייה בלבד.



"מספרים שכל מתכנת חייב להכיר" (2012). איפה הייתם בקורס מבני-נתונים?!

המחיר הנוסף


בניגוד למשתמע בקורס מבני-נתונים, אלגוריתמים בפועל לא רצים על הלוח או במוחם של מתמטיקאים, אלא על חומרת מחשב. החומרה הזו פועלת על כמה הנחות מאוד חשובות:
  • גישה לדיסק היא מאוד מאוד יקרה (לפני עידן ה SSD היא הייתה מאוד מאוד מאוד יקרה).
  • גישה לזיכרון היא יקרה, ובהחלט לא אקראית - על אף השם "Random Access Memory = RAM". 
  • נתונים, גם מהרשת, גם מהדיסק, וגם משכבות הזיכרון השונות מוגשות בבלוקים רציפים. העלות להביא את הבלוק היא החלק היקר, בעוד ההבדל בין סריקה של בית בודד או את כל הבלוק - הוא לרוב משני עד זניח.
    • אפשר לראות את ההתנהגות הזו בנתונים למעלה, כאשר קריאה של בית בודד מ SSD תיקח 0.15ms בעוד קריאה של מיליון בתים מ SSD (עשרות עד מאות בלוקים) - תיקח רק מעט יותר: כ 1.0ms.
    • שווה מאוד לקרוא ולטפל בנתונים ברציפות, ולא בסדר אקראי
  • למעבדים יש היררכיה של זיכרונות Cache. נתון שלא נמצא ב Cache יובא קודם ל L3 ואז ל L2 ואז ל L1, ולכל הבאה שכזו, יקדם חיפוש ברחבי ה Cache Level שהנתון אכן לא שם.
    • זה בזבוז להביא מילה בודדת בזיכרון, ולכן כל פעם מביאים בלוק זיכרון רציף. גודל הבלוק - תלוי בארכיטקטורת המעבד.





נתחיל בתכלס, ונראה איך זה משפיע עלינו.


קרב ראשון: Vector/ArrayList מול LinkedList


אנחנו להוסיף דינאמית איברית לרשימה. איזה מבנה-נתונים הכי מתאים? Vector (אשתמש בשם הקדום ב ++C) או רשימה משורשרת?

לוקטור יש חיסרון משמעותי שיש להגדיר את גודל הרשימה מראש. אנו מקצים מערך בגודל 16 מקומות, ואז כשהוא מתמלא מקצים מערך חדש בגודל 32 מקומות - ומעתיקים אליו את 16 הערכים שצברנו וכן האלה.

רשימה משורשרת פשוט מוסיפה עוד ועוד ערכים במחיר (O(1 לכל פעולה.

במבחן הבא יצרנו בצד רשימה של מספרים שלמים (int) עם ערכים אקראיים ואז הכנסנו אותם לרשימה (פעם וקטור ופעם רשימה משורשרת) כך שהרשימה תישאר ממוינת. כלומר: אנו כוללים הכנסות במקומות שונים לאורך המערך, כאשר ההגעה למקום הספציפי היא ע"י סריקה של הרשימה מההתחלה על למקום הנכון בצורה סדרתית (לא נשתמש בחיפוש בינארי)

מבחינה אקדמית - הרשימה המשורשרת מנצחת בגדול. היא בנויה להכנסות באמצע מבנה הנתונים וגדילה דינאמית.

בואו נראה מה קורה בפועל:


הממ... לא בדיוק.

הכנסה באמצע הרשימה יקרה משמעותית ברשימה משורשרת, למרות שבאופן תאורטי לוקטור יש דווקא עלות נוספת (העתקת המערך) בכל גדילה. מה קרה פה?!

  • כל אלמנט ברשימה המשורשרת תופס יותר זיכרון: צריך להכניס לזיכרון גם את הערך וגם את המצביע לאלמנט הבא. כנ"ל אם הערך הוא מצביע לאובייקט ולא Int. זה אותו הדבר.
  • בתאוריה: העתקה של מרחבי זיכרון רציפים היא עלות (O(n או משהו יקר אחר.
    בפועל: במעבדים מודרניים יש פקודה יעילה למדי להעתקה של בלוקים של זיכרון. זו איננה פעולה יקרה כ"כ.
  • חיפוש המקום להכנסה ברשימה משורשרת הוא הנקודה החלשה הבולטת של הרשימה המשורשרת: ה nodes של הרשימה מפוזרים אקראית במרחב הזיכרון. כשאנו סורקים את הרשימה (בממוצע n/4 איברים בכל פעם) אנחנו "חוטפים" עוד ועוד cache misses הדורשים לעדכן את זיכרון המטמון.
    • כאשר אנחנו סורקים את הוקטור, ככמט כל בלוק של זיכרון שנביא ל cache - ינוצל במלואו.
    • במקרה של שימוש ב virtual memory (לא נכלל בגרף) - המקרה גרוע הרבה יותר: ייתכן ובגלל קפיצות בין דפים שונים של ה main memory "נחטוף" Page Fault ויהיה עלינו להביא את דף הזיכרון מהדיסק, רחמנא ליצלן! 



שיפור עמודות לטובת הרשימה-המשורשרת

בואו נפרגן לרשימה המשורשרת שהייתה כוכבת (או לפחות: אופציה לגיטימית לשימוש) בקורס "מבני-נתונים". בואו נבצע את המבחן כאשר מכניסים איברים תמיד למקום הראשון ברשימה, וכן לא צריכים לסרוק אותה.

הוקטור יאלץ להעתיק זיכרון כל הזמן בכדי לפנות מקום, בעוד שהרשימה המשורשרת רק תוסיף איברים לרשימה בתסריט האידאלי מבחינתה.

עד כמה עומדת הרשימה המשורשרת למחוץ את הוקטור? בתיאוריה זה אמור להיות אכזרי כלפי הוקטור. בואו נראה בפועל:


אבוי! הרשימה המשורשרת לא מצליחה אפילו במבחן שתוכנן לטובתה. ההכנסות למקומות אקראיים (ופנויים) בזיכרון גוזלים מחיר יקר של עדכון / איפוס caches.

אין כמעט תסריטים של scale בהם הרשימה המשורשרת יכולה לנצח את הוקטור בעולם האמיתי. רק כאשר גודל האלמנט בכל node הוא גדול מאוד (מאות בתים, למשל) ואז גם ה cache מתרפרש מהר בכל מקרה וגם התקורה של ה next-pointer של הרשימה המשורשרת הופכת לזניחה.



בעולם הג'אווה - המשחק משתנה

הדוגמאות מלמעלה הן ב ++C, וכנראה מייצגות גם את מה שמתרחש ב Rust או Go. שפות שעובדות עם הזיכרון מול מערכת ההפעלה ומושפעות ישירות מארכיטקטורת המעבד.


בג'אווה (או #C) ואולי גם בפייטון / רובי - הדברים מעט שונים. יש מפרשן או JVM/CLR שנמצאים בין מערכת ההפעלה לקוד. יש שימוש מאסיבי ב Heap - שאינו רציף.

איך הדברים נראים בג'אווה? אולי שם ההבדל בין וקטור לרשימה משורשרת נמחק?
בואו נבדוק שוב את המקרה של הכנסת איבר למקום הממוין ברשימה.


אין לי גרף מתאים (הנה המקור לנתונים), אבל מאיסוף ידני של הנתונים ממבחן ב ++C, ב 20,000 איברים היו התוצאות 617 מילישניות לרשימה משורשרת, ו 234 מילישניות לוקטור - שזה יחס דומה.

ה DynamicIntArray, אם תהיתם, הוא מימוש של ArrayList המאחסן איברים מסוג int (פרמיטיב) ולא ב Integer (אובייקט) - ולכן הוא ידידותי באמת ל cache. הנתונים באמת שמורים במערך רציף (כמו ב ++C) והם לא reference לאובייקט שתלוי ב Heap (שאותו ג'אווה אכן משתדלת למלא בצורה רציפה).


המבחנים הנ"ל בוצעו על חומרה ישנה בת כמעט עשור. בואו נראה מה קורה על חומרה עדכנית:


עם השנים ה caches של המעבדים גדלים ומתייעלים היתרון של הוקטור, המבוסס על זיכרון רציף - הולך וגדל.


את זה לא סיפרו לי בקורס מבני-נתונים!




סיכום ביניים


רשימה משורשת היא מבנה נתונים עוין ל caches וזיכרון רציף. אין כמעט סיבה להשתמש בה, מלבד לטובת קריאות של קוד. זכרו את חוק המספרים הקטנים! 

אם יש לי כ 50 איברים - אז יאללה, אדרבא. כאשר יוצרים רשימה קטנה יש גם סבירות גבוה יותר שהאיברים בה יכנסו לאותו בלוק (או בלוקים בודדים) של זיכרון - וכך היא תהיה פחות עוינת ל caches.


רשימה משורשרת היא לא מבנה הנתונים המוכר היחידי שעוין caches. בעצם אלו כל המבנים המבוססים קישורים בזיכרון כמו עצי חיפוש בינריים וגראפים.

מה עושים?

במקום עצי חיפוש בינריים, משתמשים ב B-Tree. עץ חיפוש שבו כל node מותאם לגודל בלוק בדיסק (ומכאן: כמה בלוקים של זיכרון, בהתאמה). כל בלוק מכיל רשימה של מצביעים רלוונטיים. האופטימיזציה היא לצמצום הגישה לבלוקים, כך שכל פעם טוענים בלוק מהדיסק / זיכרון - מנצלים אותו ביעילות ע"י שימוש בסריקה רציפה.

מתכנתים במערכות כלליות, עשויים לא להזדקק באמת למבני-נתונים אופטימליים רוב הזמן. את העבודה הקשה עושים בסיסי-נתונים.

מפתחים של בסיסי נתונים משתמשים ב B-Tree (או B+Tree - וריאציה מעט שונה) כדרך קבע כעצי חיפוש למשל: בשימוש באינקסים.


פעם נתקלתי במימוש שבאמת הייתה בו חשיבות לשימוש ברשימה משורשרת. מה עשינו? הקצנו את הזיכרון עבור כל הרשימה בצורה רציפה (כמו וקטור) והשתמשנו רק בו. דבר דומה עשה חבר שדיברתי איתו - מה שגרם לי להבין שזה common sense ולא המצאה גדולה. חיפוש פשוט בגוגל העלה מימוש שכזה כקוד פתוח.

מה עושים בגרפים? אני לא מכיר באופן אישי, אבל אני מניח שגם Neo4J או AWS Neptune מצאו את הדרך שלהם לבנות את מבנה הנתונים החשוב והנהדר הזה Graph - בצורה שאיננה עוינת לזיכרון. או לפחות: עוינת פחות ככל האפשר.


יש עוד כמה דברים שרציתי לדבר עליהם, אבל הפוסט מתארך - אז נמשיך בפעם הבאה.

שיהיה בצלחה!





קישורים רלוונטיים:

"מה כל מתכנת צריך לדעת על זיכרון" - מסמך בן 100 עמודים שמסביר הכל, אבל לא נראה לי שווה את ההשקעה. במיוחד כי דברים גם משתנים.


2018-09-24

ג'אווה 11 - 9: שאלות ותשובות - שכדאי להכיר

עולם הג'אווה הוא בעיקרון עולם משעמם למדי.

ג'אווה בהחלט הייתה טכנולוגיה חדשנית ומרגשת - אך זה היה לפני כ 20 שנה.
מאז גרסאות של ג'אווה שוחררו בקצב של אחת לכמה שנים, עם חידושים מעטים - יחסית לשפות אחרות: #C המקבילה, רובי, Go, ג'אווהסקריפט כמובן (שלא בטוח שקצב השינויים היה רק לטובה), סקאלה, קוטלין, Closure, ועוד.
כבר לפני כמה שנים שמעתי על השוואה בין ג'אווה לקובול - שפה שחלשה על חלקים גדולים מהתעשייה, אך אם הזמן הפכה למיושנת להחריד.



החברה שפיתחה את ג'אווה במקור (Sun Microsystems) כשלה עסקית ונרכשה ע"י Oracle בשנת 2010 - שקיבלה את הנכס הנדיר שנקרא ג'אווה, אך נכס שגם דורש השקעה רבה ומחולק בחינם בעולם.

הגרסה המשמעותית האחרונה של ג'אווה הייתה גרסה 8, ששוחררה ב 2014 - ואכן הציגה כמה חידושים יפים.
הגרסה המשמעותית הבאה של ג'אווה, גרסה 11 - עומדת להיות משוחררת בימים אלו והיא מייצרת סוג של "רעידת אדמה קטנה" בסביבה שיציבות והמשכיות - היו על לאחרונה עקרונות ברזל שלה.

כשאנו מדברים על שינוים אנחנו לא מדברים רק על שינוים בשפת ג'אווה עצמה - אלא בעיקר על שינויים בפלטפורמת ה JVM, פלטפורמה שמשרתת את Scala, Closure, קוטלין, Groovy, JRuby, ועוד. חשוב לזכור שה JVM הוא גדול יותר מג'אווה.


ג'אווה גרסה 9 היא נקודת ציון בהיסטורית הגרסאות של ג'אווה - למרות שהיא עצמה חסרת חשיבות לחלוטין. ג'אווה 9 החלה כמה שינויים גדולים המגיעים לידי מימוש בגרסה 11 שיוצאת עכשיו (ספטמבר 2018):
  • Module System - מודולריזציה בתוך ה JDK, ולראשונה הסרה של קוד ישן (בכמויות לא-מבוטלות). המהלך מציע כמה יתרונות טכניים חשובים - אך משמעותו שלראשונה ה JDK הופך ל non-backward-compatible, מה שעשוי לפגוע לאורך שנים בקצב האימוץ של הגרסאות החדשות. הסרת קוד ישן מה JDK מתוכננת להמשיך בגרסאות עתידיות של ג'אווה גם כן.
  • New release cycle - הכולל releases "קטנים" כל חצי שנה, וגרסאות LTS.
  • גביית דמי-שימוש עבור עדכוני באגים ועדכוני-אבטחה - אורקל עושה מהלך משמעותי על מנת לסבסד את תחזוקת ג'אווה ע"י משתמשיה, ואולי אף יוביל לרווחים משמעותיים לאורקל עצמה.
    • יישור קו בין OracleJDK ל OpenJDK - צעד משנה למהלך הנ"ל.

גרסה 9 הציגה לראשונה התחלה של כל השינויים הנ"ל, אך הוכרז מראש שזו גרסה שתתמך לחצי-שנה בלבד, וגרסת היעד לתמיכה ארוכה (LTS, כלומר Long-term support) תהיה גרסה 11.
מאז יצאה גם ג'אווה 10 - גם היא גרסה מינורית ולא כ"כ נוספת, שסיפקה עוד כמה התקדמויות בנושאים הנ"ל (ומספר תוספות קטנות לשפה) - אבל חשיבות הגרסה הייתה בהיותה הכנה גנרלית לשחרור גרסה 11.

גרסה 11 היא זו שתעמת את קהילת ה JVM בפעם הראשונה באמת עם מציאות חדשה, והדילמות שנובעות ממנה. זו גרסת LTS שתתמך עכשיו עד 8 שנים קדימה, גרסת ג'אווה המשמעותית שתהיה זמינה בשנים הקרובות.
  • האם קהילת הג'אווה שהייתה רגילה לג'אווה חינמית תתחיל לשלם ל Oracle עבור עדכונים ותמיכה?
  • האם החלק הארי של הקהילה יישאר על גרסה 8, או שנראה אימוץ משמעותי גם של גרסאות 11 והלאה, כבר בשנים הקרובות?
נותר רק להמתין ולראות כיצד הדברים יתפתחו.



בואו נתחיל לסגור פינות...


אם התיאור הנ"ל משאיר אתכם עם סימני שאלה - זה מובן לחלוטין. אנסה להתחיל ולענות על השאלות העיקריות שצצות מהשינויים הללו.


מה ההבדל בעצם בין OpenJDK ו OracleJDK? ומה השתנה?

החל מגרסה 7 החלה להיות משוחררת ההפצה של OpenJDK, שהיא הפצה תחת רישיון GNU GPL v2 עם החרגה ל linking - רישיון Open Source חופשי, המאפשר שימוש בג'אווה בכדי לכתוב תוכנה מסחרית ולמכור אותה.

היוזמה הייתה עוד של חברת Sun, והגנה על האופי הפתוח של ג'אווה.

אורקל מציעה גם "הפצה משופרת" של ג'אווה ברישיון מסחרי בשם OracleJDK הכוללת עוד כמה ספריות (בעיקר מסביב לטכנולוגיית JavaFX, שלא תפסה) ו tooling שהגיע עם ההפצה כמו כלי ניטור ובדיקות ביצועים. הכוונה הייתה כנראה כן להפיק הכנסות מג'אווה - אך ההצלחה לא הייתה גדולה.

החל מגרסה 9, הפצות ה OpenJDK וה OracleJDK מתאחדות. זה לא יקרה ע"י הסרה של תוכן מתוך ה OracleJDK אלא בעיקר ע"י פתיחה של התוספות שאורקל סיפקה להיות OpenSource. כמה תכונות עם רישיון מסחרי (כמו ה Java Web Start) אכן יוסרו מה JDK, אבל בעיקר בגלל שהאימוץ שלהן נמוך למדי. התהליך מסתיים עכשיו בשחרור גרסה 11, בו תוכן ההפצות הוא זהה - וההבדל ביניהן הוא רק הרישיון.

למה לי לשלם לאורקל כסף על מה שזמין בחינם וזוכה לתמיכה של הקהילה (כלומר: OpenJDK)?

שאלה מצוינת! ביחד עם ההחלטה ליישר בין ההפצות, יש החלטה יותר משמעותית לצמצם את התמיכה שאורקל סיפקה ל OpenJDK בחינם. כלומר: קיבלנו (אנחנו הקהילה) עוד כמה ספריות וכלים (כגון Flight Recorder ו Mission Control - כלי בעל יכולות לניטור ביצועים על מערכת חיה לאורך), אבל אנחנו מפסיקים לקבל מאורקל תיקוני באגים ותיקוני אבטחה זמן ארוך לאחר שגרסת הג'אווה שוחררה. מעתה - התיקונים שאורקל תספק ל OpenJDK בחינם יהיו רק לפרקי זמן קצרים לאחר שחרור הגרסה.

אם הדבר היה נתון לבחירת המשתמשים - נראה שכמעט כל המשתמשים היו מעדיפים את המצב הקודם. במהלך הזה אורקל "דוחפת" את המשתמשים לרכוש ממנה (או מספקי צד-שלישי) תמיכה בתשלום לג'אווה.
להגנתה של אורקל יאמר שהיא כנראה משקיעה בג'אווה הרבה יותר ממה שהיא מרוויחה ממנה, וזה סה"כ לגיטימי להפסיק להיות פילנטרופ.


כיצד אורקל עומדת לצמצם את התמיכה שמסופקת ב OpenJDK וכיצד זה קשור למחזור שחרור הגרסאות החדש?

ג'אווה 9 הציגה מחזור חדש של שחרור גרסאות שנראה כך:



לגרסה 8 של ג'אווה, אורקל סיפקה תיקוני-באגים ותיקוני אבטחה בחינם למשך כחמש שנים. התמיכה מסתיימת, חשוב לציין, בינואר 2019. בקרוב מאוד.

החל מגרסה 9, אורקל מתחייבת שחרר גרסה פעמיים בשנה, מה שיאפשר לשחרר תוספות לשפה וה JVM בתדירות גבוהה יותר.
הבחירה במספרים שלמים עבור הגרסאות הללו לא מצביע על כמות התוכן שיתווסף, אלא על אופנה של השנים האחרונות ״לרוץ״ עם מספרי גרסאות (למשל: ראקט הייתה בגרסה 0.14 לפני שלוש שנים - אך כיום היא כבר בגרסה 16.5. אנגולר הייתה בגרסה 2.0 ב 2014, אך עוד מעט משתחררת גרסה 7.0).

מכיוון שה JVM הוא תשתית קריטית למערכת שרצה עליו, סביר שרוב הארגונים לא יסכימו לעדכן גרסאות מיד לכשיצאו (אלא אם יוכח שהגרסאות החדשות שמשתחררות הן יציבות בצורה מופלאה - מה שיהווה הפתעה), ולכן יהיו גם גרסאות LTS שימשיכו לקבל עדכונים ותיקונים לטווח ארוך יותר, גם כאשר יש גרסה חדשה יותר זמינה.
רוצים תזכורת מה יכול להשתבש בגרסה חדשה של JDK? הנה דוגמה לבאג שהוצג בגרסה 9 של ה JDK:



תיקוני באגים ותיקוני אבטחה ישוחררו רק על הגרסה האחרונה (בחינם), או על גרסאות LTS (בתשלום). אם חשוב לנו לקבל עדכונים שוטפים יש לנו שלוש ברירות:
  • לעדכן את גרסת ה JVM/ג'אווה שלנו כל 6 חודשים כמו שעון. כל יום שאנו לא על הגרסה האחרונה הוא יום שבוא יכול להשתחרר עדכון אבטחה קריטי שלא נקבל.
  • להשתמש ב OracleJDK בתשלום ולעבוד על גרסאות LTS על מנת לקבל עדכוני אבטחה שוטפים. התשלום הוא כ $2.5 דולר לחודש ל Desktop/Laptop ו $25 לחודש לכל Core פיסי של שרת [1].
  • לקנות תמיכה מספק צד-שלישי, כגון Azul או RedHat כאשר הם אלו שיספקו תיקונים לבאגים ובעיות אבטחה. כל חברה - עם המדיניות שלה.
    • למשל, ל Azul יש תוכנית בשם Medium Term Support (בקיצור MTS), שבה היא תתמוך לאורך שנתיים וחצי בכל גרסה שניה של ג'אווה שאיננה LTS. יתרון גדול בגישה הזו היא חפיפה בתמיכה בין גרסאות ה MTS - מה שלא קיים בתמיכה ה"חינמית" של אורקל .
    • חשוב לספקי צד-שלישי יספקו גם תמיכה לג'אווה גרסה 8 לעוד כמה שנים - היכן שבכל מקרה רוב התעשייה צפויה להיות בזמן הזה. הנה למשל הצהרה של יבמ.
עוד פרט שכדאי לציין הוא שהרישיון של OracleJDK (לפחות לגרסה 11) מאפשר שימוש ללא הגבלה לצורך פיתוח. התשלום הוא רק עבור שימוש production (שאותו ישלם לקוח המריץ את הקוד On-Premises, או ספק SaaS - עבור המכונות שהוא מריץ).



האם ההצהרה על שחרור גרסת ג'אווה כל 6 חודשים היא לא מטעה? בעצם נראה שגרסאות ה LTS הן אלו שמשנות - והן ימשיכו בקצב אטי יחסית של כל 3 שנים?

באמת היא כנראה איפשהו באמצע. אורקל מצהירה שכל גרסה שתשוחרר תעבור Quality cycle מפרך כמו של גרסה מ'אגורית עד היום. כמו כן יש הצהרה שהיא תשחרר לפחות שני עדכונים (תיקוני באגים ואבטחה) מינוריים - לכל גרסה שתצא. גרסאות שאינן LTS  הן לא גרסאות "בטא" - ע"פ ההצהרה.
סביר להניח ששחרור גרסאות תכוף יאפשר לג'אווה להתפתח מהר יותר - אך במידה. מי שצפוי לעבוד באמת עם גרסאות שאינן LTS הוא רק פלח צר של Early adopters (למשל: סטארטאפ בתחילת דרכו), במיוחד בשנים הראשונות.


האם מה שאורקל עושה הוא "בסדר"? האם זה לא "מחטף מרושע"?

אין לי תשובה ברורה. בד"כ כשארגון רוצה לקבל הכנסות מפרויקט Open Source - הוא מציע ערך נוסף על הקיים בכדי לקבל עליו תשלום: Premium support או יכולות נוספות (למשל: Redis modules).
אורקל בעצם ממשיכה לגבות תשלום על תמיכה, תוך כדי שהיא מפסיקה בפועל את התמיכה-בחינם שהיא סיפקה לאורך שנים רבות. אם היה מדובר בפרויקט קטן - זה אולי לא היה מתקבל בשמחה, אך זה לא היה עושה הרבה רעש. ג'אווה היא אחת מהתשתיות (ביחד עם לינוקס, MySQL ועוד כמה) המשמעותיות ביותר המסופקות בחינם. ההשפעה - היא משמעותית!

נקודת סולידריות מסוימת היא כן ההשקעה הגדולה שאורקל סיפקה לאורך שנים - ובחינם.
מנקודת מבט מסוימת, אורקל מפנה מקום לקהילה / חברות צד-שלישי לקחת על עצמן את האחריות במתן תמיכה לג'אווה בחינם. יבמ, גוגל, וסאפ הן 3 חברות גדולות, עם עניין עמוק בג'אווה שיכלו לעשות זאת - אך אף אחת לא התנדבה.

אורקל כמובן לא מוותרת על ג'אווה - ומשאירה את "גרעין השליטה" על הטכנולוגיה - אצלה. אפשר לומר שמתקבל על הדעת להניח שהמהלך של אורקל בא לצמצם עלויות שאורקל לא חפצה בהן, וזו לא "תוכנית זדונית לסחוט מהעולם כמה כסף שאפשר". לא נראה שאורקל ניסתה לחסום ספקי צד-שלישי לתת תמיכה בתשלום לג'אווה, וכך בעצם להתחרות ב OracleJDK offering שלה.

כאב ראש לאנשים רבים בעולם - ככל הנראה ייגרם בכל זאת.



דיברנו על איזו בעיה של תאימות לאחור, במה בדיוק מדובר?


ג'אווה 9 הציגה לראשונה את ה Module System (הידוע לשעבר כ Project Jigsaw או super packages) - כלי מודולריזציה בשפה.

packages, כלי ארגון הקוד עד עכשיו, הוא כלי נחמד - אבל עם מגבלה עיקרית: הוא עוזר לארגן את תוך ה package (למשל: ניתן להגדיר מחלקות כ private ל package) אך הוא לא מתמודד עם ניהול תלויות בין packages.
  • אין לי יכולת אמיתית לחשוף מחלקות מסוימות בצורה סלקטיבית ל packages אחרים - הרזולוציה היא רק private או public.
  • אין ניהול רציני של התלויות בין ה packages. 
    • כל מחלקה יכולה להוסיף כל תלות שהיא רוצה ל package אחר. כלומר: התלויות בין ה packages מגודרות שוב ושוב, ומפוזרות בין ה classes השונים של המחלקה - שזה לא DRY, וקשה מאוד לבקרה.
    • כל מחלקה יכולה להוסיף את עצמה לכל package - כך שאין שליטה ממה מורכב בדיוק ה package. זו בעיה משנית.
ה Module System (בקיצור: MDS) של ג'אווה מאפשר להגדיר קובץ בשם module-info.java המגדיר מודול ואת התלויות שלו. הנה דוגמה לכזו הגדרה:

module monitor.rest {
    requires spark.core;
    requires monitor.statistics;
    exports monitor.rest;
}

הקומפיילר יתייחס להגדרות ויאכוף אותן (יופי!).
חשוב לציין שמילים שמורות חדשות, כמו module ו requires הן בעלות משמעות רק בקובץ ה module-info, ולא ישפיעו על שאר קוד הג'אווה.

קישורים: Java 9 modules cheat sheet, מדריך, ועוד כמה מקורות טובים

ג'אווה השתמשה ב MDS בעצמה עבור ה JDK. במידה מסוימת זה היה סוג של "eat your own dog food", אבל מצד שני - זה היה צורך מרכזי של ה JDK עצמו, שעם השנים התלויות הרבות והמיותרות בו הפכו אותו לקשה מאוד לתחזוקה. באורקל טוענים שזו אחת הסיבות לשחרור הכל-כך איטי של שינויים בג'אווה.

במעבר מ Java 8 ל Java 11, אורקל חילקה את ה JDK לכ 80 מודולים שונים, חלקים יצאו מה JDK ויהיו זמינים כספרייה נפרדת, או שנפרד מהם לגמרי. הנה רשימת היכולות שהוסרו מה JDK:
  • corba - לזקנים ביננו, שזכו להכיר את המפלצת.
  • java.transaction (מדובר בטרנזקציות אפליקטיביות, לא של JDBC. כאלו כמו שהיו ב EJB גרסה 1).
  • java.xml.ws וספריות נוספות של טיפול ב XML (למשל JAXB). בתחילת שנות ה 2000 התייחסו ל XML כ "פורמט שהולך לתאר את כל המידע בעולם". תזכרו את זה כשאתם מתחילים להתלהב מטכנולוגיה חדשה.
  • JavaFX - טכנולוגיית Desktop UI של ג'אווה שלא ממש הצליחה. לג'אווה יש היסטוריה מרשימה ביותר של טכנולוגיות UI כושלות (אפשר לדבר בהזדמנות גם למה...)
    • JavaFX תמשיך להיות זמינה כמודול עצמאי שלא קשור ל JDK.
  • Applets
  • Java WebStart (טכנולוגיה להתקנה ועדכון אוטומטי של אפליקציות ג'אווה)
  • Nashorn שהחליף את Rhino כמפרשן JavaScript שרץ על גבי ג'אווה ומסופק כחלק מה JDK. 
    • נשהורן הוא חדש, אך שפת ג'אווהסקריפט מתפתחת בקצב כ"כ מהיר, שהייתה מספיקה גרסה אחת בכדי להבין שלא ישים לתחזק עוד מפרשן JavaScript כחלק מה JDK.
בנוסף, בג'אווה 9 עד 11 הסירו כמה פונקציות שהיו deprecated לאורך זמן רב. זו הפעם הראשונה בהיסטוריה של ג'אווה שקוד באמת מוסר מה JDK.
אם אתם משתמשים באחת מהטכנולוגיות או ה APIs שהוסרו מה JDK - יהיה עליכם ליבא אותן בצורה מפורשת (למשל: באמצעות מייבן) או למצוא חלופה (למשל ל Applets או Nashron - שלא ייתמכו יותר).

המודולריזציה של ה JDK תסייע גם להלחם בצריכת הזיכרון הגבוהה של ג'אווה, ובגודל ה distributables. ג'אווה עובדת עם Dynamic linking (קישור של קוד בזמן ריצה, ולא בזמן קומפילציה). יש בכך כמה יתרונות (קל מאוד לייצר Plugin architecture, ניתן לבצע אופטימזציות מסוימות) אבל המחיר הוא קבצי jars גדולים מהנדרש (אנו כוללים את כל הקוד בספריה, אפילו אם אנו זקוקים רק ל 5% ממנה) וגם צריכת זיכרון גבוהה יותר.

בעולם ה micro-services וה FaaS (למשל: AWS lambda) - התכונות הללו הן מגבלה רצינית של ג'אווה. למבדה קטנה בג'אווה יכולה בקלות לדרוש jar file של עשרות MB ולצרוך מאות MBs של זיכרון. למבדה דומה ב ++C או Go תדרוש שבריר מהמשאבים של ג'אווה. כנ"ל לגבי ג'אווהסקריפט או פייטון - אבל מסיבות קצת שונות.

ג'אווה 9 הוסיפה כלי בשם jlink המאפשר לבנות אפליקציית ג'אווה עם static linking. השימוש העיקרי הוא FaaS או הפצה של ג'אווה למכשירים עם מגבלות במשאבים. החיסרון של jlink הוא שהתוצר לא ניתן לעדכון ללא החלפה מלאה של התוצר הבינרי. היום ניתן לעדכן גרסאת JRE ולקבל עדכוני אבטחה / באגים קריטיים מבלי לעדכן את קוד האפליקציה. אפליקציה שנבנתה עם jlink תדרוש בנייה מחדש ו deploy מחדש - על מנת להחיל את אותם העדכונים.





הסכנה שבשינוי


מעבר בין גרסאות ג'אווה בדרך-כלל לא היה עניין גדול, אולי צריך להחליף בקוד משתנים ופונקציות השתמשו בשם שם מילה שמורה חדשה.

המעבר בין ג'אווה 8 לג'אווה 11, או בין JDK 8 ל JDK 11 (למי שמשתמש בשפת JVM שאיננה ג'אווה) - עומד להיות עניין גדול יותר.

רבים מהכלים וספריות של ג'אווה דרשו עדכון על מנת לתמוך ב MDS. סביר להניח שעל מנת להשתמש בג'אווה 11 בעצמכם, יהיה עליכם לעדכן גרסאות של ספריות לגרסאות שעשו כבר את המעבר.

כפי שאתם יודעים, עדכון גרסה של ספריה הוא לפעמים שינוי של שורה בכלי ניהול התלויות (גריידל או מייבן, למשל), אבל לעתים נדרשים שינויים גדולים בקוד - שיכולים להיות מורכבים. כל השינויים הללו שדחינו כבר חודשים רבים...

הסכנה שבחובה לעדכן, אפילו אם זה שינוי קטן, הוא שספריה שאתם תלויים בה - איננה מעודכנת. לבחור שמתחזק אותה לא הייתה חצי שעה לסגור את הסיפור קצה לקצה.

הבעיה הזו היא טרנזיטיבית, כך שאם אתם משתמשים בספריה שמשתמשת בספריה, שמשתמשת בספריה אחרת שלא עודכנה - יכול להיות שנתקעתם.


בואו ניזכר שנייה מה קרה בפייטון:

ראשי הקהילה תיארו זאת כך:

"באותה תקופה שעבדנו על פייטון 3 - השימוש בפייטון בעולם צמח בקצב מהיר מאוד. הנחנו שבשל המגמה, יהיה יותר קוד בפייטון 3 מאשר בפייטון 2 תוך מספר שנים - ולכן החלטנו "לספוג את הכאב" שבמעבר לא-תואם לאחור בכוונה לייצר עתיד טוב יותר לקהילת הפייטון. היום ברור שיעבור עוד עשור או יותר - עד שיהיה יותר קוד פייטון 3 מאשר קוד פייטון 2".

התוכנית לא כ"כ הצליחה: בסיסי קוד שברמת תחזוקה / שליטה בקוד גבוהה - היה ניתן להמיר במאמץ לא גדול - אבל המפתחים לא המירו את הקוד - כי ספריות רבות שהשתמשו בהן עדיין לא עברו לפייטון 3.

מתחזקי ספריות ותיקות כבר הפחיתו את ההשקעה בספריה - ולא היו מוכנים לעשות את המעבר. גם להם היו לפעמים ספריות אחרות שהם תלויים בהן - שלא ביצעו מעבר לפייטון 3. ה Distros העיקריים של לינוקס המשיכו להיות מסופקים עם פייטון 2 - וחוזר חלילה.

וכך - כל העגלה נתקעה. קריאה לקהילה לבצע את המעבר לא נענתה בחיוב, או לפחות לא בקצב מהיר. הוקמו אתרים כמו http://py3readiness.org ו http://python3wos.appspot.com שמדדו ועודדו - את האימוץ של פייטון 3.

פייטון 3 שוחררה ב 2008, אבל לקח בערך עשור עד שרוב הקהילה הצליחה לעשות את המעבר.

-----------------


השינויים הנדרשים במעבר מג'אווה 8 לג'אווה 11 - הם קטנים יותר משמעותית מאלו שהיו בפייטון 3. בעיקר צריך לעדכן תלויות של מודולים ומחלקות, בעוד בפייטון היו נדרשים שינויים בקוד עצמו = הרבה יותר עבודה.
המודעות לסיכונים, כנראה גדולה - הסיפור של פייטון 3 הדהד בכל התעשייה במשך שנים ככישלון צורב.

האם המעבר לג'אווה יהיה מהיר יחסית, או שיגרר לאורך שנים?

נחכה ונראה.

יש, אגב, אנשים שנבהלים ומבהילים מהסיטואציה, במיוחד לאור הפסקת התמיכה בג'אווה 8 המתוכנן לינואר 2019.
אני אישית לא מודאג כל-כך מעניין התמיכה. אם אני הייתי אורקל, הייתי רוצה להלחיץ קצת את התעשייה להתחיל ולבצע את המעבר, אך ברגע האמת - הייתי ממשיך לספק עדכונים קריטיים גם מעבר. אם אורקל לא יעשו זאת - פתרונות אחרים יימצאו. ככל שהקהילה עשירה יותר - סביר יותר שימצאו עוד איזונים ובלמים שיגנו עליה מפני התדרדרות.

קישורים: מדריך לעדכון פרויקט מייבן לג'אווה 11.



שיהיה בהצלחה!



----

[1] החישוב הוא באמת יותר מורכב, ופחות קל להבנה. לארכיטקטורות שונות של חומרה, יש הגדרה שונה למהו "Processor". רישיונות הוא בד"כ נושא סבוך בחברות Enterprise - אז אל תניחו שהסיבוכיות הזו היא "תרגיל" של אורקל.